mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 21:59:00 +02:00
Remove CM6 performance measurement (#22663)
GitOrigin-RevId: 2178f2b66c200005517ed8ff52afadd8b5fda14b
This commit is contained in:
@@ -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<string, unknown> = {
|
||||
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(
|
||||
|
||||
@@ -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<EditorView | null>(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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -17,7 +17,6 @@ declare global {
|
||||
removeListener: (event: string, listener: any) => void
|
||||
}
|
||||
}
|
||||
_reportCM6Perf: () => void
|
||||
MathJax: Record<string, any>
|
||||
crypto: {
|
||||
randomUUID: () => string
|
||||
|
||||
Reference in New Issue
Block a user