@buoy-gg/benchmark 2.1.3 → 2.1.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/lib/commonjs/benchmarking/BenchmarkComparator.js +221 -1
  2. package/lib/commonjs/benchmarking/BenchmarkRecorder.js +497 -1
  3. package/lib/commonjs/benchmarking/BenchmarkStorage.js +235 -1
  4. package/lib/commonjs/benchmarking/index.js +83 -1
  5. package/lib/commonjs/benchmarking/types.js +13 -1
  6. package/lib/commonjs/components/BenchmarkCompareView.js +475 -1
  7. package/lib/commonjs/components/BenchmarkDetailView.js +346 -1
  8. package/lib/commonjs/components/BenchmarkModal.js +505 -1
  9. package/lib/commonjs/components/BenchmarkSessionCard.js +193 -1
  10. package/lib/commonjs/index.js +62 -1
  11. package/lib/commonjs/preset.js +86 -1
  12. package/lib/module/benchmarking/BenchmarkComparator.js +216 -1
  13. package/lib/module/benchmarking/BenchmarkRecorder.js +493 -1
  14. package/lib/module/benchmarking/BenchmarkStorage.js +227 -1
  15. package/lib/module/benchmarking/index.js +48 -1
  16. package/lib/module/benchmarking/types.js +13 -1
  17. package/lib/module/components/BenchmarkCompareView.js +469 -1
  18. package/lib/module/components/BenchmarkDetailView.js +340 -1
  19. package/lib/module/components/BenchmarkModal.js +499 -1
  20. package/lib/module/components/BenchmarkSessionCard.js +187 -1
  21. package/lib/module/index.js +39 -1
  22. package/lib/module/preset.js +81 -1
  23. package/lib/typescript/benchmarking/BenchmarkComparator.d.ts.map +1 -0
  24. package/lib/typescript/benchmarking/BenchmarkRecorder.d.ts.map +1 -0
  25. package/lib/typescript/benchmarking/BenchmarkStorage.d.ts.map +1 -0
  26. package/lib/typescript/benchmarking/index.d.ts.map +1 -0
  27. package/lib/typescript/benchmarking/types.d.ts.map +1 -0
  28. package/lib/typescript/components/BenchmarkCompareView.d.ts.map +1 -0
  29. package/lib/typescript/components/BenchmarkDetailView.d.ts.map +1 -0
  30. package/lib/typescript/components/BenchmarkModal.d.ts.map +1 -0
  31. package/lib/typescript/components/BenchmarkSessionCard.d.ts.map +1 -0
  32. package/lib/typescript/index.d.ts.map +1 -0
  33. package/lib/typescript/preset.d.ts.map +1 -0
  34. package/package.json +2 -2
@@ -1 +1,493 @@
1
- "use strict";import{Platform}from"react-native";function generateSessionId(e){return`${e}_${Date.now()}_${Math.random().toString(36).substring(2,8)}`}function percentile(e,t){if(0===e.length)return 0;const s=Math.ceil(t/100*e.length)-1;return e[Math.max(0,Math.min(s,e.length-1))]}function captureMemorySnapshot(){return null}function getBenchmarkContext(e,t){let s="unknown";return"ios"===Platform.OS?s="ios":"android"===Platform.OS?s="android":"web"===Platform.OS&&(s="web"),{platform:s,osVersion:Platform.Version?.toString(),isDev:__DEV__??!1,batchSize:e,showRenderCount:t}}function computeStats(e,t){const s=e.length;if(0===s)return{batchCount:0,totalNodesReceived:0,totalNodesFiltered:0,totalNodesProcessed:0,avgFilterTime:0,avgMeasureTime:0,avgTrackTime:0,avgCallbackTime:0,avgTotalTime:0,minTotalTime:0,maxTotalTime:0,p50TotalTime:0,p95TotalTime:0,p99TotalTime:0,avgOverlayRenderTime:0,avgHighlightsPerRender:0};let o=0,r=0,a=0,i=0,n=0,l=0,c=0,m=0;const h=[];for(const t of e)o+=t.nodesReceived,r+=t.nodesFiltered,a+=t.nodesInBatch,i+=t.filteringTime,n+=t.measurementTime,l+=t.trackingTime,c+=t.callbackTime,m+=t.totalTime,h.push(t.totalTime);h.sort((e,t)=>e-t);let d=0,g=0;for(const e of t)d+=e.renderTime,g+=e.highlightCount;return{batchCount:s,totalNodesReceived:o,totalNodesFiltered:r,totalNodesProcessed:a,avgFilterTime:i/s,avgMeasureTime:n/s,avgTrackTime:l/s,avgCallbackTime:c/s,avgTotalTime:m/s,minTotalTime:h[0],maxTotalTime:h[h.length-1],p50TotalTime:percentile(h,50),p95TotalTime:percentile(h,95),p99TotalTime:percentile(h,99),avgOverlayRenderTime:t.length>0?d/t.length:0,avgHighlightsPerRender:t.length>0?g/t.length:0}}export class BenchmarkRecorder{state="idle";sessionId="";sessionName="";sessionStartTime=0;verbose=!1;captureMemory=!0;batches=[];overlayRenders=[];memoryStart=null;memoryEnd=null;marks=[];measures=[];batchSize=150;showRenderCount=!0;listeners=new Set;activeMeasures=new Map;getState(){return this.state}isRecording(){return"recording"===this.state}setBatchSize(e){this.batchSize=e}setShowRenderCount(e){this.showRenderCount=e}startSession(e){"recording"!==this.state?(this.sessionId=generateSessionId(e.name),this.sessionName=e.name,this.sessionDescription=e.description,this.verbose=e.verbose??!1,this.captureMemory=e.captureMemory??!0,this.batches=[],this.overlayRenders=[],this.marks=[],this.measures=[],this.activeMeasures.clear(),this.captureMemory&&(this.memoryStart=captureMemorySnapshot()),this.sessionStartTime=performance.now(),this.marks.push({name:"session_start",startTime:0,detail:{name:this.sessionName}}),this.state="recording",this.verbose&&(console.log(`[BenchmarkRecorder] Session started: ${this.sessionName}`),console.log(` ID: ${this.sessionId}`)),this.notifyListeners("start")):console.warn("[BenchmarkRecorder] Session already recording. Stop it first.")}stopSession(){if("recording"!==this.state)return console.warn("[BenchmarkRecorder] No active session to stop."),null;const e=performance.now()-this.sessionStartTime;this.marks.push({name:"session_end",startTime:e}),this.measures.push({name:"session_total",startTime:0,duration:e}),this.captureMemory&&(this.memoryEnd=captureMemorySnapshot());let t=null;null!=this.memoryStart?.usedJSHeapSize&&null!=this.memoryEnd?.usedJSHeapSize&&(t=this.memoryEnd.usedJSHeapSize-this.memoryStart.usedJSHeapSize);const s=computeStats(this.batches,this.overlayRenders),o={version:"1.0",id:this.sessionId,name:this.sessionName,description:this.sessionDescription,createdAt:Date.now(),duration:e,context:getBenchmarkContext(this.batchSize,this.showRenderCount),batches:[...this.batches],overlayRenders:[...this.overlayRenders],marks:[...this.marks],measures:[...this.measures],stats:s,memoryStart:this.memoryStart,memoryEnd:this.memoryEnd,memoryDelta:t};return this.state="stopped",this.verbose&&this.logReport(o),this.notifyListeners("stop"),o}recordBatch(e){"recording"===this.state&&(this.batches.push(e),this.marks.push({name:`batch_${e.batchId}`,startTime:performance.now()-this.sessionStartTime,detail:{nodesReceived:e.nodesReceived,nodesProcessed:e.nodesInBatch,totalTime:e.totalTime}}),this.verbose&&console.log(`[BenchmarkRecorder] Batch ${e.batchId}: ${e.nodesInBatch} nodes in ${e.totalTime.toFixed(1)}ms`),this.notifyListeners("batch",e))}recordOverlayRender(e,t){if("recording"!==this.state)return;const s={highlightCount:e,renderTime:t,timestamp:performance.now()};this.overlayRenders.push(s),this.marks.push({name:"overlay_render",startTime:performance.now()-this.sessionStartTime,detail:{highlightCount:e,renderTime:t}}),this.verbose&&console.log(`[BenchmarkRecorder] Overlay render: ${e} highlights in ${t.toFixed(1)}ms`),this.notifyListeners("overlay",s)}mark(e,t){"recording"===this.state&&(this.marks.push({name:e,startTime:performance.now()-this.sessionStartTime,detail:t}),this.verbose&&console.log(`[BenchmarkRecorder] Mark: ${e}`))}startMeasure(e){"recording"===this.state&&(this.activeMeasures.set(e,performance.now()),this.verbose&&console.log(`[BenchmarkRecorder] Measure started: ${e}`))}endMeasure(e,t){if("recording"!==this.state)return null;const s=this.activeMeasures.get(e);if(void 0===s)return console.warn(`[BenchmarkRecorder] No active measure: ${e}`),null;const o=performance.now()-s;return this.measures.push({name:e,startTime:s-this.sessionStartTime,duration:o,detail:t}),this.activeMeasures.delete(e),this.verbose&&console.log(`[BenchmarkRecorder] Measure ended: ${e} = ${o.toFixed(1)}ms`),o}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(e,t){for(const s of this.listeners)try{s(e,t)}catch(e){console.error("[BenchmarkRecorder] Error in event listener:",e)}}logReport(e){const{stats:t,memoryDelta:s}=e;if(console.log("\n╔══════════════════════════════════════════════════════════════╗"),console.log("║ BENCHMARK REPORT ║"),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log(`║ Name: ${e.name.padEnd(55)}║`),console.log(`║ ID: ${e.id.substring(0,57).padEnd(57)}║`),console.log(`║ Duration: ${e.duration.toFixed(1).padStart(8)}ms ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ BATCH STATS ║"),console.log(`║ Count: ${t.batchCount.toString().padStart(6)} ║`),console.log(`║ Nodes received: ${t.totalNodesReceived.toString().padStart(8)} ║`),console.log(`║ Nodes processed: ${t.totalNodesProcessed.toString().padStart(7)} ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ TIMING (avg per batch) ║"),console.log(`║ Filter: ${t.avgFilterTime.toFixed(1).padStart(8)}ms ║`),console.log(`║ Measure: ${t.avgMeasureTime.toFixed(1).padStart(7)}ms ← Primary bottleneck ║`),console.log(`║ Track: ${t.avgTrackTime.toFixed(1).padStart(9)}ms ║`),console.log(`║ Callback: ${t.avgCallbackTime.toFixed(1).padStart(6)}ms ║`),console.log(`║ Total: ${t.avgTotalTime.toFixed(1).padStart(9)}ms ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ PERCENTILES ║"),console.log(`║ P50: ${t.p50TotalTime.toFixed(1).padStart(8)}ms ║`),console.log(`║ P95: ${t.p95TotalTime.toFixed(1).padStart(8)}ms ║`),console.log(`║ P99: ${t.p99TotalTime.toFixed(1).padStart(8)}ms ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ OVERLAY RENDERS ║"),console.log(`║ Avg time: ${t.avgOverlayRenderTime.toFixed(1).padStart(7)}ms ║`),console.log(`║ Avg highlights: ${t.avgHighlightsPerRender.toFixed(0).padStart(5)} ║`),null!=s){const e=(s/1024/1024).toFixed(2),t=s>=0?"+":"";console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ MEMORY ║"),console.log(`║ Delta: ${t}${e.padStart(7)}MB ║`)}console.log("╚══════════════════════════════════════════════════════════════╝\n")}}export const benchmarkRecorder=new BenchmarkRecorder;export default BenchmarkRecorder;
1
+ /**
2
+ * BenchmarkRecorder
3
+ *
4
+ * Records performance metrics during a benchmark session. Uses performance.now()
5
+ * for high-resolution timing (the only Performance API available in React Native).
6
+ * Collects batch metrics, overlay render times, memory snapshots, and custom marks/measures.
7
+ *
8
+ * Usage:
9
+ * const recorder = new BenchmarkRecorder();
10
+ * recorder.startSession({ name: 'MyBenchmark' });
11
+ *
12
+ * // Record batch metrics (called by HighlightUpdatesController)
13
+ * recorder.recordBatch(batchMetrics);
14
+ *
15
+ * // Record overlay renders (called by HighlightUpdatesOverlay)
16
+ * recorder.recordOverlayRender(count, timeMs);
17
+ *
18
+ * // Add custom marks/measures
19
+ * recorder.mark('customEvent');
20
+ * recorder.startMeasure('apiCall');
21
+ * recorder.endMeasure('apiCall');
22
+ *
23
+ * // Stop and get report
24
+ * const report = recorder.stopSession();
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+
29
+ "use strict";
30
+
31
+ import { Platform } from "react-native";
32
+
33
+ // React Native only provides performance.now()
34
+
35
+ /**
36
+ * Generate a unique session ID
37
+ */
38
+ function generateSessionId(name) {
39
+ const timestamp = Date.now();
40
+ const random = Math.random().toString(36).substring(2, 8);
41
+ return `${name}_${timestamp}_${random}`;
42
+ }
43
+
44
+ /**
45
+ * Calculate percentile from sorted array
46
+ */
47
+ function percentile(sortedArr, p) {
48
+ if (sortedArr.length === 0) return 0;
49
+ const index = Math.ceil(p / 100 * sortedArr.length) - 1;
50
+ return sortedArr[Math.max(0, Math.min(index, sortedArr.length - 1))];
51
+ }
52
+
53
+ /**
54
+ * Capture current memory snapshot (may not be available in all environments)
55
+ */
56
+ function captureMemorySnapshot() {
57
+ // Memory API is not available in React Native
58
+ // Could potentially use native modules in the future
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Get current benchmark context
64
+ */
65
+ function getBenchmarkContext(batchSize, showRenderCount) {
66
+ let platform = "unknown";
67
+ if (Platform.OS === "ios") platform = "ios";else if (Platform.OS === "android") platform = "android";else if (Platform.OS === "web") platform = "web";
68
+ return {
69
+ platform,
70
+ osVersion: Platform.Version?.toString(),
71
+ isDev: __DEV__ ?? false,
72
+ batchSize,
73
+ showRenderCount
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Compute aggregated statistics from batch and overlay metrics
79
+ */
80
+ function computeStats(batches, overlayRenders) {
81
+ const batchCount = batches.length;
82
+ if (batchCount === 0) {
83
+ return {
84
+ batchCount: 0,
85
+ totalNodesReceived: 0,
86
+ totalNodesFiltered: 0,
87
+ totalNodesProcessed: 0,
88
+ avgFilterTime: 0,
89
+ avgMeasureTime: 0,
90
+ avgTrackTime: 0,
91
+ avgCallbackTime: 0,
92
+ avgTotalTime: 0,
93
+ minTotalTime: 0,
94
+ maxTotalTime: 0,
95
+ p50TotalTime: 0,
96
+ p95TotalTime: 0,
97
+ p99TotalTime: 0,
98
+ avgOverlayRenderTime: 0,
99
+ avgHighlightsPerRender: 0
100
+ };
101
+ }
102
+
103
+ // Sum up totals
104
+ let totalNodesReceived = 0;
105
+ let totalNodesFiltered = 0;
106
+ let totalNodesProcessed = 0;
107
+ let totalFilterTime = 0;
108
+ let totalMeasureTime = 0;
109
+ let totalTrackTime = 0;
110
+ let totalCallbackTime = 0;
111
+ let totalPipelineTime = 0;
112
+ const totalTimes = [];
113
+ for (const batch of batches) {
114
+ totalNodesReceived += batch.nodesReceived;
115
+ totalNodesFiltered += batch.nodesFiltered;
116
+ totalNodesProcessed += batch.nodesInBatch;
117
+ totalFilterTime += batch.filteringTime;
118
+ totalMeasureTime += batch.measurementTime;
119
+ totalTrackTime += batch.trackingTime;
120
+ totalCallbackTime += batch.callbackTime;
121
+ totalPipelineTime += batch.totalTime;
122
+ totalTimes.push(batch.totalTime);
123
+ }
124
+
125
+ // Sort for percentiles
126
+ totalTimes.sort((a, b) => a - b);
127
+
128
+ // Overlay stats
129
+ let totalOverlayTime = 0;
130
+ let totalHighlights = 0;
131
+ for (const render of overlayRenders) {
132
+ totalOverlayTime += render.renderTime;
133
+ totalHighlights += render.highlightCount;
134
+ }
135
+ return {
136
+ batchCount,
137
+ totalNodesReceived,
138
+ totalNodesFiltered,
139
+ totalNodesProcessed,
140
+ avgFilterTime: totalFilterTime / batchCount,
141
+ avgMeasureTime: totalMeasureTime / batchCount,
142
+ avgTrackTime: totalTrackTime / batchCount,
143
+ avgCallbackTime: totalCallbackTime / batchCount,
144
+ avgTotalTime: totalPipelineTime / batchCount,
145
+ minTotalTime: totalTimes[0],
146
+ maxTotalTime: totalTimes[totalTimes.length - 1],
147
+ p50TotalTime: percentile(totalTimes, 50),
148
+ p95TotalTime: percentile(totalTimes, 95),
149
+ p99TotalTime: percentile(totalTimes, 99),
150
+ avgOverlayRenderTime: overlayRenders.length > 0 ? totalOverlayTime / overlayRenders.length : 0,
151
+ avgHighlightsPerRender: overlayRenders.length > 0 ? totalHighlights / overlayRenders.length : 0
152
+ };
153
+ }
154
+
155
+ /**
156
+ * BenchmarkRecorder - Records performance metrics during a benchmark session
157
+ */
158
+ export class BenchmarkRecorder {
159
+ state = "idle";
160
+ sessionId = "";
161
+ sessionName = "";
162
+ sessionStartTime = 0;
163
+ verbose = false;
164
+ captureMemory = true;
165
+
166
+ // Collected data
167
+ batches = [];
168
+ overlayRenders = [];
169
+ memoryStart = null;
170
+ memoryEnd = null;
171
+
172
+ // Internal marks and measures (since React Native doesn't have full Performance API)
173
+ marks = [];
174
+ measures = [];
175
+
176
+ // Context
177
+ batchSize = 150;
178
+ showRenderCount = true;
179
+
180
+ // Event listeners
181
+ listeners = new Set();
182
+
183
+ // Active measures (for startMeasure/endMeasure)
184
+ activeMeasures = new Map();
185
+
186
+ /**
187
+ * Get current session state
188
+ */
189
+ getState() {
190
+ return this.state;
191
+ }
192
+
193
+ /**
194
+ * Check if currently recording
195
+ */
196
+ isRecording() {
197
+ return this.state === "recording";
198
+ }
199
+
200
+ /**
201
+ * Set the batch size context (for report metadata)
202
+ */
203
+ setBatchSize(size) {
204
+ this.batchSize = size;
205
+ }
206
+
207
+ /**
208
+ * Set the showRenderCount context (for report metadata)
209
+ */
210
+ setShowRenderCount(enabled) {
211
+ this.showRenderCount = enabled;
212
+ }
213
+
214
+ /**
215
+ * Start a new benchmark session
216
+ */
217
+ startSession(options) {
218
+ if (this.state === "recording") {
219
+ console.warn("[BenchmarkRecorder] Session already recording. Stop it first.");
220
+ return;
221
+ }
222
+
223
+ // Reset state
224
+ this.sessionId = generateSessionId(options.name);
225
+ this.sessionName = options.name;
226
+ this.sessionDescription = options.description;
227
+ this.verbose = options.verbose ?? false;
228
+ this.captureMemory = options.captureMemory ?? true;
229
+ this.batches = [];
230
+ this.overlayRenders = [];
231
+ this.marks = [];
232
+ this.measures = [];
233
+ this.activeMeasures.clear();
234
+
235
+ // Capture start memory
236
+ if (this.captureMemory) {
237
+ this.memoryStart = captureMemorySnapshot();
238
+ }
239
+
240
+ // Record start time
241
+ this.sessionStartTime = performance.now();
242
+
243
+ // Add start mark
244
+ this.marks.push({
245
+ name: "session_start",
246
+ startTime: 0,
247
+ detail: {
248
+ name: this.sessionName
249
+ }
250
+ });
251
+ this.state = "recording";
252
+ if (this.verbose) {
253
+ console.log(`[BenchmarkRecorder] Session started: ${this.sessionName}`);
254
+ console.log(` ID: ${this.sessionId}`);
255
+ }
256
+ this.notifyListeners("start");
257
+ }
258
+
259
+ /**
260
+ * Stop the current session and generate a report
261
+ */
262
+ stopSession() {
263
+ if (this.state !== "recording") {
264
+ console.warn("[BenchmarkRecorder] No active session to stop.");
265
+ return null;
266
+ }
267
+ const endTime = performance.now();
268
+ const duration = endTime - this.sessionStartTime;
269
+
270
+ // Add end mark
271
+ this.marks.push({
272
+ name: "session_end",
273
+ startTime: duration
274
+ });
275
+
276
+ // Add total measure
277
+ this.measures.push({
278
+ name: "session_total",
279
+ startTime: 0,
280
+ duration
281
+ });
282
+
283
+ // Capture end memory
284
+ if (this.captureMemory) {
285
+ this.memoryEnd = captureMemorySnapshot();
286
+ }
287
+
288
+ // Calculate memory delta
289
+ let memoryDelta = null;
290
+ if (this.memoryStart?.usedJSHeapSize != null && this.memoryEnd?.usedJSHeapSize != null) {
291
+ memoryDelta = this.memoryEnd.usedJSHeapSize - this.memoryStart.usedJSHeapSize;
292
+ }
293
+
294
+ // Compute aggregated stats
295
+ const stats = computeStats(this.batches, this.overlayRenders);
296
+
297
+ // Build report
298
+ const report = {
299
+ version: "1.0",
300
+ id: this.sessionId,
301
+ name: this.sessionName,
302
+ description: this.sessionDescription,
303
+ createdAt: Date.now(),
304
+ duration,
305
+ context: getBenchmarkContext(this.batchSize, this.showRenderCount),
306
+ batches: [...this.batches],
307
+ overlayRenders: [...this.overlayRenders],
308
+ marks: [...this.marks],
309
+ measures: [...this.measures],
310
+ stats,
311
+ memoryStart: this.memoryStart,
312
+ memoryEnd: this.memoryEnd,
313
+ memoryDelta
314
+ };
315
+ this.state = "stopped";
316
+ if (this.verbose) {
317
+ this.logReport(report);
318
+ }
319
+ this.notifyListeners("stop");
320
+ return report;
321
+ }
322
+
323
+ /**
324
+ * Record a batch of highlight updates
325
+ */
326
+ recordBatch(metrics) {
327
+ if (this.state !== "recording") return;
328
+ this.batches.push(metrics);
329
+
330
+ // Add a mark for this batch
331
+ this.marks.push({
332
+ name: `batch_${metrics.batchId}`,
333
+ startTime: performance.now() - this.sessionStartTime,
334
+ detail: {
335
+ nodesReceived: metrics.nodesReceived,
336
+ nodesProcessed: metrics.nodesInBatch,
337
+ totalTime: metrics.totalTime
338
+ }
339
+ });
340
+ if (this.verbose) {
341
+ console.log(`[BenchmarkRecorder] Batch ${metrics.batchId}: ` + `${metrics.nodesInBatch} nodes in ${metrics.totalTime.toFixed(1)}ms`);
342
+ }
343
+ this.notifyListeners("batch", metrics);
344
+ }
345
+
346
+ /**
347
+ * Record an overlay render
348
+ */
349
+ recordOverlayRender(highlightCount, renderTime) {
350
+ if (this.state !== "recording") return;
351
+ const metrics = {
352
+ highlightCount,
353
+ renderTime,
354
+ timestamp: performance.now()
355
+ };
356
+ this.overlayRenders.push(metrics);
357
+ this.marks.push({
358
+ name: "overlay_render",
359
+ startTime: performance.now() - this.sessionStartTime,
360
+ detail: {
361
+ highlightCount,
362
+ renderTime
363
+ }
364
+ });
365
+ if (this.verbose) {
366
+ console.log(`[BenchmarkRecorder] Overlay render: ${highlightCount} highlights in ${renderTime.toFixed(1)}ms`);
367
+ }
368
+ this.notifyListeners("overlay", metrics);
369
+ }
370
+
371
+ /**
372
+ * Add a custom mark at the current time
373
+ */
374
+ mark(name, detail) {
375
+ if (this.state !== "recording") return;
376
+ this.marks.push({
377
+ name,
378
+ startTime: performance.now() - this.sessionStartTime,
379
+ detail
380
+ });
381
+ if (this.verbose) {
382
+ console.log(`[BenchmarkRecorder] Mark: ${name}`);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Start a custom measure
388
+ */
389
+ startMeasure(name) {
390
+ if (this.state !== "recording") return;
391
+ this.activeMeasures.set(name, performance.now());
392
+ if (this.verbose) {
393
+ console.log(`[BenchmarkRecorder] Measure started: ${name}`);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * End a custom measure
399
+ */
400
+ endMeasure(name, detail) {
401
+ if (this.state !== "recording") return null;
402
+ const startTime = this.activeMeasures.get(name);
403
+ if (startTime === undefined) {
404
+ console.warn(`[BenchmarkRecorder] No active measure: ${name}`);
405
+ return null;
406
+ }
407
+ const endTime = performance.now();
408
+ const duration = endTime - startTime;
409
+ this.measures.push({
410
+ name,
411
+ startTime: startTime - this.sessionStartTime,
412
+ duration,
413
+ detail
414
+ });
415
+ this.activeMeasures.delete(name);
416
+ if (this.verbose) {
417
+ console.log(`[BenchmarkRecorder] Measure ended: ${name} = ${duration.toFixed(1)}ms`);
418
+ }
419
+ return duration;
420
+ }
421
+
422
+ /**
423
+ * Subscribe to benchmark events
424
+ */
425
+ subscribe(listener) {
426
+ this.listeners.add(listener);
427
+ return () => {
428
+ this.listeners.delete(listener);
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Notify event listeners
434
+ */
435
+ notifyListeners(event, data) {
436
+ for (const listener of this.listeners) {
437
+ try {
438
+ listener(event, data);
439
+ } catch (error) {
440
+ console.error("[BenchmarkRecorder] Error in event listener:", error);
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Log a summary of the report
447
+ */
448
+ logReport(report) {
449
+ const {
450
+ stats,
451
+ memoryDelta
452
+ } = report;
453
+ console.log("\n╔══════════════════════════════════════════════════════════════╗");
454
+ console.log("║ BENCHMARK REPORT ║");
455
+ console.log("╠══════════════════════════════════════════════════════════════╣");
456
+ console.log(`║ Name: ${report.name.padEnd(55)}║`);
457
+ console.log(`║ ID: ${report.id.substring(0, 57).padEnd(57)}║`);
458
+ console.log(`║ Duration: ${report.duration.toFixed(1).padStart(8)}ms ║`);
459
+ console.log("╠══════════════════════════════════════════════════════════════╣");
460
+ console.log("║ BATCH STATS ║");
461
+ console.log(`║ Count: ${stats.batchCount.toString().padStart(6)} ║`);
462
+ console.log(`║ Nodes received: ${stats.totalNodesReceived.toString().padStart(8)} ║`);
463
+ console.log(`║ Nodes processed: ${stats.totalNodesProcessed.toString().padStart(7)} ║`);
464
+ console.log("╠══════════════════════════════════════════════════════════════╣");
465
+ console.log("║ TIMING (avg per batch) ║");
466
+ console.log(`║ Filter: ${stats.avgFilterTime.toFixed(1).padStart(8)}ms ║`);
467
+ console.log(`║ Measure: ${stats.avgMeasureTime.toFixed(1).padStart(7)}ms ← Primary bottleneck ║`);
468
+ console.log(`║ Track: ${stats.avgTrackTime.toFixed(1).padStart(9)}ms ║`);
469
+ console.log(`║ Callback: ${stats.avgCallbackTime.toFixed(1).padStart(6)}ms ║`);
470
+ console.log(`║ Total: ${stats.avgTotalTime.toFixed(1).padStart(9)}ms ║`);
471
+ console.log("╠══════════════════════════════════════════════════════════════╣");
472
+ console.log("║ PERCENTILES ║");
473
+ console.log(`║ P50: ${stats.p50TotalTime.toFixed(1).padStart(8)}ms ║`);
474
+ console.log(`║ P95: ${stats.p95TotalTime.toFixed(1).padStart(8)}ms ║`);
475
+ console.log(`║ P99: ${stats.p99TotalTime.toFixed(1).padStart(8)}ms ║`);
476
+ console.log("╠══════════════════════════════════════════════════════════════╣");
477
+ console.log("║ OVERLAY RENDERS ║");
478
+ console.log(`║ Avg time: ${stats.avgOverlayRenderTime.toFixed(1).padStart(7)}ms ║`);
479
+ console.log(`║ Avg highlights: ${stats.avgHighlightsPerRender.toFixed(0).padStart(5)} ║`);
480
+ if (memoryDelta != null) {
481
+ const deltaMB = (memoryDelta / 1024 / 1024).toFixed(2);
482
+ const sign = memoryDelta >= 0 ? "+" : "";
483
+ console.log("╠══════════════════════════════════════════════════════════════╣");
484
+ console.log("║ MEMORY ║");
485
+ console.log(`║ Delta: ${sign}${deltaMB.padStart(7)}MB ║`);
486
+ }
487
+ console.log("╚══════════════════════════════════════════════════════════════╝\n");
488
+ }
489
+ }
490
+
491
+ // Export singleton instance for convenience
492
+ export const benchmarkRecorder = new BenchmarkRecorder();
493
+ export default BenchmarkRecorder;