@lytjs/common-performance 6.5.0 → 6.7.0

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.
package/src/index.ts CHANGED
@@ -1,583 +1,588 @@
1
- /**
2
- * @lytjs/common-performance
3
- * Performance monitoring API for component render timing
4
- * FIX: P2-11 RUNTIME-NEW-02 - 性能监控 API
5
- */
6
-
7
- declare const __DEV__: boolean;
8
-
9
- import { warn } from '@lytjs/common-error';
10
-
11
- // ==================== Types ====================
12
-
13
- /**
14
- * Performance entry for a single render operation
15
- */
16
- export interface RenderPerformanceEntry {
17
- /** Component name or identifier */
18
- componentName: string;
19
-
20
- /** Type of operation */
21
- operation: 'mount' | 'patch' | 'unmount';
22
-
23
- /** Start time (high resolution timestamp) */
24
- startTime: number;
25
-
26
- /** End time (high resolution timestamp) */
27
- endTime: number;
28
-
29
- /** Duration in milliseconds */
30
- duration: number;
31
-
32
- /** Additional metadata */
33
- metadata?: Record<string, unknown>;
34
- }
35
-
36
- /**
37
- * Component performance statistics
38
- */
39
- export interface ComponentPerformanceStats {
40
- /** Component name */
41
- componentName: string;
42
-
43
- /** Number of mount operations */
44
- mountCount: number;
45
-
46
- /** Number of patch (update) operations */
47
- patchCount: number;
48
-
49
- /** Number of unmount operations */
50
- unmountCount: number;
51
-
52
- /** Total time spent in mount operations (ms) */
53
- totalMountTime: number;
54
-
55
- /** Total time spent in patch operations (ms) */
56
- totalPatchTime: number;
57
-
58
- /** Total time spent in unmount operations (ms) */
59
- totalUnmountTime: number;
60
-
61
- /** Average mount time (ms) */
62
- averageMountTime: number;
63
-
64
- /** Average patch time (ms) */
65
- averagePatchTime: number;
66
-
67
- /** Average unmount time (ms) */
68
- averageUnmountTime: number;
69
-
70
- /** Maximum mount time (ms) */
71
- maxMountTime: number;
72
-
73
- /** Maximum patch time (ms) */
74
- maxPatchTime: number;
75
-
76
- /** Maximum unmount time (ms) */
77
- maxUnmountTime: number;
78
-
79
- /** Last render timestamp */
80
- lastRenderTime: number;
81
- }
82
-
83
- /**
84
- * Performance monitor options
85
- */
86
- export interface PerformanceMonitorOptions {
87
- /** Maximum number of entries to keep in history */
88
- maxHistorySize?: number;
89
-
90
- /** Whether to enable monitoring by default */
91
- enabled?: boolean;
92
-
93
- /** Callback when a new entry is recorded */
94
- onEntry?: (entry: RenderPerformanceEntry) => void;
95
-
96
- /** Callback when stats are updated */
97
- onStatsUpdate?: (stats: ComponentPerformanceStats) => void;
98
- }
99
-
100
- // ==================== Performance Monitor Class ====================
101
-
102
- /**
103
- * PerformanceMonitor - Tracks component render performance
104
- *
105
- * @example
106
- * ```ts
107
- * const monitor = new PerformanceMonitor({ maxHistorySize: 100 });
108
- *
109
- * // Start timing a render
110
- * const endTiming = monitor.startTiming('MyComponent', 'patch');
111
- *
112
- * // ... perform render ...
113
- *
114
- * // End timing
115
- * endTiming();
116
- *
117
- * // Get stats
118
- * const stats = monitor.getStats('MyComponent');
119
- * console.log(`Average render time: ${stats?.averagePatchTime}ms`);
120
- * ```
121
- */
122
- export class PerformanceMonitor {
123
- // FIX: P2-v11-17 使用环形缓冲区替代数组 + shift(),
124
- // 避免 history.shift() 的 O(n) 时间复杂度
125
- private historyBuffer: (RenderPerformanceEntry | null)[] = [];
126
- private historyHead = 0;
127
- private historyCount = 0;
128
- private stats: Map<string, ComponentPerformanceStats> = new Map();
129
- private options: Required<PerformanceMonitorOptions>;
130
- private _enabled: boolean;
131
-
132
- constructor(options: PerformanceMonitorOptions = {}) {
133
- this.options = {
134
- maxHistorySize: options.maxHistorySize ?? 1000,
135
- enabled: options.enabled ?? true,
136
- onEntry: options.onEntry ?? (() => {}),
137
- onStatsUpdate: options.onStatsUpdate ?? (() => {}),
138
- };
139
- this._enabled = this.options.enabled;
140
- }
141
-
142
- /**
143
- * Check if monitoring is enabled
144
- */
145
- get enabled(): boolean {
146
- return this._enabled;
147
- }
148
-
149
- /**
150
- * Enable or disable monitoring
151
- */
152
- set enabled(value: boolean) {
153
- this._enabled = value;
154
- }
155
-
156
- /**
157
- * Start timing a render operation.
158
- * Returns a function that should be called when the operation completes.
159
- *
160
- * @param componentName - Name of the component being rendered
161
- * @param operation - Type of operation (mount, patch, unmount)
162
- * @param metadata - Optional metadata to attach to the entry
163
- * @returns A function to end the timing
164
- *
165
- * @example
166
- * ```ts
167
- * const endTiming = monitor.startTiming('MyComponent', 'patch');
168
- * // ... render logic ...
169
- * endTiming({ propsChanged: true });
170
- * ```
171
- */
172
- startTiming(
173
- componentName: string,
174
- operation: 'mount' | 'patch' | 'unmount',
175
- metadata?: Record<string, unknown>,
176
- ): (endMetadata?: Record<string, unknown>) => RenderPerformanceEntry | null {
177
- if (!this._enabled) {
178
- return () => null;
179
- }
180
-
181
- // FIX: P2-v11-16 添加 performance.now() 回退,
182
- // 在非浏览器环境(如 SSR、Node.js)中使用 Date.now() 替代
183
- const getTimestamp = (): number => {
184
- if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
185
- return performance.now();
186
- }
187
- return Date.now();
188
- };
189
-
190
- const startTime = getTimestamp();
191
-
192
- return (endMetadata?: Record<string, unknown>) => {
193
- const endTime = getTimestamp();
194
- const duration = endTime - startTime;
195
-
196
- const entry: RenderPerformanceEntry = {
197
- componentName,
198
- operation,
199
- startTime,
200
- endTime,
201
- duration,
202
- metadata: { ...metadata, ...endMetadata },
203
- };
204
-
205
- this.recordEntry(entry);
206
- return entry;
207
- };
208
- }
209
-
210
- /**
211
- * Record a performance entry directly
212
- */
213
- recordEntry(entry: RenderPerformanceEntry): void {
214
- if (!this._enabled) return;
215
-
216
- // FIX: P2-v11-17 使用环形缓冲区写入,O(1) 操作
217
- if (this.historyBuffer.length < this.options.maxHistorySize) {
218
- this.historyBuffer.push(entry);
219
- this.historyCount++;
220
- } else {
221
- this.historyBuffer[this.historyHead] = entry;
222
- this.historyHead = (this.historyHead + 1) % this.options.maxHistorySize;
223
- // historyCount 保持为 maxHistorySize
224
- }
225
-
226
- // Update stats
227
- this.updateStats(entry);
228
-
229
- // Notify callback
230
- this.options.onEntry(entry);
231
- }
232
-
233
- /**
234
- * Update statistics for a component
235
- */
236
- private updateStats(entry: RenderPerformanceEntry): void {
237
- const { componentName, operation, duration } = entry;
238
-
239
- let stats = this.stats.get(componentName);
240
- if (!stats) {
241
- stats = {
242
- componentName,
243
- mountCount: 0,
244
- patchCount: 0,
245
- unmountCount: 0,
246
- totalMountTime: 0,
247
- totalPatchTime: 0,
248
- totalUnmountTime: 0,
249
- averageMountTime: 0,
250
- averagePatchTime: 0,
251
- averageUnmountTime: 0,
252
- maxMountTime: 0,
253
- maxPatchTime: 0,
254
- maxUnmountTime: 0,
255
- lastRenderTime: Date.now(),
256
- };
257
- this.stats.set(componentName, stats);
258
- }
259
-
260
- // Update counters and times
261
- switch (operation) {
262
- case 'mount':
263
- stats.mountCount++;
264
- stats.totalMountTime += duration;
265
- stats.averageMountTime = stats.totalMountTime / stats.mountCount;
266
- stats.maxMountTime = Math.max(stats.maxMountTime, duration);
267
- break;
268
- case 'patch':
269
- stats.patchCount++;
270
- stats.totalPatchTime += duration;
271
- stats.averagePatchTime = stats.totalPatchTime / stats.patchCount;
272
- stats.maxPatchTime = Math.max(stats.maxPatchTime, duration);
273
- break;
274
- case 'unmount':
275
- stats.unmountCount++;
276
- stats.totalUnmountTime += duration;
277
- stats.averageUnmountTime = stats.totalUnmountTime / stats.unmountCount;
278
- stats.maxUnmountTime = Math.max(stats.maxUnmountTime, duration);
279
- break;
280
- }
281
-
282
- stats.lastRenderTime = Date.now();
283
- this.options.onStatsUpdate(stats);
284
- }
285
-
286
- /**
287
- * Get performance statistics for a specific component
288
- */
289
- getStats(componentName: string): ComponentPerformanceStats | undefined {
290
- return this.stats.get(componentName);
291
- }
292
-
293
- /**
294
- * Get all component statistics
295
- */
296
- getAllStats(): ComponentPerformanceStats[] {
297
- return Array.from(this.stats.values());
298
- }
299
-
300
- /**
301
- * Get performance history
302
- * FIX: P2-v11-17 从环形缓冲区读取历史记录
303
- */
304
- getHistory(): RenderPerformanceEntry[] {
305
- const result: RenderPerformanceEntry[] = [];
306
- for (let i = 0; i < this.historyCount; i++) {
307
- const idx = (this.historyHead + i) % this.historyBuffer.length;
308
- const entry = this.historyBuffer[idx];
309
- if (entry) result.push(entry);
310
- }
311
- return result;
312
- }
313
-
314
- /**
315
- * Get history for a specific component
316
- */
317
- getComponentHistory(componentName: string): RenderPerformanceEntry[] {
318
- return this.getHistory().filter((entry) => entry.componentName === componentName);
319
- }
320
-
321
- /**
322
- * Get history for a specific operation type
323
- */
324
- getOperationHistory(operation: 'mount' | 'patch' | 'unmount'): RenderPerformanceEntry[] {
325
- return this.getHistory().filter((entry) => entry.operation === operation);
326
- }
327
-
328
- /**
329
- * Get the slowest renders
330
- */
331
- getSlowestRenders(limit: number = 10): RenderPerformanceEntry[] {
332
- return this.getHistory()
333
- .sort((a, b) => b.duration - a.duration)
334
- .slice(0, limit);
335
- }
336
-
337
- /**
338
- * Get average render time across all components
339
- */
340
- getGlobalAverageRenderTime(): number {
341
- const history = this.getHistory();
342
- if (history.length === 0) return 0;
343
- const total = history.reduce((sum, entry) => sum + entry.duration, 0);
344
- return total / history.length;
345
- }
346
-
347
- /**
348
- * Get total render time across all components
349
- */
350
- getGlobalTotalRenderTime(): number {
351
- return this.getHistory().reduce((sum, entry) => sum + entry.duration, 0);
352
- }
353
-
354
- /**
355
- * Clear all history and stats
356
- * FIX: P2 统一 clear/clearComponent 策略:使用 fill(null) 预分配固定大小缓冲区,
357
- * 与 clearComponent 保持一致的环形缓冲区重建方式,避免 clear 后缓冲区大小丢失
358
- */
359
- clear(): void {
360
- this.historyBuffer = new Array(this.options.maxHistorySize).fill(null);
361
- this.historyHead = 0;
362
- this.historyCount = 0;
363
- this.stats.clear();
364
- }
365
-
366
- /**
367
- * Clear history and stats for a specific component
368
- */
369
- clearComponent(componentName: string): boolean {
370
- // FIX: P2-batch2-12 修复 clearComponent 破坏环形缓冲区的问题。
371
- // 原实现直接将过滤后的数组赋值给 historyBuffer 并重置 head/tail,
372
- // 但丢失了环形缓冲区的固定大小约束。正确做法是重建固定大小的缓冲区。
373
- const filtered = this.getHistory().filter((entry) => entry.componentName !== componentName);
374
- // 重建固定大小的环形缓冲区
375
- this.historyBuffer = new Array(this.options.maxHistorySize).fill(null);
376
- this.historyHead = 0;
377
- this.historyCount = filtered.length;
378
- for (let i = 0; i < filtered.length; i++) {
379
- // FIX: DTS build error - 类型匹配
380
- this.historyBuffer[i] = filtered[i] as RenderPerformanceEntry | null;
381
- }
382
- return this.stats.delete(componentName);
383
- }
384
-
385
- /**
386
- * Generate a performance report
387
- */
388
- generateReport(): PerformanceReport {
389
- const allStats = this.getAllStats();
390
- const history = this.getHistory();
391
- const totalRenders = history.length;
392
- const totalRenderTime = this.getGlobalTotalRenderTime();
393
- const averageRenderTime = this.getGlobalAverageRenderTime();
394
-
395
- // Find slowest component
396
- const slowestComponent = allStats.length > 0
397
- ? allStats.reduce((slowest, current) => {
398
- const currentAvg = current.averagePatchTime || current.averageMountTime;
399
- const slowestAvg = slowest.averagePatchTime || slowest.averageMountTime;
400
- return currentAvg > slowestAvg ? current : slowest;
401
- })
402
- : null;
403
-
404
- // Find most rendered component
405
- const mostRenderedComponent = allStats.length > 0
406
- ? allStats.reduce((most, current) =>
407
- current.patchCount > most.patchCount ? current : most
408
- )
409
- : null;
410
-
411
- return {
412
- timestamp: Date.now(),
413
- totalRenders,
414
- totalRenderTime,
415
- averageRenderTime,
416
- componentCount: allStats.length,
417
- slowestComponent: slowestComponent?.componentName ?? null,
418
- mostRenderedComponent: mostRenderedComponent?.componentName ?? null,
419
- componentStats: allStats,
420
- };
421
- }
422
- }
423
-
424
- /**
425
- * Performance report structure
426
- */
427
- export interface PerformanceReport {
428
- /** Report generation timestamp */
429
- timestamp: number;
430
-
431
- /** Total number of render operations */
432
- totalRenders: number;
433
-
434
- /** Total time spent rendering (ms) */
435
- totalRenderTime: number;
436
-
437
- /** Average render time (ms) */
438
- averageRenderTime: number;
439
-
440
- /** Number of unique components tracked */
441
- componentCount: number;
442
-
443
- /** Name of the slowest component (by average render time) */
444
- slowestComponent: string | null;
445
-
446
- /** Name of the most rendered component */
447
- mostRenderedComponent: string | null;
448
-
449
- /** Detailed stats for each component */
450
- componentStats: ComponentPerformanceStats[];
451
- }
452
-
453
- // ==================== Global Instance ====================
454
-
455
- /** Global performance monitor instance */
456
- let globalMonitor: PerformanceMonitor | null = null;
457
-
458
- /**
459
- * Get the global performance monitor instance
460
- * Creates one if it doesn't exist
461
- */
462
- export function getPerformanceMonitor(): PerformanceMonitor {
463
- if (!globalMonitor) {
464
- globalMonitor = new PerformanceMonitor();
465
- }
466
- return globalMonitor;
467
- }
468
-
469
- /**
470
- * Set the global performance monitor instance
471
- */
472
- export function setPerformanceMonitor(monitor: PerformanceMonitor): void {
473
- globalMonitor = monitor;
474
- }
475
-
476
- /**
477
- * Initialize the global performance monitor with options
478
- */
479
- export function initPerformanceMonitor(options: PerformanceMonitorOptions = {}): PerformanceMonitor {
480
- globalMonitor = new PerformanceMonitor(options);
481
- return globalMonitor;
482
- }
483
-
484
- // ==================== Convenience Functions ====================
485
-
486
- /**
487
- * Start timing a component render operation using the global monitor
488
- */
489
- export function startRenderTiming(
490
- componentName: string,
491
- operation: 'mount' | 'patch' | 'unmount',
492
- metadata?: Record<string, unknown>,
493
- ): (endMetadata?: Record<string, unknown>) => RenderPerformanceEntry | null {
494
- return getPerformanceMonitor().startTiming(componentName, operation, metadata);
495
- }
496
-
497
- /**
498
- * Record a render entry using the global monitor
499
- */
500
- export function recordRenderEntry(entry: RenderPerformanceEntry): void {
501
- getPerformanceMonitor().recordEntry(entry);
502
- }
503
-
504
- /**
505
- * Get component stats from the global monitor
506
- */
507
- export function getComponentStats(componentName: string): ComponentPerformanceStats | undefined {
508
- return getPerformanceMonitor().getStats(componentName);
509
- }
510
-
511
- /**
512
- * Generate a performance report from the global monitor
513
- */
514
- export function generatePerformanceReport(): PerformanceReport {
515
- return getPerformanceMonitor().generateReport();
516
- }
517
-
518
- /**
519
- * Check if performance monitoring is enabled
520
- */
521
- export function isPerformanceMonitoringEnabled(): boolean {
522
- return globalMonitor?.enabled ?? false;
523
- }
524
-
525
- /**
526
- * Enable or disable performance monitoring
527
- */
528
- export function setPerformanceMonitoringEnabled(enabled: boolean): void {
529
- const monitor = getPerformanceMonitor();
530
- monitor.enabled = enabled;
531
- }
532
-
533
- // ==================== Decorator / Wrapper ====================
534
-
535
- /**
536
- * Wrap a render function with performance monitoring
537
- *
538
- * @example
539
- * ```ts
540
- * const monitoredRender = withPerformanceTracking('MyComponent', (props) => {
541
- * return h('div', props.content);
542
- * });
543
- * ```
544
- */
545
- export function withPerformanceTracking<T extends (...args: unknown[]) => unknown>(
546
- componentName: string,
547
- renderFn: T,
548
- operation: 'mount' | 'patch' | 'unmount' = 'patch',
549
- ): T {
550
- return ((...args: unknown[]) => {
551
- const endTiming = startRenderTiming(componentName, operation);
552
- try {
553
- const result = renderFn(...args);
554
- endTiming();
555
- return result;
556
- } catch (error) {
557
- endTiming({ error: true });
558
- throw error;
559
- }
560
- }) as T;
561
- }
562
-
563
- // ==================== DevTools Integration ====================
564
-
565
- /**
566
- * Connect performance monitor to browser DevTools
567
- * Only works in development mode
568
- */
569
- export function connectToDevTools(): void {
570
- if (typeof window === 'undefined') return;
571
-
572
- // Expose monitor to window for DevTools access
573
- // FIX: DTS build error - 先转换为 unknown 再转换为 Record
574
- (window as unknown as Record<string, unknown>).__LYTJS_PERFORMANCE_MONITOR__ = getPerformanceMonitor();
575
-
576
- // Mark initialization for DevTools detection
577
- (window as unknown as Record<string, unknown>).__LYTJS_DEVTOOLS_HOOK__ = true;
578
-
579
- if (__DEV__) {
580
- // FIX: P1-14 删除 require 调用,直接使用顶部已导入的 warn
581
- warn('Performance monitor connected to DevTools');
582
- }
583
- }
1
+ /**
2
+ * @lytjs/common-performance
3
+ * Performance monitoring API for component render timing
4
+ * FIX: P2-11 RUNTIME-NEW-02 - 性能监控 API
5
+ */
6
+
7
+ declare const __DEV__: boolean;
8
+
9
+ import { warn } from '@lytjs/common-error';
10
+
11
+ // ==================== Types ====================
12
+
13
+ /**
14
+ * Performance entry for a single render operation
15
+ */
16
+ export interface RenderPerformanceEntry {
17
+ /** Component name or identifier */
18
+ componentName: string;
19
+
20
+ /** Type of operation */
21
+ operation: 'mount' | 'patch' | 'unmount';
22
+
23
+ /** Start time (high resolution timestamp) */
24
+ startTime: number;
25
+
26
+ /** End time (high resolution timestamp) */
27
+ endTime: number;
28
+
29
+ /** Duration in milliseconds */
30
+ duration: number;
31
+
32
+ /** Additional metadata */
33
+ metadata?: Record<string, unknown>;
34
+ }
35
+
36
+ /**
37
+ * Component performance statistics
38
+ */
39
+ export interface ComponentPerformanceStats {
40
+ /** Component name */
41
+ componentName: string;
42
+
43
+ /** Number of mount operations */
44
+ mountCount: number;
45
+
46
+ /** Number of patch (update) operations */
47
+ patchCount: number;
48
+
49
+ /** Number of unmount operations */
50
+ unmountCount: number;
51
+
52
+ /** Total time spent in mount operations (ms) */
53
+ totalMountTime: number;
54
+
55
+ /** Total time spent in patch operations (ms) */
56
+ totalPatchTime: number;
57
+
58
+ /** Total time spent in unmount operations (ms) */
59
+ totalUnmountTime: number;
60
+
61
+ /** Average mount time (ms) */
62
+ averageMountTime: number;
63
+
64
+ /** Average patch time (ms) */
65
+ averagePatchTime: number;
66
+
67
+ /** Average unmount time (ms) */
68
+ averageUnmountTime: number;
69
+
70
+ /** Maximum mount time (ms) */
71
+ maxMountTime: number;
72
+
73
+ /** Maximum patch time (ms) */
74
+ maxPatchTime: number;
75
+
76
+ /** Maximum unmount time (ms) */
77
+ maxUnmountTime: number;
78
+
79
+ /** Last render timestamp */
80
+ lastRenderTime: number;
81
+ }
82
+
83
+ /**
84
+ * Performance monitor options
85
+ */
86
+ export interface PerformanceMonitorOptions {
87
+ /** Maximum number of entries to keep in history */
88
+ maxHistorySize?: number;
89
+
90
+ /** Whether to enable monitoring by default */
91
+ enabled?: boolean;
92
+
93
+ /** Callback when a new entry is recorded */
94
+ onEntry?: (entry: RenderPerformanceEntry) => void;
95
+
96
+ /** Callback when stats are updated */
97
+ onStatsUpdate?: (stats: ComponentPerformanceStats) => void;
98
+ }
99
+
100
+ // ==================== Performance Monitor Class ====================
101
+
102
+ /**
103
+ * PerformanceMonitor - Tracks component render performance
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const monitor = new PerformanceMonitor({ maxHistorySize: 100 });
108
+ *
109
+ * // Start timing a render
110
+ * const endTiming = monitor.startTiming('MyComponent', 'patch');
111
+ *
112
+ * // ... perform render ...
113
+ *
114
+ * // End timing
115
+ * endTiming();
116
+ *
117
+ * // Get stats
118
+ * const stats = monitor.getStats('MyComponent');
119
+ * console.log(`Average render time: ${stats?.averagePatchTime}ms`);
120
+ * ```
121
+ */
122
+ export class PerformanceMonitor {
123
+ // FIX: P2-v11-17 使用环形缓冲区替代数组 + shift(),
124
+ // 避免 history.shift() 的 O(n) 时间复杂度
125
+ private historyBuffer: (RenderPerformanceEntry | null)[] = [];
126
+ private historyHead = 0;
127
+ private historyCount = 0;
128
+ private stats: Map<string, ComponentPerformanceStats> = new Map();
129
+ private options: Required<PerformanceMonitorOptions>;
130
+ private _enabled: boolean;
131
+
132
+ constructor(options: PerformanceMonitorOptions = {}) {
133
+ this.options = {
134
+ maxHistorySize: options.maxHistorySize ?? 1000,
135
+ enabled: options.enabled ?? true,
136
+ onEntry: options.onEntry ?? (() => {}),
137
+ onStatsUpdate: options.onStatsUpdate ?? (() => {}),
138
+ };
139
+ this._enabled = this.options.enabled;
140
+ }
141
+
142
+ /**
143
+ * Check if monitoring is enabled
144
+ */
145
+ get enabled(): boolean {
146
+ return this._enabled;
147
+ }
148
+
149
+ /**
150
+ * Enable or disable monitoring
151
+ */
152
+ set enabled(value: boolean) {
153
+ this._enabled = value;
154
+ }
155
+
156
+ /**
157
+ * Start timing a render operation.
158
+ * Returns a function that should be called when the operation completes.
159
+ *
160
+ * @param componentName - Name of the component being rendered
161
+ * @param operation - Type of operation (mount, patch, unmount)
162
+ * @param metadata - Optional metadata to attach to the entry
163
+ * @returns A function to end the timing
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * const endTiming = monitor.startTiming('MyComponent', 'patch');
168
+ * // ... render logic ...
169
+ * endTiming({ propsChanged: true });
170
+ * ```
171
+ */
172
+ startTiming(
173
+ componentName: string,
174
+ operation: 'mount' | 'patch' | 'unmount',
175
+ metadata?: Record<string, unknown>,
176
+ ): (endMetadata?: Record<string, unknown>) => RenderPerformanceEntry | null {
177
+ if (!this._enabled) {
178
+ return () => null;
179
+ }
180
+
181
+ // FIX: P2-v11-16 添加 performance.now() 回退,
182
+ // 在非浏览器环境(如 SSR、Node.js)中使用 Date.now() 替代
183
+ const getTimestamp = (): number => {
184
+ if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
185
+ return performance.now();
186
+ }
187
+ return Date.now();
188
+ };
189
+
190
+ const startTime = getTimestamp();
191
+
192
+ return (endMetadata?: Record<string, unknown>) => {
193
+ const endTime = getTimestamp();
194
+ const duration = endTime - startTime;
195
+
196
+ const entry: RenderPerformanceEntry = {
197
+ componentName,
198
+ operation,
199
+ startTime,
200
+ endTime,
201
+ duration,
202
+ metadata: { ...metadata, ...endMetadata },
203
+ };
204
+
205
+ this.recordEntry(entry);
206
+ return entry;
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Record a performance entry directly
212
+ */
213
+ recordEntry(entry: RenderPerformanceEntry): void {
214
+ if (!this._enabled) return;
215
+
216
+ // FIX: P2-v11-17 使用环形缓冲区写入,O(1) 操作
217
+ if (this.historyBuffer.length < this.options.maxHistorySize) {
218
+ this.historyBuffer.push(entry);
219
+ this.historyCount++;
220
+ } else {
221
+ this.historyBuffer[this.historyHead] = entry;
222
+ this.historyHead = (this.historyHead + 1) % this.options.maxHistorySize;
223
+ // historyCount 保持为 maxHistorySize
224
+ }
225
+
226
+ // Update stats
227
+ this.updateStats(entry);
228
+
229
+ // Notify callback
230
+ this.options.onEntry(entry);
231
+ }
232
+
233
+ /**
234
+ * Update statistics for a component
235
+ */
236
+ private updateStats(entry: RenderPerformanceEntry): void {
237
+ const { componentName, operation, duration } = entry;
238
+
239
+ let stats = this.stats.get(componentName);
240
+ if (!stats) {
241
+ stats = {
242
+ componentName,
243
+ mountCount: 0,
244
+ patchCount: 0,
245
+ unmountCount: 0,
246
+ totalMountTime: 0,
247
+ totalPatchTime: 0,
248
+ totalUnmountTime: 0,
249
+ averageMountTime: 0,
250
+ averagePatchTime: 0,
251
+ averageUnmountTime: 0,
252
+ maxMountTime: 0,
253
+ maxPatchTime: 0,
254
+ maxUnmountTime: 0,
255
+ lastRenderTime: Date.now(),
256
+ };
257
+ this.stats.set(componentName, stats);
258
+ }
259
+
260
+ // Update counters and times
261
+ switch (operation) {
262
+ case 'mount':
263
+ stats.mountCount++;
264
+ stats.totalMountTime += duration;
265
+ stats.averageMountTime = stats.totalMountTime / stats.mountCount;
266
+ stats.maxMountTime = Math.max(stats.maxMountTime, duration);
267
+ break;
268
+ case 'patch':
269
+ stats.patchCount++;
270
+ stats.totalPatchTime += duration;
271
+ stats.averagePatchTime = stats.totalPatchTime / stats.patchCount;
272
+ stats.maxPatchTime = Math.max(stats.maxPatchTime, duration);
273
+ break;
274
+ case 'unmount':
275
+ stats.unmountCount++;
276
+ stats.totalUnmountTime += duration;
277
+ stats.averageUnmountTime = stats.totalUnmountTime / stats.unmountCount;
278
+ stats.maxUnmountTime = Math.max(stats.maxUnmountTime, duration);
279
+ break;
280
+ }
281
+
282
+ stats.lastRenderTime = Date.now();
283
+ this.options.onStatsUpdate(stats);
284
+ }
285
+
286
+ /**
287
+ * Get performance statistics for a specific component
288
+ */
289
+ getStats(componentName: string): ComponentPerformanceStats | undefined {
290
+ return this.stats.get(componentName);
291
+ }
292
+
293
+ /**
294
+ * Get all component statistics
295
+ */
296
+ getAllStats(): ComponentPerformanceStats[] {
297
+ return Array.from(this.stats.values());
298
+ }
299
+
300
+ /**
301
+ * Get performance history
302
+ * FIX: P2-v11-17 从环形缓冲区读取历史记录
303
+ */
304
+ getHistory(): RenderPerformanceEntry[] {
305
+ const result: RenderPerformanceEntry[] = [];
306
+ for (let i = 0; i < this.historyCount; i++) {
307
+ const idx = (this.historyHead + i) % this.historyBuffer.length;
308
+ const entry = this.historyBuffer[idx];
309
+ if (entry) result.push(entry);
310
+ }
311
+ return result;
312
+ }
313
+
314
+ /**
315
+ * Get history for a specific component
316
+ */
317
+ getComponentHistory(componentName: string): RenderPerformanceEntry[] {
318
+ return this.getHistory().filter((entry) => entry.componentName === componentName);
319
+ }
320
+
321
+ /**
322
+ * Get history for a specific operation type
323
+ */
324
+ getOperationHistory(operation: 'mount' | 'patch' | 'unmount'): RenderPerformanceEntry[] {
325
+ return this.getHistory().filter((entry) => entry.operation === operation);
326
+ }
327
+
328
+ /**
329
+ * Get the slowest renders
330
+ */
331
+ getSlowestRenders(limit: number = 10): RenderPerformanceEntry[] {
332
+ return this.getHistory()
333
+ .sort((a, b) => b.duration - a.duration)
334
+ .slice(0, limit);
335
+ }
336
+
337
+ /**
338
+ * Get average render time across all components
339
+ */
340
+ getGlobalAverageRenderTime(): number {
341
+ const history = this.getHistory();
342
+ if (history.length === 0) return 0;
343
+ const total = history.reduce((sum, entry) => sum + entry.duration, 0);
344
+ return total / history.length;
345
+ }
346
+
347
+ /**
348
+ * Get total render time across all components
349
+ */
350
+ getGlobalTotalRenderTime(): number {
351
+ return this.getHistory().reduce((sum, entry) => sum + entry.duration, 0);
352
+ }
353
+
354
+ /**
355
+ * Clear all history and stats
356
+ * FIX: P2 统一 clear/clearComponent 策略:使用 fill(null) 预分配固定大小缓冲区,
357
+ * 与 clearComponent 保持一致的环形缓冲区重建方式,避免 clear 后缓冲区大小丢失
358
+ */
359
+ clear(): void {
360
+ this.historyBuffer = new Array(this.options.maxHistorySize).fill(null);
361
+ this.historyHead = 0;
362
+ this.historyCount = 0;
363
+ this.stats.clear();
364
+ }
365
+
366
+ /**
367
+ * Clear history and stats for a specific component
368
+ */
369
+ clearComponent(componentName: string): boolean {
370
+ // FIX: P2-batch2-12 修复 clearComponent 破坏环形缓冲区的问题。
371
+ // 原实现直接将过滤后的数组赋值给 historyBuffer 并重置 head/tail,
372
+ // 但丢失了环形缓冲区的固定大小约束。正确做法是重建固定大小的缓冲区。
373
+ const filtered = this.getHistory().filter((entry) => entry.componentName !== componentName);
374
+ // 重建固定大小的环形缓冲区
375
+ this.historyBuffer = new Array(this.options.maxHistorySize).fill(null);
376
+ this.historyHead = 0;
377
+ this.historyCount = filtered.length;
378
+ for (let i = 0; i < filtered.length; i++) {
379
+ // FIX: DTS build error - 类型匹配
380
+ this.historyBuffer[i] = filtered[i] as RenderPerformanceEntry | null;
381
+ }
382
+ return this.stats.delete(componentName);
383
+ }
384
+
385
+ /**
386
+ * Generate a performance report
387
+ */
388
+ generateReport(): PerformanceReport {
389
+ const allStats = this.getAllStats();
390
+ const history = this.getHistory();
391
+ const totalRenders = history.length;
392
+ const totalRenderTime = this.getGlobalTotalRenderTime();
393
+ const averageRenderTime = this.getGlobalAverageRenderTime();
394
+
395
+ // Find slowest component
396
+ const slowestComponent =
397
+ allStats.length > 0
398
+ ? allStats.reduce((slowest, current) => {
399
+ const currentAvg = current.averagePatchTime || current.averageMountTime;
400
+ const slowestAvg = slowest.averagePatchTime || slowest.averageMountTime;
401
+ return currentAvg > slowestAvg ? current : slowest;
402
+ })
403
+ : null;
404
+
405
+ // Find most rendered component
406
+ const mostRenderedComponent =
407
+ allStats.length > 0
408
+ ? allStats.reduce((most, current) =>
409
+ current.patchCount > most.patchCount ? current : most,
410
+ )
411
+ : null;
412
+
413
+ return {
414
+ timestamp: Date.now(),
415
+ totalRenders,
416
+ totalRenderTime,
417
+ averageRenderTime,
418
+ componentCount: allStats.length,
419
+ slowestComponent: slowestComponent?.componentName ?? null,
420
+ mostRenderedComponent: mostRenderedComponent?.componentName ?? null,
421
+ componentStats: allStats,
422
+ };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Performance report structure
428
+ */
429
+ export interface PerformanceReport {
430
+ /** Report generation timestamp */
431
+ timestamp: number;
432
+
433
+ /** Total number of render operations */
434
+ totalRenders: number;
435
+
436
+ /** Total time spent rendering (ms) */
437
+ totalRenderTime: number;
438
+
439
+ /** Average render time (ms) */
440
+ averageRenderTime: number;
441
+
442
+ /** Number of unique components tracked */
443
+ componentCount: number;
444
+
445
+ /** Name of the slowest component (by average render time) */
446
+ slowestComponent: string | null;
447
+
448
+ /** Name of the most rendered component */
449
+ mostRenderedComponent: string | null;
450
+
451
+ /** Detailed stats for each component */
452
+ componentStats: ComponentPerformanceStats[];
453
+ }
454
+
455
+ // ==================== Global Instance ====================
456
+
457
+ /** Global performance monitor instance */
458
+ let globalMonitor: PerformanceMonitor | null = null;
459
+
460
+ /**
461
+ * Get the global performance monitor instance
462
+ * Creates one if it doesn't exist
463
+ */
464
+ export function getPerformanceMonitor(): PerformanceMonitor {
465
+ if (!globalMonitor) {
466
+ globalMonitor = new PerformanceMonitor();
467
+ }
468
+ return globalMonitor;
469
+ }
470
+
471
+ /**
472
+ * Set the global performance monitor instance
473
+ */
474
+ export function setPerformanceMonitor(monitor: PerformanceMonitor): void {
475
+ globalMonitor = monitor;
476
+ }
477
+
478
+ /**
479
+ * Initialize the global performance monitor with options
480
+ */
481
+ export function initPerformanceMonitor(
482
+ options: PerformanceMonitorOptions = {},
483
+ ): PerformanceMonitor {
484
+ globalMonitor = new PerformanceMonitor(options);
485
+ return globalMonitor;
486
+ }
487
+
488
+ // ==================== Convenience Functions ====================
489
+
490
+ /**
491
+ * Start timing a component render operation using the global monitor
492
+ */
493
+ export function startRenderTiming(
494
+ componentName: string,
495
+ operation: 'mount' | 'patch' | 'unmount',
496
+ metadata?: Record<string, unknown>,
497
+ ): (endMetadata?: Record<string, unknown>) => RenderPerformanceEntry | null {
498
+ return getPerformanceMonitor().startTiming(componentName, operation, metadata);
499
+ }
500
+
501
+ /**
502
+ * Record a render entry using the global monitor
503
+ */
504
+ export function recordRenderEntry(entry: RenderPerformanceEntry): void {
505
+ getPerformanceMonitor().recordEntry(entry);
506
+ }
507
+
508
+ /**
509
+ * Get component stats from the global monitor
510
+ */
511
+ export function getComponentStats(componentName: string): ComponentPerformanceStats | undefined {
512
+ return getPerformanceMonitor().getStats(componentName);
513
+ }
514
+
515
+ /**
516
+ * Generate a performance report from the global monitor
517
+ */
518
+ export function generatePerformanceReport(): PerformanceReport {
519
+ return getPerformanceMonitor().generateReport();
520
+ }
521
+
522
+ /**
523
+ * Check if performance monitoring is enabled
524
+ */
525
+ export function isPerformanceMonitoringEnabled(): boolean {
526
+ return globalMonitor?.enabled ?? false;
527
+ }
528
+
529
+ /**
530
+ * Enable or disable performance monitoring
531
+ */
532
+ export function setPerformanceMonitoringEnabled(enabled: boolean): void {
533
+ const monitor = getPerformanceMonitor();
534
+ monitor.enabled = enabled;
535
+ }
536
+
537
+ // ==================== Decorator / Wrapper ====================
538
+
539
+ /**
540
+ * Wrap a render function with performance monitoring
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * const monitoredRender = withPerformanceTracking('MyComponent', (props) => {
545
+ * return h('div', props.content);
546
+ * });
547
+ * ```
548
+ */
549
+ export function withPerformanceTracking<T extends (...args: unknown[]) => unknown>(
550
+ componentName: string,
551
+ renderFn: T,
552
+ operation: 'mount' | 'patch' | 'unmount' = 'patch',
553
+ ): T {
554
+ return ((...args: unknown[]) => {
555
+ const endTiming = startRenderTiming(componentName, operation);
556
+ try {
557
+ const result = renderFn(...args);
558
+ endTiming();
559
+ return result;
560
+ } catch (error) {
561
+ endTiming({ error: true });
562
+ throw error;
563
+ }
564
+ }) as T;
565
+ }
566
+
567
+ // ==================== DevTools Integration ====================
568
+
569
+ /**
570
+ * Connect performance monitor to browser DevTools
571
+ * Only works in development mode
572
+ */
573
+ export function connectToDevTools(): void {
574
+ if (typeof window === 'undefined') return;
575
+
576
+ // Expose monitor to window for DevTools access
577
+ // FIX: DTS build error - 先转换为 unknown 再转换为 Record
578
+ (window as unknown as Record<string, unknown>).__LYTJS_PERFORMANCE_MONITOR__ =
579
+ getPerformanceMonitor();
580
+
581
+ // Mark initialization for DevTools detection
582
+ (window as unknown as Record<string, unknown>).__LYTJS_DEVTOOLS_HOOK__ = true;
583
+
584
+ if (__DEV__) {
585
+ // FIX: P1-14 删除 require 调用,直接使用顶部已导入的 warn
586
+ warn('Performance monitor connected to DevTools');
587
+ }
588
+ }