import { round } from 'lodash' import grammarlyExtensionPresent from '../../shared/utils/grammarly' import getMeta from '../../utils/meta' const TIMER_DOM_UPDATE_NAME = 'Ace-DomUpdate' const TIMER_MEASURE_NAME = 'Ace-Keypress-Measure' 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 keypressesSinceDomUpdateCount = 0 const unpaintedKeypressStartTimes: number[] = [] let animationFrameRequest: number | null = null function timeInputToRender() { if (!performanceOptionsSupport) return ++keypressesSinceDomUpdateCount unpaintedKeypressStartTimes.push(performance.now()) if (!animationFrameRequest) { animationFrameRequest = window.requestAnimationFrame(() => { animationFrameRequest = null performance.mark(TIMER_DOM_UPDATE_NAME, { detail: { keypressesSinceDomUpdateCount }, }) keypressesSinceDomUpdateCount = 0 const keypressEnd = performance.now() for (const keypressStart of unpaintedKeypressStartTimes) { performance.measure(TIMER_MEASURE_NAME, { start: keypressStart, end: keypressEnd, }) } unpaintedKeypressStartTimes.length = 0 }) } } export function initAcePerfListener(textareaEl: HTMLTextAreaElement) { textareaEl?.addEventListener('beforeinput', timeInputToRender) } export function tearDownAcePerfListener(textareaEl: HTMLTextAreaElement) { textareaEl?.removeEventListener('beforeinput', timeInputToRender) } function calculateMean(durations: number[]) { if (durations.length === 0) return 0 const sum = durations.reduce((acc, entry) => acc + entry, 0) return sum / durations.length } export function reportAcePerf() { const durations = performance .getEntriesByName(TIMER_MEASURE_NAME, 'measure') .map(({ duration }) => duration) performance.clearMeasures(TIMER_MEASURE_NAME) const meanKeypressPaint = round(calculateMean(durations), 2) 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[] performance.clearMarks(TIMER_DOM_UPDATE_NAME) 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 ) const release = getMeta('ol-ExposedSettings')?.sentryRelease || null return { numberOfEntries: durations.length, meanKeypressPaint, grammarly, sessionLength, memory, lags, nonLags, longestLag, meanLagsPerMeasure, meanKeypressesPerMeasure, release, } } window._reportAcePerf = () => { console.log(reportAcePerf()) }