From 980b62aa39eb7be703e7eba4252feddbaa48f272 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 11 Oct 2022 09:35:10 +0100 Subject: [PATCH] Merge pull request #9901 from overleaf/as-td-cm6-perf-editing-sessions Connect CM6 perf measurement to editing sessions GitOrigin-RevId: 04d846fb012477994c069da0630306ea7cf57723 --- .../Features/Analytics/AnalyticsController.js | 16 +--- services/web/frontend/js/ide.js | 29 ++++++- .../js/infrastructure/cm6-performance.ts | 84 +++++++++++++++++++ services/web/frontend/js/main/event.js | 14 ++-- 4 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 services/web/frontend/js/infrastructure/cm6-performance.ts diff --git a/services/web/app/src/Features/Analytics/AnalyticsController.js b/services/web/app/src/Features/Analytics/AnalyticsController.js index 40bb834109..079441db94 100644 --- a/services/web/app/src/Features/Analytics/AnalyticsController.js +++ b/services/web/app/src/Features/Analytics/AnalyticsController.js @@ -10,7 +10,7 @@ async function updateEditingSession(req, res, next) { } const userId = SessionManager.getLoggedInUserId(req.session) const { projectId } = req.params - const segmentation = _getSegmentation(req) + const segmentation = req.body.segmentation || {} let countryCode = null if (userId) { @@ -45,20 +45,6 @@ function recordEvent(req, res, next) { res.sendStatus(202) } -function _getSegmentation(req) { - const segmentation = req.body ? req.body.segmentation : null - const cleanedSegmentation = {} - if ( - segmentation && - segmentation.editorType && - typeof segmentation.editorType === 'string' && - segmentation.editorType.length < 100 - ) { - cleanedSegmentation.editorType = segmentation.editorType - } - return cleanedSegmentation -} - module.exports = { updateEditingSession, recordEvent, diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js index 54982c5d1f..2bc448c4a8 100644 --- a/services/web/frontend/js/ide.js +++ b/services/web/frontend/js/ide.js @@ -66,6 +66,7 @@ import './features/share-project-modal/controllers/react-share-project-modal-con import './features/source-editor/controllers/editor-switch-controller' import getMeta from './utils/meta' import { cleanupServiceWorker } from './utils/service-worker-cleanup' +import { reportCM6Perf } from './infrastructure/cm6-performance' App.controller( 'IdeController', @@ -256,10 +257,30 @@ If the project has been renamed please look in your project list for a new proje }) ide.editingSessionHeartbeat = () => { - const segmentation = { - editorType: ide.editorManager.getEditorType(), - } - eventTracking.editingSessionHeartbeat(segmentation) + eventTracking.editingSessionHeartbeat(() => { + const editorType = ide.editorManager.getEditorType() + + const segmentation = { + editorType, + } + + if (editorType === 'cm6') { + const cm6PerfData = reportCM6Perf() + + // Ignore if no typing has happened + if (cm6PerfData.numberOfEntries > 0) { + segmentation.cm6PerfMax = cm6PerfData.max + segmentation.cm6PerfMean = cm6PerfData.mean + segmentation.cm6PerfMedian = cm6PerfData.median + segmentation.cm6PerfNinetyFifthPercentile = + cm6PerfData.ninetyFifthPercentile + segmentation.cm6PerfDocLength = cm6PerfData.docLength + segmentation.cm6PerfNumberOfEntries = cm6PerfData.numberOfEntries + } + } + + return segmentation + }) } $scope.$on('cursor:editor:update', () => { diff --git a/services/web/frontend/js/infrastructure/cm6-performance.ts b/services/web/frontend/js/infrastructure/cm6-performance.ts new file mode 100644 index 0000000000..631b952c90 --- /dev/null +++ b/services/web/frontend/js/infrastructure/cm6-performance.ts @@ -0,0 +1,84 @@ +import { Transaction } from '@codemirror/state' + +const TIMER_START_NAME = 'CM6-BeforeUpdate' +const TIMER_END_NAME = 'CM6-AfterUpdate' +const TIMER_MEASURE_NAME = 'CM6-Update' + +let latestDocLength = 0 + +export function timedDispatch(dispatchFn: (tr: Transaction) => void) { + return (tr: Transaction) => { + performance.mark(TIMER_START_NAME) + + dispatchFn(tr) + + performance.mark(TIMER_END_NAME) + performance.measure(TIMER_MEASURE_NAME, { + start: TIMER_START_NAME, + end: TIMER_END_NAME, + detail: { + isInputUserEvent: tr.isUserEvent('input'), + }, + }) + + latestDocLength = tr.state.doc.length + } +} + +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] +} + +export function reportCM6Perf() { + // Get entries triggered by keystrokes + const cm6Entries = performance.getEntriesByName( + TIMER_MEASURE_NAME, + 'measure' + ) as PerformanceMeasure[] + + const inputDurations = cm6Entries + .filter(entry => entry.detail?.isInputUserEvent) + .map(({ duration }) => duration) + .sort((a, b) => a - b) + + const max = inputDurations.reduce((a, b) => Math.max(a, b), 0) + const mean = calculateMean(inputDurations) + const median = calculateMedian(inputDurations) + const ninetyFifthPercentile = calculate95thPercentile(inputDurations) + + performance.clearMeasures(TIMER_MEASURE_NAME) + + return { + max, + mean, + median, + ninetyFifthPercentile, + docLength: latestDocLength, + numberOfEntries: inputDurations.length, + } +} + +window._reportCM6Perf = () => { + console.log(reportCM6Perf()) +} diff --git a/services/web/frontend/js/main/event.js b/services/web/frontend/js/main/event.js index 748fb1183b..d06391fcf1 100644 --- a/services/web/frontend/js/main/event.js +++ b/services/web/frontend/js/main/event.js @@ -70,11 +70,13 @@ App.factory('eventTracking', function ($http, localStorage) { } }, - editingSessionHeartbeat(segmentation) { - sl_console.log('[Event] heartbeat trigger', segmentation) - if (!(nextHeartbeat <= new Date())) { - return - } + editingSessionHeartbeat(segmentationCb = () => {}) { + sl_console.log('[Event] heartbeat trigger') + + // If the next heartbeat is in the future, stop + if (nextHeartbeat > new Date()) return + + const segmentation = segmentationCb() sl_console.log('[Event] send heartbeat request', segmentation) _sendEditingSessionHeartbeat(segmentation) @@ -90,7 +92,7 @@ App.factory('eventTracking', function ($http, localStorage) { ? (heartbeatsSent - 2) * 60 : 300 - return (nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate()) + nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate() }, sendMB,