-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtiming.js
More file actions
133 lines (123 loc) · 4.63 KB
/
Copy pathtiming.js
File metadata and controls
133 lines (123 loc) · 4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
* Timing math for a recorded view transition.
*
* The instrumentation records four raw marks (all `performance.now()` values,
* so they are monotonic and comparable):
*
* startedAt document.startViewTransition() was called
* callbackStart the DOM-update callback began running
* callbackEnd the DOM-update callback's returned promise settled
* readyAt the `ready` promise settled (pseudo-tree built, animations armed)
* finishedAt the `finished` promise settled (animations done, tree torn down)
*
* Any of them may be missing: a skipped transition never reaches `ready`, and a
* transition still in flight has no `finishedAt`. Everything here is
* null-tolerant on purpose so the panel can render partial records live.
*/
/**
* @typedef {object} TransitionMarks
* @property {number} startedAt
* @property {number|null} [callbackStart]
* @property {number|null} [callbackEnd]
* @property {number|null} [readyAt]
* @property {number|null} [finishedAt]
*/
/**
* @typedef {object} Phase
* @property {string} id
* @property {string} label
* @property {number} start offset in ms from transition start
* @property {number} duration ms
*/
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
/**
* Difference between two marks, or null when either is unavailable.
* Negative results are clamped to 0: clock marks are monotonic, but a record
* reassembled out of order shouldn't render a negative bar.
*
* @param {number|null|undefined} from
* @param {number|null|undefined} to
* @returns {number|null}
*/
export function span(from, to) {
if (!isNum(from) || !isNum(to)) return null;
return Math.max(0, to - from);
}
/**
* Headline durations for a record.
*
* @param {TransitionMarks} marks
* @returns {{callbackMs: number|null, readyMs: number|null, finishedMs: number|null, animationMs: number|null, totalMs: number|null}}
*/
export function summarize(marks) {
const { startedAt, callbackStart, callbackEnd, readyAt, finishedAt } = marks ?? {};
return {
// How long the developer's DOM mutation took. This blocks the transition:
// the browser cannot snapshot the "new" state until it resolves.
callbackMs: span(callbackStart, callbackEnd),
// Call -> pseudo-element tree exists and animations are about to run.
readyMs: span(startedAt, readyAt),
// Call -> everything torn down.
finishedMs: span(startedAt, finishedAt),
// The visible part: ready -> finished.
animationMs: span(readyAt, finishedAt),
totalMs: span(startedAt, finishedAt ?? readyAt ?? callbackEnd),
};
}
/**
* Lay the marks out as contiguous phases for the CSS bar chart.
* Phases with unknown bounds are dropped rather than rendered as zero-width.
*
* @param {TransitionMarks} marks
* @returns {Phase[]}
*/
export function phases(marks) {
const { startedAt, callbackStart, callbackEnd, readyAt, finishedAt } = marks ?? {};
if (!isNum(startedAt)) return [];
/** @type {Array<[string, string, number|null|undefined, number|null|undefined]>} */
const raw = [
['dispatch', 'Call → callback', startedAt, callbackStart],
['callback', 'DOM update callback', callbackStart, callbackEnd],
['prepare', 'Callback → ready', callbackEnd, readyAt],
['animate', 'Animating', readyAt, finishedAt],
];
const out = [];
for (const [id, label, from, to] of raw) {
const duration = span(from, to);
if (duration === null) continue;
out.push({ id, label, start: /** @type {number} */ (from) - startedAt, duration });
}
return out;
}
/**
* Scale phases to percentage widths for rendering. The scale is shared across
* all phases of one record so the bars stay proportional to each other.
*
* @param {Phase[]} list
* @returns {Array<Phase & {startPct: number, widthPct: number}>}
*/
export function toBars(list) {
const items = Array.isArray(list) ? list : [];
const total = items.reduce((max, p) => Math.max(max, p.start + p.duration), 0);
if (total <= 0) return items.map((p) => ({ ...p, startPct: 0, widthPct: 0 }));
return items.map((p) => ({
...p,
startPct: (p.start / total) * 100,
// Floor the width so a sub-millisecond phase is still visible.
widthPct: Math.max(0.5, (p.duration / total) * 100),
}));
}
/**
* Format a duration for the UI. Keeps sub-millisecond precision where it
* matters and avoids a wall of decimals where it doesn't.
*
* @param {number|null|undefined} ms
* @returns {string}
*/
export function formatMs(ms) {
if (!isNum(ms)) return '—';
if (ms < 1) return `${ms.toFixed(2)} ms`;
if (ms < 100) return `${ms.toFixed(1)} ms`;
if (ms < 1000) return `${Math.round(ms)} ms`;
return `${(ms / 1000).toFixed(2)} s`;
}