@aethervtt/shard-core 0.1.0 → 0.2.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/dist/scheduler/capture/capture.d.ts +192 -0
- package/dist/scheduler/capture/capture.js +410 -0
- package/dist/scheduler/capture/index.d.ts +3 -0
- package/dist/scheduler/capture/index.js +17 -0
- package/dist/scheduler/capture/recorder.d.ts +83 -0
- package/dist/scheduler/capture/recorder.js +224 -0
- package/dist/scheduler/capture/samples.d.ts +49 -0
- package/dist/scheduler/capture/samples.js +91 -0
- package/dist/scheduler/index.d.ts +3 -1
- package/dist/scheduler/index.js +29 -2
- package/dist/scheduler/profiler.d.ts +180 -5
- package/dist/scheduler/profiler.js +430 -17
- package/dist/scheduler/schedule.d.ts +5 -1
- package/dist/scheduler/schedule.js +33 -3
- package/dist/test-env.d.ts +95 -1
- package/dist/test-env.js +70 -2
- package/package.json +6 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { type TrackName } from '../profiler.js';
|
|
2
|
+
export interface CaptureFrames {
|
|
3
|
+
/** `Time.frame` of each frame, oldest first. */
|
|
4
|
+
numbers: Float64Array;
|
|
5
|
+
/** When each started, on the profiler's clock (ms). */
|
|
6
|
+
starts: Float64Array;
|
|
7
|
+
/** Each frame's CPU time: the `frame` span. */
|
|
8
|
+
cpu: Float64Array;
|
|
9
|
+
}
|
|
10
|
+
export interface CaptureEvents {
|
|
11
|
+
ids: Uint16Array;
|
|
12
|
+
tracks: Uint8Array;
|
|
13
|
+
depths: Uint8Array;
|
|
14
|
+
starts: Float64Array;
|
|
15
|
+
durations: Float64Array;
|
|
16
|
+
}
|
|
17
|
+
export interface CaptureInit {
|
|
18
|
+
frames: CaptureFrames;
|
|
19
|
+
events: CaptureEvents;
|
|
20
|
+
mode: 'frames' | 'until';
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
/** The flight recorder's slow frame. */
|
|
23
|
+
trigger: {
|
|
24
|
+
frame: number;
|
|
25
|
+
frameMs: number;
|
|
26
|
+
} | undefined;
|
|
27
|
+
/** The flight recorder gave up waiting for a slow frame. */
|
|
28
|
+
timedOut: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface Stats {
|
|
31
|
+
p50: number;
|
|
32
|
+
p95: number;
|
|
33
|
+
max: number;
|
|
34
|
+
}
|
|
35
|
+
/** One span over a capture (or a window of aggregates): per-frame p50/p95/max over frames it ran in. */
|
|
36
|
+
export interface SpanStats {
|
|
37
|
+
span: string;
|
|
38
|
+
track: TrackName;
|
|
39
|
+
calls: number;
|
|
40
|
+
/** Total ms over the capture. */
|
|
41
|
+
total: number;
|
|
42
|
+
p50: number;
|
|
43
|
+
p95: number;
|
|
44
|
+
max: number;
|
|
45
|
+
}
|
|
46
|
+
export interface HeapStats {
|
|
47
|
+
usedBytes: number;
|
|
48
|
+
totalBytes?: number;
|
|
49
|
+
limitBytes?: number;
|
|
50
|
+
/** Where it came from: `v8`, `measureUserAgentSpecificMemory`, `performance.memory`. */
|
|
51
|
+
source: string;
|
|
52
|
+
}
|
|
53
|
+
/** A function the sampling profiler saw most, by self time. */
|
|
54
|
+
export interface HotFunction {
|
|
55
|
+
name: string;
|
|
56
|
+
url?: string;
|
|
57
|
+
line?: number;
|
|
58
|
+
selfMs: number;
|
|
59
|
+
/** Of every sample, 0–1. */
|
|
60
|
+
share: number;
|
|
61
|
+
}
|
|
62
|
+
export interface CaptureWarning {
|
|
63
|
+
code: string;
|
|
64
|
+
message: string;
|
|
65
|
+
hint?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface CaptureSummary {
|
|
68
|
+
frames: {
|
|
69
|
+
count: number;
|
|
70
|
+
cpu: Stats;
|
|
71
|
+
gpu?: Stats;
|
|
72
|
+
interval: Stats;
|
|
73
|
+
};
|
|
74
|
+
clock: {
|
|
75
|
+
resolutionMs: number;
|
|
76
|
+
isolated: boolean;
|
|
77
|
+
gpuQuantized: boolean;
|
|
78
|
+
};
|
|
79
|
+
/** The 20 spans with the most time, `frame` and `gpu:frame` aside (those are `frames`). */
|
|
80
|
+
top: SpanStats[];
|
|
81
|
+
/** The 5 slowest frames, each with the spans most above their median in it. */
|
|
82
|
+
worst: {
|
|
83
|
+
frame: number;
|
|
84
|
+
cpuMs: number;
|
|
85
|
+
gpuMs?: number;
|
|
86
|
+
over: {
|
|
87
|
+
span: string;
|
|
88
|
+
ms: number;
|
|
89
|
+
medianMs: number;
|
|
90
|
+
}[];
|
|
91
|
+
}[];
|
|
92
|
+
memory: {
|
|
93
|
+
gpu: {
|
|
94
|
+
bytes: number;
|
|
95
|
+
byCategory: Record<string, number>;
|
|
96
|
+
};
|
|
97
|
+
heap?: HeapStats;
|
|
98
|
+
};
|
|
99
|
+
truncated: boolean;
|
|
100
|
+
capture: {
|
|
101
|
+
mode: 'frames' | 'until';
|
|
102
|
+
/** First and last frame captured. */
|
|
103
|
+
range: [number, number] | null;
|
|
104
|
+
trigger?: {
|
|
105
|
+
frame: number;
|
|
106
|
+
frameMs: number;
|
|
107
|
+
};
|
|
108
|
+
timedOut?: true;
|
|
109
|
+
};
|
|
110
|
+
/** The sampling profiler's hottest functions, when the capture sampled. */
|
|
111
|
+
hottest?: HotFunction[];
|
|
112
|
+
warnings: CaptureWarning[];
|
|
113
|
+
}
|
|
114
|
+
export interface SummaryExtras {
|
|
115
|
+
clock: {
|
|
116
|
+
resolutionMs: number;
|
|
117
|
+
isolated: boolean;
|
|
118
|
+
gpuQuantized: boolean;
|
|
119
|
+
};
|
|
120
|
+
memory?: CaptureSummary['memory'];
|
|
121
|
+
hottest?: HotFunction[];
|
|
122
|
+
warnings?: CaptureWarning[];
|
|
123
|
+
}
|
|
124
|
+
/** A sampled stretch of JavaScript for the trace's samples track. */
|
|
125
|
+
export interface TraceSample {
|
|
126
|
+
name: string;
|
|
127
|
+
start: number;
|
|
128
|
+
ms: number;
|
|
129
|
+
}
|
|
130
|
+
export interface TraceEvent {
|
|
131
|
+
name: string;
|
|
132
|
+
cat?: string;
|
|
133
|
+
ph: 'X' | 'b' | 'e' | 'M' | 'i';
|
|
134
|
+
ts?: number;
|
|
135
|
+
dur?: number;
|
|
136
|
+
pid: number;
|
|
137
|
+
tid: number;
|
|
138
|
+
id?: string;
|
|
139
|
+
s?: 'g' | 'p' | 't';
|
|
140
|
+
args?: Record<string, unknown>;
|
|
141
|
+
}
|
|
142
|
+
/** Chrome Trace Event JSON: Perfetto and `chrome://tracing` open it. */
|
|
143
|
+
export interface ChromeTrace {
|
|
144
|
+
traceEvents: TraceEvent[];
|
|
145
|
+
displayTimeUnit: 'ms';
|
|
146
|
+
otherData: Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
export declare class Capture {
|
|
149
|
+
readonly mode: 'frames' | 'until';
|
|
150
|
+
readonly truncated: boolean;
|
|
151
|
+
readonly trigger: {
|
|
152
|
+
frame: number;
|
|
153
|
+
frameMs: number;
|
|
154
|
+
} | undefined;
|
|
155
|
+
readonly timedOut: boolean;
|
|
156
|
+
readonly frames: CaptureFrames;
|
|
157
|
+
readonly events: CaptureEvents;
|
|
158
|
+
/** The frame (index into `frames`) each event belongs to, or -1 outside the window. */
|
|
159
|
+
readonly frameOf: Int32Array;
|
|
160
|
+
/** The event each event nests in on its track, or -1. */
|
|
161
|
+
readonly parent: Int32Array;
|
|
162
|
+
/** Each event's own time: its duration less the events nested directly in it. */
|
|
163
|
+
readonly self: Float64Array;
|
|
164
|
+
constructor(init: CaptureInit);
|
|
165
|
+
get frameCount(): number;
|
|
166
|
+
get eventCount(): number;
|
|
167
|
+
/**
|
|
168
|
+
* When frame `k` ends: the next one's start. The last frame lasts as long as its CPU time, or the
|
|
169
|
+
* interval before it if that's longer, so GPU spans placed from its submit time stay in it.
|
|
170
|
+
*/
|
|
171
|
+
frameEnd(k: number): number;
|
|
172
|
+
/**
|
|
173
|
+
* Time `key` covers in each frame (0075's budget keys): every span the key covers
|
|
174
|
+
* (`spanCovers`), with time covered twice counted once: a covered span nested in another covered
|
|
175
|
+
* span adds nothing.
|
|
176
|
+
*/
|
|
177
|
+
spanTime(key: string): Float64Array;
|
|
178
|
+
/** The summary (0074): frame stats, the top spans, and the worst frames with what grew in them. */
|
|
179
|
+
summary(extras: SummaryExtras): CaptureSummary;
|
|
180
|
+
/**
|
|
181
|
+
* Chrome Trace Event JSON: an `X` event per span, `b`/`e` pairs for async spans, a thread per
|
|
182
|
+
* track, and the frame in each event's `args`. Times are µs from the first frame's start.
|
|
183
|
+
*/
|
|
184
|
+
trace(extras?: {
|
|
185
|
+
samples?: readonly TraceSample[];
|
|
186
|
+
otherData?: Record<string, unknown>;
|
|
187
|
+
}): ChromeTrace;
|
|
188
|
+
/** Each event's frame: the last frame that started at or before it, within the window. */
|
|
189
|
+
private attribute;
|
|
190
|
+
/** Nesting by containment on each track that nests, and each event's self time. */
|
|
191
|
+
private nest;
|
|
192
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { ShardError } from "../../error.js";
|
|
2
|
+
import { spanCovers, spanName, TRACK, trackName } from "../profiler.js";
|
|
3
|
+
const EPSILON = 1e-3;
|
|
4
|
+
const SAMPLES_TID = 255;
|
|
5
|
+
const r3 = (ms) => Math.round(ms * 1e3) / 1e3;
|
|
6
|
+
function percentileOf(sorted, p) {
|
|
7
|
+
if (sorted.length === 0) return 0;
|
|
8
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
9
|
+
}
|
|
10
|
+
function statsOf(values) {
|
|
11
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
12
|
+
return {
|
|
13
|
+
p50: r3(percentileOf(sorted, 0.5)),
|
|
14
|
+
p95: r3(percentileOf(sorted, 0.95)),
|
|
15
|
+
max: r3(sorted[sorted.length - 1] ?? 0)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function median(values) {
|
|
19
|
+
const sorted = Float64Array.from(values).sort();
|
|
20
|
+
return percentileOf(sorted, 0.5);
|
|
21
|
+
}
|
|
22
|
+
const nests = (track) => track !== TRACK.async && track !== TRACK.gc;
|
|
23
|
+
function threadName(track) {
|
|
24
|
+
if (track === TRACK.main) return "main";
|
|
25
|
+
if (track === TRACK.gpu) return "gpu (approximate: placed at submit)";
|
|
26
|
+
if (track === TRACK.async) return "async";
|
|
27
|
+
if (track === TRACK.gc) return "gc";
|
|
28
|
+
return `worker ${track - TRACK.worker}`;
|
|
29
|
+
}
|
|
30
|
+
class Capture {
|
|
31
|
+
mode;
|
|
32
|
+
truncated;
|
|
33
|
+
trigger;
|
|
34
|
+
timedOut;
|
|
35
|
+
frames;
|
|
36
|
+
events;
|
|
37
|
+
/** The frame (index into `frames`) each event belongs to, or -1 outside the window. */
|
|
38
|
+
frameOf;
|
|
39
|
+
/** The event each event nests in on its track, or -1. */
|
|
40
|
+
parent;
|
|
41
|
+
/** Each event's own time: its duration less the events nested directly in it. */
|
|
42
|
+
self;
|
|
43
|
+
constructor(init) {
|
|
44
|
+
this.mode = init.mode;
|
|
45
|
+
this.truncated = init.truncated;
|
|
46
|
+
this.trigger = init.trigger;
|
|
47
|
+
this.timedOut = init.timedOut;
|
|
48
|
+
this.frames = init.frames;
|
|
49
|
+
this.events = init.events;
|
|
50
|
+
const n = init.events.ids.length;
|
|
51
|
+
this.frameOf = new Int32Array(n);
|
|
52
|
+
this.parent = new Int32Array(n).fill(-1);
|
|
53
|
+
this.self = Float64Array.from(init.events.durations);
|
|
54
|
+
this.attribute();
|
|
55
|
+
this.nest();
|
|
56
|
+
}
|
|
57
|
+
get frameCount() {
|
|
58
|
+
return this.frames.numbers.length;
|
|
59
|
+
}
|
|
60
|
+
get eventCount() {
|
|
61
|
+
return this.events.ids.length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* When frame `k` ends: the next one's start. The last frame lasts as long as its CPU time, or the
|
|
65
|
+
* interval before it if that's longer, so GPU spans placed from its submit time stay in it.
|
|
66
|
+
*/
|
|
67
|
+
frameEnd(k) {
|
|
68
|
+
const f = this.frames;
|
|
69
|
+
if (k + 1 < f.starts.length) return f.starts[k + 1];
|
|
70
|
+
const interval = k > 0 ? f.starts[k] - f.starts[k - 1] : 0;
|
|
71
|
+
return f.starts[k] + Math.max(f.cpu[k] || 0, interval);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Time `key` covers in each frame (0075's budget keys): every span the key covers
|
|
75
|
+
* (`spanCovers`), with time covered twice counted once: a covered span nested in another covered
|
|
76
|
+
* span adds nothing.
|
|
77
|
+
*/
|
|
78
|
+
spanTime(key) {
|
|
79
|
+
const out = new Float64Array(this.frameCount);
|
|
80
|
+
const covered = new Uint8Array(this.eventCount);
|
|
81
|
+
const cache = /* @__PURE__ */ new Map();
|
|
82
|
+
const covers = (id) => {
|
|
83
|
+
let c = cache.get(id);
|
|
84
|
+
if (c === void 0) {
|
|
85
|
+
c = spanCovers(key, spanName(id));
|
|
86
|
+
cache.set(id, c);
|
|
87
|
+
}
|
|
88
|
+
return c;
|
|
89
|
+
};
|
|
90
|
+
const { ids, durations } = this.events;
|
|
91
|
+
for (let e = 0; e < this.eventCount; e++) covered[e] = covers(ids[e]) ? 1 : 0;
|
|
92
|
+
for (let e = 0; e < this.eventCount; e++) {
|
|
93
|
+
const f = this.frameOf[e];
|
|
94
|
+
if (f < 0 || !covered[e]) continue;
|
|
95
|
+
let p = this.parent[e];
|
|
96
|
+
let inside = false;
|
|
97
|
+
while (p >= 0) {
|
|
98
|
+
if (covered[p]) {
|
|
99
|
+
inside = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
p = this.parent[p];
|
|
103
|
+
}
|
|
104
|
+
if (!inside) out[f] = out[f] + durations[e];
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** The summary (0074): frame stats, the top spans, and the worst frames with what grew in them. */
|
|
109
|
+
summary(extras) {
|
|
110
|
+
const F = this.frameCount;
|
|
111
|
+
const { ids, tracks, durations } = this.events;
|
|
112
|
+
const totals = /* @__PURE__ */ new Map();
|
|
113
|
+
const selfs = /* @__PURE__ */ new Map();
|
|
114
|
+
const ran = /* @__PURE__ */ new Map();
|
|
115
|
+
const calls = /* @__PURE__ */ new Map();
|
|
116
|
+
const trackOf = /* @__PURE__ */ new Map();
|
|
117
|
+
for (let e = 0; e < this.eventCount; e++) {
|
|
118
|
+
const f = this.frameOf[e];
|
|
119
|
+
if (f < 0) continue;
|
|
120
|
+
const id = ids[e];
|
|
121
|
+
let total = totals.get(id);
|
|
122
|
+
if (!total) {
|
|
123
|
+
total = new Float64Array(F);
|
|
124
|
+
totals.set(id, total);
|
|
125
|
+
selfs.set(id, new Float64Array(F));
|
|
126
|
+
ran.set(id, new Uint8Array(F));
|
|
127
|
+
trackOf.set(id, tracks[e]);
|
|
128
|
+
}
|
|
129
|
+
total[f] = total[f] + durations[e];
|
|
130
|
+
const self = selfs.get(id);
|
|
131
|
+
self[f] = self[f] + this.self[e];
|
|
132
|
+
ran.get(id)[f] = 1;
|
|
133
|
+
calls.set(id, (calls.get(id) ?? 0) + 1);
|
|
134
|
+
}
|
|
135
|
+
const nameOf = new Map([...totals.keys()].map((id) => [id, spanName(id)]));
|
|
136
|
+
const excluded = (id) => nameOf.get(id) === "frame" || nameOf.get(id) === "gpu:frame";
|
|
137
|
+
const cpu = [];
|
|
138
|
+
for (let k = 0; k < F; k++)
|
|
139
|
+
if (!Number.isNaN(this.frames.cpu[k])) cpu.push(this.frames.cpu[k]);
|
|
140
|
+
const interval = [];
|
|
141
|
+
for (let k = 1; k < F; k++) interval.push(this.frames.starts[k] - this.frames.starts[k - 1]);
|
|
142
|
+
let gpuFrame;
|
|
143
|
+
let gpuRan;
|
|
144
|
+
for (const [id, name] of nameOf) {
|
|
145
|
+
if (name === "gpu:frame") {
|
|
146
|
+
gpuFrame = totals.get(id);
|
|
147
|
+
gpuRan = ran.get(id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const gpu = [];
|
|
151
|
+
if (gpuFrame && gpuRan) {
|
|
152
|
+
for (let k = 0; k < F; k++) if (gpuRan[k]) gpu.push(gpuFrame[k]);
|
|
153
|
+
}
|
|
154
|
+
const top = [];
|
|
155
|
+
for (const [id, total] of totals) {
|
|
156
|
+
if (excluded(id)) continue;
|
|
157
|
+
const r = ran.get(id);
|
|
158
|
+
const perFrame = [];
|
|
159
|
+
let sum = 0;
|
|
160
|
+
for (let k = 0; k < F; k++) {
|
|
161
|
+
sum += total[k];
|
|
162
|
+
if (r[k]) perFrame.push(total[k]);
|
|
163
|
+
}
|
|
164
|
+
top.push({
|
|
165
|
+
span: nameOf.get(id),
|
|
166
|
+
track: trackName(trackOf.get(id)),
|
|
167
|
+
calls: calls.get(id),
|
|
168
|
+
total: r3(sum),
|
|
169
|
+
...statsOf(perFrame)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
top.sort((a, b) => b.total - a.total || (a.span < b.span ? -1 : 1));
|
|
173
|
+
top.length = Math.min(top.length, 20);
|
|
174
|
+
const medianSelf = /* @__PURE__ */ new Map();
|
|
175
|
+
const medianTotal = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const [id, total] of totals) {
|
|
177
|
+
medianSelf.set(id, median(selfs.get(id)));
|
|
178
|
+
medianTotal.set(id, median(total));
|
|
179
|
+
}
|
|
180
|
+
const order = [];
|
|
181
|
+
for (let k = 0; k < F; k++) if (!Number.isNaN(this.frames.cpu[k])) order.push(k);
|
|
182
|
+
order.sort((a, b) => this.frames.cpu[b] - this.frames.cpu[a] || a - b);
|
|
183
|
+
const worst = order.slice(0, 5).map((k) => {
|
|
184
|
+
const over = [];
|
|
185
|
+
for (const [id, self] of selfs) {
|
|
186
|
+
if (excluded(id)) continue;
|
|
187
|
+
const excess = self[k] - medianSelf.get(id);
|
|
188
|
+
if (excess <= 0.01) continue;
|
|
189
|
+
over.push({
|
|
190
|
+
span: nameOf.get(id),
|
|
191
|
+
ms: r3(totals.get(id)[k]),
|
|
192
|
+
medianMs: r3(medianTotal.get(id)),
|
|
193
|
+
excess
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
over.sort((a, b) => b.excess - a.excess || (a.span < b.span ? -1 : 1));
|
|
197
|
+
const entry = {
|
|
198
|
+
frame: this.frames.numbers[k],
|
|
199
|
+
cpuMs: r3(this.frames.cpu[k]),
|
|
200
|
+
over: over.slice(0, 5).map(({ span, ms, medianMs }) => ({ span, ms, medianMs }))
|
|
201
|
+
};
|
|
202
|
+
if (gpuFrame && gpuRan?.[k]) entry.gpuMs = r3(gpuFrame[k]);
|
|
203
|
+
return entry;
|
|
204
|
+
});
|
|
205
|
+
const warnings = [...extras.warnings ?? []];
|
|
206
|
+
const warn = (error) => warnings.push(error.toJSON());
|
|
207
|
+
if (!extras.clock.isolated) {
|
|
208
|
+
warn(
|
|
209
|
+
new ShardError(
|
|
210
|
+
"perf/clock-coarse",
|
|
211
|
+
`The page isn't cross-origin isolated: its clock steps ${extras.clock.resolutionMs} ms, so short spans read as 0 or one step`,
|
|
212
|
+
{
|
|
213
|
+
hint: "Serve it with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: credentialless (shard dev and the playground do)."
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (this.truncated) {
|
|
219
|
+
warn(
|
|
220
|
+
new ShardError(
|
|
221
|
+
"perf/capture-truncated",
|
|
222
|
+
"The event buffer filled: the capture stopped early",
|
|
223
|
+
{
|
|
224
|
+
hint: 'Capture fewer frames, or pass a larger "events".'
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (this.timedOut) {
|
|
230
|
+
warn(
|
|
231
|
+
new ShardError(
|
|
232
|
+
"perf/no-slow-frame",
|
|
233
|
+
`No frame went over the flight recorder's "frameMs" before it timed out`,
|
|
234
|
+
{ hint: 'Lower "until.frameMs", or raise "timeout".' }
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const summary = {
|
|
239
|
+
frames: {
|
|
240
|
+
count: F,
|
|
241
|
+
cpu: statsOf(cpu),
|
|
242
|
+
interval: statsOf(interval)
|
|
243
|
+
},
|
|
244
|
+
clock: extras.clock,
|
|
245
|
+
top,
|
|
246
|
+
worst,
|
|
247
|
+
memory: extras.memory ?? { gpu: { bytes: 0, byCategory: {} } },
|
|
248
|
+
truncated: this.truncated,
|
|
249
|
+
capture: {
|
|
250
|
+
mode: this.mode,
|
|
251
|
+
range: F > 0 ? [this.frames.numbers[0], this.frames.numbers[F - 1]] : null
|
|
252
|
+
},
|
|
253
|
+
warnings
|
|
254
|
+
};
|
|
255
|
+
if (gpu.length > 0) summary.frames.gpu = statsOf(gpu);
|
|
256
|
+
if (this.trigger) summary.capture.trigger = this.trigger;
|
|
257
|
+
if (this.timedOut) summary.capture.timedOut = true;
|
|
258
|
+
if (extras.hottest) summary.hottest = extras.hottest;
|
|
259
|
+
return summary;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Chrome Trace Event JSON: an `X` event per span, `b`/`e` pairs for async spans, a thread per
|
|
263
|
+
* track, and the frame in each event's `args`. Times are µs from the first frame's start.
|
|
264
|
+
*/
|
|
265
|
+
trace(extras = {}) {
|
|
266
|
+
const { ids, tracks, starts, durations } = this.events;
|
|
267
|
+
const origin = this.frameCount > 0 ? this.frames.starts[0] : 0;
|
|
268
|
+
const us = (ms) => Math.round((ms - origin) * 1e6) / 1e3;
|
|
269
|
+
const dus = (ms) => Math.round(ms * 1e6) / 1e3;
|
|
270
|
+
const order = [];
|
|
271
|
+
const used = /* @__PURE__ */ new Set();
|
|
272
|
+
for (let e = 0; e < this.eventCount; e++) {
|
|
273
|
+
if (this.frameOf[e] < 0) continue;
|
|
274
|
+
order.push(e);
|
|
275
|
+
used.add(tracks[e]);
|
|
276
|
+
}
|
|
277
|
+
order.sort(
|
|
278
|
+
(a, b) => tracks[a] - tracks[b] || starts[a] - starts[b] || durations[b] - durations[a] || a - b
|
|
279
|
+
);
|
|
280
|
+
const events = [
|
|
281
|
+
{ name: "process_name", ph: "M", pid: 1, tid: 0, args: { name: "Shard" } }
|
|
282
|
+
];
|
|
283
|
+
const threads = [...used].sort((a, b) => a - b);
|
|
284
|
+
if (extras.samples?.length) threads.push(SAMPLES_TID - 1);
|
|
285
|
+
for (const track of threads) {
|
|
286
|
+
const tid = track + 1;
|
|
287
|
+
const name = tid === SAMPLES_TID ? "js samples" : threadName(track);
|
|
288
|
+
events.push({ name: "thread_name", ph: "M", pid: 1, tid, args: { name } });
|
|
289
|
+
events.push({ name: "thread_sort_index", ph: "M", pid: 1, tid, args: { sort_index: tid } });
|
|
290
|
+
}
|
|
291
|
+
for (const e of order) {
|
|
292
|
+
const track = tracks[e];
|
|
293
|
+
const name = spanName(ids[e]);
|
|
294
|
+
const frame = this.frames.numbers[this.frameOf[e]];
|
|
295
|
+
const cat = trackName(track);
|
|
296
|
+
if (track === TRACK.async) {
|
|
297
|
+
const id = `0x${e.toString(16)}`;
|
|
298
|
+
events.push({
|
|
299
|
+
name,
|
|
300
|
+
cat,
|
|
301
|
+
ph: "b",
|
|
302
|
+
ts: us(starts[e]),
|
|
303
|
+
pid: 1,
|
|
304
|
+
tid: track + 1,
|
|
305
|
+
id,
|
|
306
|
+
args: { frame }
|
|
307
|
+
});
|
|
308
|
+
events.push({
|
|
309
|
+
name,
|
|
310
|
+
cat,
|
|
311
|
+
ph: "e",
|
|
312
|
+
ts: us(starts[e] + durations[e]),
|
|
313
|
+
pid: 1,
|
|
314
|
+
tid: track + 1,
|
|
315
|
+
id,
|
|
316
|
+
args: { frame }
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
events.push({
|
|
321
|
+
name,
|
|
322
|
+
cat,
|
|
323
|
+
ph: "X",
|
|
324
|
+
ts: us(starts[e]),
|
|
325
|
+
dur: dus(durations[e]),
|
|
326
|
+
pid: 1,
|
|
327
|
+
tid: track + 1,
|
|
328
|
+
args: { frame }
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
for (const s of extras.samples ?? []) {
|
|
332
|
+
events.push({
|
|
333
|
+
name: s.name,
|
|
334
|
+
cat: "samples",
|
|
335
|
+
ph: "X",
|
|
336
|
+
ts: us(s.start),
|
|
337
|
+
dur: dus(s.ms),
|
|
338
|
+
pid: 1,
|
|
339
|
+
tid: SAMPLES_TID
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
traceEvents: events,
|
|
344
|
+
displayTimeUnit: "ms",
|
|
345
|
+
otherData: {
|
|
346
|
+
producer: "shard profiler (0074)",
|
|
347
|
+
gpuTrack: "Approximate: WebGPU's clock isn't the CPU's, so each frame's GPU passes are placed from its submit time.",
|
|
348
|
+
frames: this.frameCount > 0 ? [this.frames.numbers[0], this.frames.numbers[this.frameCount - 1]] : [],
|
|
349
|
+
...extras.otherData
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// --- internals ---------------------------------------------------------------
|
|
354
|
+
/** Each event's frame: the last frame that started at or before it, within the window. */
|
|
355
|
+
attribute() {
|
|
356
|
+
const starts = this.frames.starts;
|
|
357
|
+
const F = starts.length;
|
|
358
|
+
const lastEnd = F > 0 ? this.frameEnd(F - 1) : 0;
|
|
359
|
+
const evStarts = this.events.starts;
|
|
360
|
+
for (let e = 0; e < this.eventCount; e++) {
|
|
361
|
+
const t = evStarts[e];
|
|
362
|
+
if (F === 0 || t < starts[0] - EPSILON || t > lastEnd + EPSILON) {
|
|
363
|
+
this.frameOf[e] = -1;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
let lo = 0;
|
|
367
|
+
let hi = F - 1;
|
|
368
|
+
while (lo < hi) {
|
|
369
|
+
const mid = lo + hi + 1 >> 1;
|
|
370
|
+
if (starts[mid] <= t + EPSILON) lo = mid;
|
|
371
|
+
else hi = mid - 1;
|
|
372
|
+
}
|
|
373
|
+
this.frameOf[e] = lo;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Nesting by containment on each track that nests, and each event's self time. */
|
|
377
|
+
nest() {
|
|
378
|
+
const { tracks, starts, durations, depths } = this.events;
|
|
379
|
+
const order = [];
|
|
380
|
+
for (let e = 0; e < this.eventCount; e++) if (nests(tracks[e])) order.push(e);
|
|
381
|
+
order.sort(
|
|
382
|
+
(a, b) => tracks[a] - tracks[b] || starts[a] - starts[b] || durations[b] - durations[a] || depths[a] - depths[b] || a - b
|
|
383
|
+
);
|
|
384
|
+
const stack = [];
|
|
385
|
+
let track = -1;
|
|
386
|
+
for (const e of order) {
|
|
387
|
+
if (tracks[e] !== track) {
|
|
388
|
+
track = tracks[e];
|
|
389
|
+
stack.length = 0;
|
|
390
|
+
}
|
|
391
|
+
const start = starts[e];
|
|
392
|
+
const end = start + durations[e];
|
|
393
|
+
while (stack.length > 0) {
|
|
394
|
+
const p = stack[stack.length - 1];
|
|
395
|
+
if (start >= starts[p] - EPSILON && end <= starts[p] + durations[p] + EPSILON) break;
|
|
396
|
+
stack.pop();
|
|
397
|
+
}
|
|
398
|
+
if (stack.length > 0) {
|
|
399
|
+
const p = stack[stack.length - 1];
|
|
400
|
+
this.parent[e] = p;
|
|
401
|
+
this.self[p] = this.self[p] - durations[e];
|
|
402
|
+
}
|
|
403
|
+
stack.push(e);
|
|
404
|
+
}
|
|
405
|
+
for (let e = 0; e < this.eventCount; e++) if (this.self[e] < 0) this.self[e] = 0;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
export {
|
|
409
|
+
Capture
|
|
410
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Capture, type CaptureEvents, type CaptureFrames, type CaptureInit, type CaptureSummary, type CaptureWarning, type ChromeTrace, type HeapStats, type HotFunction, type SpanStats, type Stats, type SummaryExtras, type TraceEvent, type TraceSample, } from './capture.js';
|
|
2
|
+
export { type CaptureOptions, CaptureRecorder, startCapture } from './recorder.js';
|
|
3
|
+
export { type CpuProfile, hottestFromCpuProfile, hottestFromSelfProfile, type SelfProfileTrace, samplesFromSelfProfile, } from './samples.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Capture
|
|
3
|
+
} from "./capture.js";
|
|
4
|
+
import { CaptureRecorder, startCapture } from "./recorder.js";
|
|
5
|
+
import {
|
|
6
|
+
hottestFromCpuProfile,
|
|
7
|
+
hottestFromSelfProfile,
|
|
8
|
+
samplesFromSelfProfile
|
|
9
|
+
} from "./samples.js";
|
|
10
|
+
export {
|
|
11
|
+
Capture,
|
|
12
|
+
CaptureRecorder,
|
|
13
|
+
hottestFromCpuProfile,
|
|
14
|
+
hottestFromSelfProfile,
|
|
15
|
+
samplesFromSelfProfile,
|
|
16
|
+
startCapture
|
|
17
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type Profiler, type ProfilerSink } from '../profiler.js';
|
|
2
|
+
import { Capture } from './capture.js';
|
|
3
|
+
export interface CaptureOptions {
|
|
4
|
+
/** Captures the next N frames. Default 300 when `until` isn't given. */
|
|
5
|
+
frames?: number;
|
|
6
|
+
/**
|
|
7
|
+
* The flight recorder: keeps a rolling window and stops `after` frames past the first frame whose
|
|
8
|
+
* CPU time is over `frameMs`.
|
|
9
|
+
*/
|
|
10
|
+
until?: {
|
|
11
|
+
frameMs: number;
|
|
12
|
+
};
|
|
13
|
+
/** Frames the flight recorder keeps before the slow one. Default 120. */
|
|
14
|
+
before?: number;
|
|
15
|
+
/** Frames it records after the slow one. Default 30. */
|
|
16
|
+
after?: number;
|
|
17
|
+
/** Seconds the flight recorder waits for a slow frame before giving up. Default 60. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/** Event buffer size: 16 bytes an event. Default 2²⁰ (16 MB). */
|
|
20
|
+
events?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Also hands each span to this as it's recorded, e.g. `performance.measure` with DevTools track
|
|
23
|
+
* data. It allocates: off unless asked for.
|
|
24
|
+
*/
|
|
25
|
+
devtools?: (name: string, track: number, start: number, ms: number) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A running capture (0074): the profiler's sink while it records. Events go into one preallocated
|
|
29
|
+
* buffer of 16-byte records (start f64, duration f32, span id u16, track u8, depth u8), so recording
|
|
30
|
+
* allocates nothing once it started.
|
|
31
|
+
*/
|
|
32
|
+
export declare class CaptureRecorder implements ProfilerSink {
|
|
33
|
+
readonly mode: 'frames' | 'until';
|
|
34
|
+
readonly capacity: number;
|
|
35
|
+
/** Resolves once the capture has every frame it wanted (or gave up waiting). */
|
|
36
|
+
readonly done: Promise<Capture>;
|
|
37
|
+
private readonly profiler;
|
|
38
|
+
private readonly starts;
|
|
39
|
+
private readonly durations;
|
|
40
|
+
private readonly ids;
|
|
41
|
+
private readonly bytes;
|
|
42
|
+
private cursor;
|
|
43
|
+
private written;
|
|
44
|
+
private readonly frameCap;
|
|
45
|
+
private readonly frameNumbers;
|
|
46
|
+
private readonly frameStarts;
|
|
47
|
+
private readonly frameCpu;
|
|
48
|
+
private readonly frameFirst;
|
|
49
|
+
private frameCount;
|
|
50
|
+
private state;
|
|
51
|
+
private readonly frames;
|
|
52
|
+
private readonly untilMs;
|
|
53
|
+
private readonly after;
|
|
54
|
+
private readonly timeoutMs;
|
|
55
|
+
private startedAt;
|
|
56
|
+
private trigger;
|
|
57
|
+
private triggerMs;
|
|
58
|
+
private afterLeft;
|
|
59
|
+
private drainLeft;
|
|
60
|
+
private gpuSeen;
|
|
61
|
+
private firstNumber;
|
|
62
|
+
private lastNumber;
|
|
63
|
+
private timedOut;
|
|
64
|
+
private truncatedFlag;
|
|
65
|
+
private stopping;
|
|
66
|
+
private readonly devtools;
|
|
67
|
+
private resolveDone;
|
|
68
|
+
constructor(profiler: Profiler, options?: CaptureOptions);
|
|
69
|
+
/** Starts listening: from the next frame on. Throws `perf/capture-running` if one already is. */
|
|
70
|
+
start(): this;
|
|
71
|
+
/** Ends the capture now with what it has. */
|
|
72
|
+
stop(): void;
|
|
73
|
+
get running(): boolean;
|
|
74
|
+
event(id: number, track: number, depth: number, start: number, ms: number, frame: number): void;
|
|
75
|
+
frameStart(frame: number, at: number): void;
|
|
76
|
+
frameEnd(_frame: number, ms: number): void;
|
|
77
|
+
private finish;
|
|
78
|
+
private complete;
|
|
79
|
+
/** The recorded frames and events, in order. */
|
|
80
|
+
private result;
|
|
81
|
+
}
|
|
82
|
+
/** Starts a capture on `profiler`: resolves with the capture once it has its frames. */
|
|
83
|
+
export declare function startCapture(profiler: Profiler, options?: CaptureOptions): CaptureRecorder;
|