Browse Source

Merge pull request #9938 from overleaf/revert-9935-revert-9901-as-td-cm6-perf-editing-sessions

Bring back reporting of CM6 perf measurement in editing sessions

GitOrigin-RevId: 236c6e23f09a2ddaebf1c231ed77404c4b64179d
Alasdair Smith 3 years ago
parent
commit
47b3b72076

+ 1 - 15
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,

+ 25 - 4
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', () => {

+ 80 - 0
services/web/frontend/js/infrastructure/cm6-performance.ts

@@ -0,0 +1,80 @@
+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)
+
+    if (tr.isUserEvent('input')) {
+      performance.measure(TIMER_MEASURE_NAME, TIMER_START_NAME, TIMER_END_NAME)
+    }
+
+    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
+    .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())
+}

+ 8 - 6
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,