|
| 1 | +import { performance } from 'node:perf_hooks' |
| 2 | + |
| 3 | +export type BuildHook = 'buildStart' | 'watchChange' | 'transform' | 'writeBundle' |
| 4 | + |
| 5 | +interface BuildInterval { |
| 6 | + hook: BuildHook, |
| 7 | + start: number, |
| 8 | + end: number, |
| 9 | +} |
| 10 | + |
| 11 | +interface BuildIntervalToken { |
| 12 | + hook: BuildHook, |
| 13 | + start: number, |
| 14 | + generation: number, |
| 15 | +} |
| 16 | + |
| 17 | +export interface BuildTimingSummary { |
| 18 | + wallMs: number, |
| 19 | + attributedMs: number, |
| 20 | + unattributedMs: number, |
| 21 | + buildStartMs: number, |
| 22 | + watchChangeMs: number, |
| 23 | + transformSumMs: number, |
| 24 | + transformUnionMs: number, |
| 25 | + transformMaxConcurrency: number, |
| 26 | + transformOverlapMs: number, |
| 27 | + writeBundleMs: number, |
| 28 | +} |
| 29 | + |
| 30 | +function measureIntervals(intervals: readonly BuildInterval[]) { |
| 31 | + if (intervals.length === 0) { |
| 32 | + return { sum: 0, union: 0, maxConcurrency: 0 } |
| 33 | + } |
| 34 | + |
| 35 | + const sum = intervals.reduce((total, interval) => total + interval.end - interval.start, 0) |
| 36 | + const points = intervals.flatMap(interval => [ |
| 37 | + { time: interval.start, delta: 1 }, |
| 38 | + { time: interval.end, delta: -1 }, |
| 39 | + ]) |
| 40 | + |
| 41 | + points.sort((left, right) => { |
| 42 | + if (left.time === right.time) return left.delta - right.delta |
| 43 | + return left.time - right.time |
| 44 | + }) |
| 45 | + |
| 46 | + let active = 0 |
| 47 | + let maxConcurrency = 0 |
| 48 | + let previous = points[0].time |
| 49 | + let union = 0 |
| 50 | + |
| 51 | + for (const point of points) { |
| 52 | + if (active > 0) union += point.time - previous |
| 53 | + active += point.delta |
| 54 | + maxConcurrency = Math.max(maxConcurrency, active) |
| 55 | + previous = point.time |
| 56 | + } |
| 57 | + |
| 58 | + return { sum, union, maxConcurrency } |
| 59 | +} |
| 60 | + |
| 61 | +/** |
| 62 | + * 跟踪一次声明构建周期内的插件 hook 区间。 |
| 63 | + * |
| 64 | + * 逐次耗时之和只用于解释工作量;用户可感知耗时使用单调时钟墙钟, |
| 65 | + * 并发 transform 的归因使用区间并集,避免重复计算重叠时间。 |
| 66 | + */ |
| 67 | +export class BuildTimeTracker { |
| 68 | + private readonly now: () => number |
| 69 | + private generation = 0 |
| 70 | + private cycleStart = 0 |
| 71 | + private intervals: BuildInterval[] = [] |
| 72 | + |
| 73 | + constructor(now: () => number = () => performance.now()) { |
| 74 | + this.now = now |
| 75 | + this.reset() |
| 76 | + } |
| 77 | + |
| 78 | + reset(start = this.now()) { |
| 79 | + this.generation++ |
| 80 | + this.cycleStart = start |
| 81 | + this.intervals = [] |
| 82 | + } |
| 83 | + |
| 84 | + begin(hook: BuildHook): BuildIntervalToken { |
| 85 | + return { hook, start: this.now(), generation: this.generation } |
| 86 | + } |
| 87 | + |
| 88 | + end(token: BuildIntervalToken) { |
| 89 | + if (token.generation !== this.generation) return |
| 90 | + |
| 91 | + const end = this.now() |
| 92 | + this.intervals.push({ hook: token.hook, start: token.start, end }) |
| 93 | + } |
| 94 | + |
| 95 | + async track<T>(hook: BuildHook, action: () => Promise<T>): Promise<T> { |
| 96 | + const interval = this.begin(hook) |
| 97 | + |
| 98 | + try { |
| 99 | + return await action() |
| 100 | + } finally { |
| 101 | + this.end(interval) |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + getIntervals(): readonly BuildInterval[] { |
| 106 | + return [...this.intervals].sort((left, right) => left.start - right.start) |
| 107 | + } |
| 108 | + |
| 109 | + summarize(end = this.now()): BuildTimingSummary { |
| 110 | + const intervals = this.getIntervals() |
| 111 | + const all = measureIntervals(intervals) |
| 112 | + const phase = (hook: BuildHook) => |
| 113 | + measureIntervals(intervals.filter(interval => interval.hook === hook)) |
| 114 | + const transforms = phase('transform') |
| 115 | + const wallMs = Math.max(0, end - this.cycleStart) |
| 116 | + |
| 117 | + return { |
| 118 | + wallMs, |
| 119 | + attributedMs: all.union, |
| 120 | + unattributedMs: Math.max(0, wallMs - all.union), |
| 121 | + buildStartMs: phase('buildStart').union, |
| 122 | + watchChangeMs: phase('watchChange').union, |
| 123 | + transformSumMs: transforms.sum, |
| 124 | + transformUnionMs: transforms.union, |
| 125 | + transformMaxConcurrency: transforms.maxConcurrency, |
| 126 | + transformOverlapMs: transforms.sum - transforms.union, |
| 127 | + writeBundleMs: phase('writeBundle').union, |
| 128 | + } |
| 129 | + } |
| 130 | +} |
0 commit comments