diff --git a/services/web/frontend/js/features/ide-react/hooks/use-editing-session-heartbeat.ts b/services/web/frontend/js/features/ide-react/hooks/use-editing-session-heartbeat.ts index 521596bcd4..a30b1790ae 100644 --- a/services/web/frontend/js/features/ide-react/hooks/use-editing-session-heartbeat.ts +++ b/services/web/frontend/js/features/ide-react/hooks/use-editing-session-heartbeat.ts @@ -1,7 +1,6 @@ import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' import { EditorType } from '@/features/ide-react/editor/types/editor-type' -import { reportCM6Perf } from '@/infrastructure/cm6-performance' import { putJSON } from '@/infrastructure/fetch-json' import { debugConsole } from '@/utils/debugging' import moment from 'moment' @@ -10,23 +9,9 @@ import useEventListener from '@/shared/hooks/use-event-listener' import useDomEventListener from '@/shared/hooks/use-dom-event-listener' function createEditingSessionHeartbeatData(editorType: EditorType) { - const segmentation: Record = { + return { editorType, } - const cm6PerfData = reportCM6Perf() - - // Ignore if no typing has happened - if (cm6PerfData.numberOfEntries > 0) { - for (const [key, value] of Object.entries(cm6PerfData)) { - const segmentationPropName = - 'cm6Perf' + key.charAt(0).toUpperCase() + key.slice(1) - if (value !== null) { - segmentation[segmentationPropName] = value - } - } - } - - return segmentation } function sendEditingSessionHeartbeat( diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx index 5aaa9e9b95..7aeb19d2e6 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx @@ -7,7 +7,6 @@ import CodeMirrorSearch from './codemirror-search' import { CodeMirrorToolbar } from './codemirror-toolbar' import { CodemirrorOutline } from './codemirror-outline' import { CodeMirrorCommandTooltip } from './codemirror-command-tooltip' -import { dispatchTimer } from '../../../infrastructure/cm6-performance' import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import { FigureModal } from './figure-modal/figure-modal' import { ReviewPanelProviders } from '@/features/review-panel-new/context/review-panel-providers' @@ -45,20 +44,16 @@ function CodeMirrorEditor() { // create the view using the initial state and intercept transactions const viewRef = useRef(null) if (viewRef.current === null) { - const timer = dispatchTimer() - // @ts-ignore (disable EditContext-based editing until stable) EditorView.EDIT_CONTEXT = false const view = new EditorView({ state, dispatchTransactions: trs => { - timer.start(trs) view.update(trs) if (isMounted.current) { setState(view.state) } - timer.end(trs, view) }, }) viewRef.current = view diff --git a/services/web/frontend/js/infrastructure/cm6-performance.ts b/services/web/frontend/js/infrastructure/cm6-performance.ts deleted file mode 100644 index e3a4d3ec51..0000000000 --- a/services/web/frontend/js/infrastructure/cm6-performance.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { Transaction } from '@codemirror/state' -import { EditorView } from '@codemirror/view' -import { round } from 'lodash' -import grammarlyExtensionPresent from '../shared/utils/grammarly' -import getMeta from '../utils/meta' -import { debugConsole } from '@/utils/debugging' - -const TIMER_START_NAME = 'CM6-BeforeUpdate' -const TIMER_END_NAME = 'CM6-AfterUpdate' -const TIMER_DOM_UPDATE_NAME = 'CM6-DomUpdate' -const TIMER_MEASURE_NAME = 'CM6-Update' -const TIMER_KEYPRESS_MEASURE_NAME = 'CM6-Keypress-Measure' - -let latestDocLength = 0 -const sessionStart = Date.now() - -let performanceOptionsSupport = false - -// Check that performance.mark and performance.measure accept an options object -try { - const testMarkName = 'featureTestMark' - performance.mark(testMarkName, { - startTime: performance.now(), - detail: { test: 1 }, - }) - performance.clearMarks(testMarkName) - - const testMeasureName = 'featureTestMeasure' - performance.measure(testMeasureName, { - start: performance.now(), - detail: { test: 1 }, - }) - performance.clearMeasures(testMeasureName) - - performanceOptionsSupport = true -} catch (e) {} - -let performanceMemorySupport = false - -function measureMemoryUsage() { - // @ts-ignore - return performance.memory.usedJSHeapSize -} - -try { - if ('memory' in window.performance) { - measureMemoryUsage() - performanceMemorySupport = true - } -} catch (e) {} - -let performanceLongtaskSupported = false -let longTaskSinceLastReportCount = 0 - -// Detect support for long task monitoring -try { - if (PerformanceObserver.supportedEntryTypes.includes('longtask')) { - performanceLongtaskSupported = true - - // Register observer for long task notifications - const observer = new PerformanceObserver(list => { - longTaskSinceLastReportCount += list.getEntries().length - }) - observer.observe({ entryTypes: ['longtask'] }) - } -} catch (e) {} - -function isInputOrDelete(userEventType: string | undefined) { - return ( - !!userEventType && ['input', 'delete'].includes(userEventType.split('.')[0]) - ) -} - -// "keypress" is not strictly accurate; what we really mean is a user-initiated -// event that either inserts or deletes exactly one character. This corresponds -// to CM6 user event types input.type, delete.forward or delete.backward -function isKeypress(userEventType: string | undefined) { - return ( - !!userEventType && - ['input.type', 'delete.forward', 'delete.backward'].includes(userEventType) - ) -} - -export function dispatchTimer(): { - start: (trs: readonly Transaction[]) => void - end: (trs: readonly Transaction[], view: EditorView) => void -} { - if (!performanceOptionsSupport) { - return { start: () => {}, end: () => {} } - } - - let userEventsSinceDomUpdateCount = 0 - let keypressesSinceDomUpdateCount = 0 - const unpaintedKeypressStartTimes: number[] = [] - - const start = (trs: readonly Transaction[]) => { - const keypressStart = performance.now() - - trs.forEach(tr => { - const userEventType = tr.annotation(Transaction.userEvent) - - if (isKeypress(userEventType)) { - unpaintedKeypressStartTimes.push(keypressStart) - } - }) - - performance.mark(TIMER_START_NAME) - } - - const end = (trs: readonly Transaction[], view: EditorView) => { - performance.mark(TIMER_END_NAME) - - let anyInputOrDelete = false - - trs.forEach(tr => { - const userEventType = tr.annotation(Transaction.userEvent) - - if (isInputOrDelete(userEventType)) { - anyInputOrDelete = true - ++userEventsSinceDomUpdateCount - - if (isKeypress(userEventType)) { - ++keypressesSinceDomUpdateCount - } - - performance.measure(TIMER_MEASURE_NAME, { - start: TIMER_START_NAME, - end: TIMER_END_NAME, - detail: { userEventType, userEventsSinceDomUpdateCount }, - }) - } - }) - - if (anyInputOrDelete) { - // The `key` property ensures that the measurement task is only run once - // per measure phase - view.requestMeasure({ - key: 'inputEventCounter', - read() { - performance.mark(TIMER_DOM_UPDATE_NAME, { - detail: { keypressesSinceDomUpdateCount }, - }) - userEventsSinceDomUpdateCount = 0 - keypressesSinceDomUpdateCount = 0 - - const keypressEnd = performance.now() - - for (const keypressStart of unpaintedKeypressStartTimes) { - performance.measure(TIMER_KEYPRESS_MEASURE_NAME, { - start: keypressStart, - end: keypressEnd, - }) - } - unpaintedKeypressStartTimes.length = 0 - }, - }) - } - - latestDocLength = trs[trs.length - 1].state.doc.length - } - - return { start, end } -} - -function calculateMean(durations: number[]) { - if (durations.length === 0) return 0 - - const sum = durations.reduce((acc, entry) => acc + entry, 0) - return sum / durations.length -} - -function calculateMedian(sortedDurations: number[]) { - if (sortedDurations.length === 0) return 0 - - const middle = Math.floor(sortedDurations.length / 2) - - if (sortedDurations.length % 2 === 0) { - return (sortedDurations[middle - 1] + sortedDurations[middle]) / 2 - } - return sortedDurations[middle] -} - -function calculate95thPercentile(sortedDurations: number[]) { - if (sortedDurations.length === 0) return 0 - - const index = Math.round((sortedDurations.length - 1) * 0.95) - return sortedDurations[index] -} - -function calculateMax(numbers: number[]) { - return numbers.reduce((a, b) => Math.max(a, b), 0) -} - -function clearCM6Perf(type: string) { - switch (type) { - case 'measure': - performance.clearMeasures(TIMER_MEASURE_NAME) - performance.clearMarks(TIMER_START_NAME) - performance.clearMarks(TIMER_END_NAME) - break - - case 'dom': - performance.clearMarks(TIMER_DOM_UPDATE_NAME) - break - - case 'keypress': - performance.clearMeasures(TIMER_KEYPRESS_MEASURE_NAME) - break - } -} - -// clear performance measures and marks when switching between Source and Rich Text -window.addEventListener('editor:visual-switch', () => { - clearCM6Perf('measure') - clearCM6Perf('dom') - clearCM6Perf('keypress') -}) - -export function reportCM6Perf() { - // Get entries triggered by keystrokes - const cm6Entries = performance.getEntriesByName( - TIMER_MEASURE_NAME, - 'measure' - ) as PerformanceMeasure[] - - clearCM6Perf('measure') - - const inputEvents = cm6Entries.filter(({ detail }) => - isInputOrDelete(detail.userEventType) - ) - - const inputDurations = inputEvents - .map(({ duration }) => duration) - .sort((a, b) => a - b) - - const max = round(calculateMax(inputDurations), 2) - const mean = round(calculateMean(inputDurations), 2) - const median = round(calculateMedian(inputDurations), 2) - const ninetyFifthPercentile = round( - calculate95thPercentile(inputDurations), - 2 - ) - const maxUserEventsBetweenDomUpdates = calculateMax( - inputEvents.map(e => e.detail.userEventsSinceDomUpdateCount) - ) - const grammarly = grammarlyExtensionPresent() - const sessionLength = Math.floor((Date.now() - sessionStart) / 1000) // In seconds - - const memory = performanceMemorySupport ? measureMemoryUsage() : null - - // Get entries for keypress counts between DOM updates - const domUpdateEntries = performance.getEntriesByName( - TIMER_DOM_UPDATE_NAME, - 'mark' - ) as PerformanceMark[] - - clearCM6Perf('dom') - - let lags = 0 - let nonLags = 0 - let longestLag = 0 - let totalKeypressCount = 0 - - for (const entry of domUpdateEntries) { - const keypressCount = entry.detail.keypressesSinceDomUpdateCount - if (keypressCount === 1) { - ++nonLags - } else if (keypressCount > 1) { - ++lags - } - if (keypressCount > longestLag) { - longestLag = keypressCount - } - totalKeypressCount += keypressCount - } - - const meanLagsPerMeasure = round(lags / (lags + nonLags), 4) - const meanKeypressesPerMeasure = round( - totalKeypressCount / (lags + nonLags), - 4 - ) - - // Get entries triggered by keystrokes - const keypressPaintEntries = performance.getEntriesByName( - TIMER_KEYPRESS_MEASURE_NAME, - 'measure' - ) as PerformanceMeasure[] - - const keypressPaintDurations = keypressPaintEntries.map( - ({ duration }) => duration - ) - - const meanKeypressPaint = round(calculateMean(keypressPaintDurations), 2) - - clearCM6Perf('keypress') - - let longTasks = null - - // Get long task entries (Chromium-based browsers only at time of writing) - if (performanceLongtaskSupported) { - longTasks = longTaskSinceLastReportCount - longTaskSinceLastReportCount = 0 - } - - const release = getMeta('ol-ExposedSettings')?.sentryRelease || null - - return { - max, - mean, - median, - ninetyFifthPercentile, - maxUserEventsBetweenDomUpdates, - docLength: latestDocLength, - numberOfEntries: inputDurations.length, - grammarly, - sessionLength, - memory, - lags, - nonLags, - longestLag, - meanLagsPerMeasure, - meanKeypressesPerMeasure, - meanKeypressPaint, - longTasks, - release, - } -} - -window._reportCM6Perf = () => { - debugConsole.warn(reportCM6Perf()) -} diff --git a/services/web/types/window.ts b/services/web/types/window.ts index 2a17efeb13..2aac5c30ed 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -17,7 +17,6 @@ declare global { removeListener: (event: string, listener: any) => void } } - _reportCM6Perf: () => void MathJax: Record crypto: { randomUUID: () => string