@aethervtt/shard-core 0.0.1 → 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.
@@ -0,0 +1,224 @@
1
+ import { ShardError } from "../../error.js";
2
+ import { spanName as spanNameOf, TRACK } from "../profiler.js";
3
+ import { Capture } from "./capture.js";
4
+ const WAITING = 0;
5
+ const RECORDING = 1;
6
+ const DRAINING = 2;
7
+ const DONE = 3;
8
+ const GPU_DRAIN_FRAMES = 4;
9
+ const DEFAULT_EVENTS = 1 << 20;
10
+ class CaptureRecorder {
11
+ mode;
12
+ capacity;
13
+ /** Resolves once the capture has every frame it wanted (or gave up waiting). */
14
+ done;
15
+ profiler;
16
+ starts;
17
+ durations;
18
+ ids;
19
+ bytes;
20
+ cursor = 0;
21
+ written = 0;
22
+ frameCap;
23
+ frameNumbers;
24
+ frameStarts;
25
+ frameCpu;
26
+ frameFirst;
27
+ frameCount = 0;
28
+ state = WAITING;
29
+ frames;
30
+ untilMs;
31
+ after;
32
+ timeoutMs;
33
+ startedAt = Number.NaN;
34
+ trigger = -1;
35
+ triggerMs = 0;
36
+ afterLeft = 0;
37
+ drainLeft = 0;
38
+ gpuSeen = false;
39
+ firstNumber = Number.POSITIVE_INFINITY;
40
+ lastNumber = Number.NEGATIVE_INFINITY;
41
+ timedOut = false;
42
+ truncatedFlag = false;
43
+ stopping = false;
44
+ devtools;
45
+ resolveDone;
46
+ constructor(profiler, options = {}) {
47
+ this.profiler = profiler;
48
+ this.mode = options.until ? "until" : "frames";
49
+ this.capacity = Math.max(16, options.events ?? DEFAULT_EVENTS);
50
+ const buffer = new ArrayBuffer(this.capacity * 16);
51
+ this.starts = new Float64Array(buffer);
52
+ this.durations = new Float32Array(buffer);
53
+ this.ids = new Uint16Array(buffer);
54
+ this.bytes = new Uint8Array(buffer);
55
+ this.frames = Math.max(1, options.frames ?? 300);
56
+ this.untilMs = options.until?.frameMs ?? Number.POSITIVE_INFINITY;
57
+ const before = Math.max(0, options.before ?? 120);
58
+ this.after = Math.max(0, options.after ?? 30);
59
+ this.timeoutMs = (options.timeout ?? 60) * 1e3;
60
+ this.frameCap = this.mode === "frames" ? this.frames : before + 1 + this.after;
61
+ this.frameNumbers = new Float64Array(this.frameCap);
62
+ this.frameStarts = new Float64Array(this.frameCap);
63
+ this.frameCpu = new Float64Array(this.frameCap);
64
+ this.frameFirst = new Float64Array(this.frameCap);
65
+ this.devtools = options.devtools;
66
+ this.done = new Promise((resolve) => {
67
+ this.resolveDone = resolve;
68
+ });
69
+ }
70
+ /** Starts listening: from the next frame on. Throws `perf/capture-running` if one already is. */
71
+ start() {
72
+ if (this.profiler.sink !== void 0) {
73
+ throw new ShardError("perf/capture-running", "A capture is already running on this app", {
74
+ hint: "Wait for it to finish (perf.capture resolves when it does), then capture again."
75
+ });
76
+ }
77
+ if (!this.profiler.enabled) {
78
+ throw new ShardError("perf/disabled", "The profiler is off (ProfilerSettings.enabled)", {
79
+ hint: "Turn it on with resource.set core/ProfilerSettings { enabled: true }."
80
+ });
81
+ }
82
+ this.profiler.sink = this;
83
+ this.startedAt = this.profiler.now();
84
+ return this;
85
+ }
86
+ /** Ends the capture now with what it has. */
87
+ stop() {
88
+ if (this.state === DONE) return;
89
+ this.finish();
90
+ }
91
+ get running() {
92
+ return this.state !== DONE;
93
+ }
94
+ // --- the sink ----------------------------------------------------------------
95
+ event(id, track, depth, start, ms, frame) {
96
+ const state = this.state;
97
+ if (state !== RECORDING && (state !== DRAINING || track !== TRACK.gpu)) return;
98
+ if (frame >= 0 && (frame < this.firstNumber || state === DRAINING && frame > this.lastNumber))
99
+ return;
100
+ if (this.mode === "frames" && this.written >= this.capacity) {
101
+ this.truncatedFlag = true;
102
+ this.stopping = true;
103
+ return;
104
+ }
105
+ const i = this.cursor;
106
+ this.starts[i * 2] = start;
107
+ this.durations[i * 4 + 2] = ms;
108
+ this.ids[i * 8 + 6] = id;
109
+ this.bytes[i * 16 + 14] = track;
110
+ this.bytes[i * 16 + 15] = depth > 255 ? 255 : depth;
111
+ this.cursor = i + 1 === this.capacity ? 0 : i + 1;
112
+ this.written++;
113
+ if (track === TRACK.gpu) this.gpuSeen = true;
114
+ if (this.devtools !== void 0) this.devtools(spanNameOf(id), track, start, ms);
115
+ }
116
+ frameStart(frame, at) {
117
+ if (this.state === WAITING) {
118
+ this.state = RECORDING;
119
+ this.firstNumber = frame;
120
+ }
121
+ if (this.state !== RECORDING) return;
122
+ const slot = this.frameCount % this.frameCap;
123
+ this.frameNumbers[slot] = frame;
124
+ this.frameStarts[slot] = at;
125
+ this.frameCpu[slot] = Number.NaN;
126
+ this.frameFirst[slot] = this.written;
127
+ }
128
+ frameEnd(_frame, ms) {
129
+ if (this.state === DRAINING) {
130
+ if (--this.drainLeft <= 0) this.complete();
131
+ return;
132
+ }
133
+ if (this.state !== RECORDING) return;
134
+ this.frameCpu[this.frameCount % this.frameCap] = ms;
135
+ this.frameCount++;
136
+ if (this.stopping) {
137
+ this.finish();
138
+ return;
139
+ }
140
+ if (this.mode === "frames") {
141
+ if (this.frameCount >= this.frames) this.finish();
142
+ return;
143
+ }
144
+ if (this.trigger < 0) {
145
+ if (ms > this.untilMs) {
146
+ this.trigger = this.frameNumbers[(this.frameCount - 1) % this.frameCap];
147
+ this.triggerMs = ms;
148
+ this.afterLeft = this.after;
149
+ if (this.afterLeft === 0) this.finish();
150
+ } else if (this.profiler.now() - this.startedAt > this.timeoutMs) {
151
+ this.timedOut = true;
152
+ this.finish();
153
+ }
154
+ return;
155
+ }
156
+ if (--this.afterLeft <= 0) this.finish();
157
+ }
158
+ // --- finishing ---------------------------------------------------------------
159
+ finish() {
160
+ if (this.frameCount > 0)
161
+ this.lastNumber = this.frameNumbers[(this.frameCount - 1) % this.frameCap];
162
+ if (this.gpuSeen && this.state === RECORDING) {
163
+ this.state = DRAINING;
164
+ this.drainLeft = GPU_DRAIN_FRAMES;
165
+ return;
166
+ }
167
+ this.complete();
168
+ }
169
+ complete() {
170
+ if (this.state === DONE) return;
171
+ this.state = DONE;
172
+ if (this.profiler.sink === this) this.profiler.sink = void 0;
173
+ this.resolveDone(this.result());
174
+ }
175
+ /** The recorded frames and events, in order. */
176
+ result() {
177
+ const frames = Math.min(this.frameCount, this.frameCap);
178
+ const firstFrame = this.frameCount - frames;
179
+ const oldest = Math.max(0, this.written - this.capacity);
180
+ let skip = 0;
181
+ while (skip < frames && this.frameFirst[(firstFrame + skip) % this.frameCap] < oldest) skip++;
182
+ const kept = frames - skip;
183
+ const frameNumbers = new Float64Array(kept);
184
+ const frameStarts = new Float64Array(kept);
185
+ const frameCpu = new Float64Array(kept);
186
+ for (let k = 0; k < kept; k++) {
187
+ const slot = (firstFrame + skip + k) % this.frameCap;
188
+ frameNumbers[k] = this.frameNumbers[slot];
189
+ frameStarts[k] = this.frameStarts[slot];
190
+ frameCpu[k] = this.frameCpu[slot];
191
+ }
192
+ const firstEvent = kept > 0 ? this.frameFirst[(firstFrame + skip) % this.frameCap] : this.written;
193
+ const count = this.written - Math.max(firstEvent, oldest);
194
+ const ids = new Uint16Array(count);
195
+ const tracks = new Uint8Array(count);
196
+ const depths = new Uint8Array(count);
197
+ const starts = new Float64Array(count);
198
+ const durations = new Float64Array(count);
199
+ for (let k = 0; k < count; k++) {
200
+ const n = this.written - count + k;
201
+ const i = n % this.capacity;
202
+ ids[k] = this.ids[i * 8 + 6];
203
+ tracks[k] = this.bytes[i * 16 + 14];
204
+ depths[k] = this.bytes[i * 16 + 15];
205
+ starts[k] = this.starts[i * 2];
206
+ durations[k] = this.durations[i * 4 + 2];
207
+ }
208
+ return new Capture({
209
+ frames: { numbers: frameNumbers, starts: frameStarts, cpu: frameCpu },
210
+ events: { ids, tracks, depths, starts, durations },
211
+ mode: this.mode,
212
+ truncated: this.truncatedFlag,
213
+ trigger: this.trigger < 0 ? void 0 : { frame: this.trigger, frameMs: this.triggerMs },
214
+ timedOut: this.timedOut
215
+ });
216
+ }
217
+ }
218
+ function startCapture(profiler, options = {}) {
219
+ return new CaptureRecorder(profiler, options).start();
220
+ }
221
+ export {
222
+ CaptureRecorder,
223
+ startCapture
224
+ };
@@ -0,0 +1,49 @@
1
+ import type { HotFunction, TraceSample } from './capture.js';
2
+ /** A V8 `.cpuprofile` (DevTools' `Profiler.Profile`), as `node:inspector` and `--cpu-prof` write it. */
3
+ export interface CpuProfile {
4
+ nodes: {
5
+ id: number;
6
+ callFrame: {
7
+ functionName: string;
8
+ url: string;
9
+ lineNumber: number;
10
+ columnNumber?: number;
11
+ };
12
+ hitCount?: number;
13
+ children?: number[];
14
+ }[];
15
+ /** µs. */
16
+ startTime: number;
17
+ endTime: number;
18
+ samples?: number[];
19
+ /** µs between samples. */
20
+ timeDeltas?: number[];
21
+ }
22
+ /** What `new Profiler(...).stop()` resolves with (JS Self-Profiling). */
23
+ export interface SelfProfileTrace {
24
+ resources: string[];
25
+ frames: {
26
+ name: string;
27
+ resourceId?: number;
28
+ line?: number;
29
+ column?: number;
30
+ }[];
31
+ stacks: {
32
+ frameId: number;
33
+ parentId?: number;
34
+ }[];
35
+ /** `timestamp` on the page's performance timeline (ms). */
36
+ samples: {
37
+ timestamp: number;
38
+ stackId?: number;
39
+ }[];
40
+ }
41
+ /** The functions with the most self time in a `.cpuprofile`. */
42
+ export declare function hottestFromCpuProfile(profile: CpuProfile, limit?: number): HotFunction[];
43
+ /** The functions on top of the stack most often in a JS Self-Profiling trace. */
44
+ export declare function hottestFromSelfProfile(trace: SelfProfileTrace, intervalMs: number, limit?: number): HotFunction[];
45
+ /**
46
+ * A JS Self-Profiling trace as stretches of the same top function, for the trace's samples track.
47
+ * `offset` moves the page's timeline onto the profiler's clock.
48
+ */
49
+ export declare function samplesFromSelfProfile(trace: SelfProfileTrace, intervalMs: number, offset?: number): TraceSample[];
@@ -0,0 +1,91 @@
1
+ const SKIP = /* @__PURE__ */ new Set(["(root)", "(idle)"]);
2
+ function rank(byKey, total, limit) {
3
+ const out = [...byKey.values()];
4
+ for (const f of out) {
5
+ f.share = total > 0 ? Math.round(f.selfMs / total * 1e3) / 1e3 : 0;
6
+ f.selfMs = Math.round(f.selfMs * 1e3) / 1e3;
7
+ }
8
+ out.sort((a, b) => b.selfMs - a.selfMs || (a.name < b.name ? -1 : 1));
9
+ return out.slice(0, limit);
10
+ }
11
+ function hottestFromCpuProfile(profile, limit = 20) {
12
+ const self = /* @__PURE__ */ new Map();
13
+ const samples = profile.samples ?? [];
14
+ const deltas = profile.timeDeltas ?? [];
15
+ if (samples.length > 0 && deltas.length === samples.length) {
16
+ for (let i = 0; i < samples.length; i++) {
17
+ const dt = i + 1 < deltas.length ? deltas[i + 1] : 0;
18
+ self.set(samples[i], (self.get(samples[i]) ?? 0) + Math.max(0, dt) / 1e3);
19
+ }
20
+ } else {
21
+ let hits = 0;
22
+ for (const node of profile.nodes) hits += node.hitCount ?? 0;
23
+ const per = hits > 0 ? (profile.endTime - profile.startTime) / 1e3 / hits : 0;
24
+ for (const node of profile.nodes) self.set(node.id, (node.hitCount ?? 0) * per);
25
+ }
26
+ const byKey = /* @__PURE__ */ new Map();
27
+ let total = 0;
28
+ for (const node of profile.nodes) {
29
+ const ms = self.get(node.id) ?? 0;
30
+ const frame = node.callFrame;
31
+ if (ms <= 0 || SKIP.has(frame.functionName)) continue;
32
+ total += ms;
33
+ const name = frame.functionName || "(anonymous)";
34
+ const key = `${name}|${frame.url}|${frame.lineNumber}`;
35
+ let f = byKey.get(key);
36
+ if (!f) {
37
+ f = { name, selfMs: 0, share: 0 };
38
+ if (frame.url) f.url = frame.url;
39
+ if (frame.lineNumber >= 0) f.line = frame.lineNumber + 1;
40
+ byKey.set(key, f);
41
+ }
42
+ f.selfMs += ms;
43
+ }
44
+ return rank(byKey, total, limit);
45
+ }
46
+ function hottestFromSelfProfile(trace, intervalMs, limit = 20) {
47
+ const byKey = /* @__PURE__ */ new Map();
48
+ let total = 0;
49
+ for (let i = 0; i < trace.samples.length; i++) {
50
+ const s = trace.samples[i];
51
+ if (s.stackId === void 0) continue;
52
+ const next = trace.samples[i + 1];
53
+ const ms = next ? Math.min(next.timestamp - s.timestamp, intervalMs * 4) : intervalMs;
54
+ const frame = trace.frames[trace.stacks[s.stackId].frameId];
55
+ const url = frame.resourceId === void 0 ? void 0 : trace.resources[frame.resourceId];
56
+ const name = frame.name || "(anonymous)";
57
+ const key = `${name}|${url ?? ""}|${frame.line ?? -1}`;
58
+ let f = byKey.get(key);
59
+ if (!f) {
60
+ f = { name, selfMs: 0, share: 0 };
61
+ if (url) f.url = url;
62
+ if (frame.line !== void 0) f.line = frame.line;
63
+ byKey.set(key, f);
64
+ }
65
+ f.selfMs += ms;
66
+ total += ms;
67
+ }
68
+ return rank(byKey, total, limit);
69
+ }
70
+ function samplesFromSelfProfile(trace, intervalMs, offset = 0) {
71
+ const out = [];
72
+ for (let i = 0; i < trace.samples.length; i++) {
73
+ const s = trace.samples[i];
74
+ if (s.stackId === void 0) continue;
75
+ const name = trace.frames[trace.stacks[s.stackId].frameId].name || "(anonymous)";
76
+ const next = trace.samples[i + 1];
77
+ const ms = next ? Math.min(next.timestamp - s.timestamp, intervalMs * 4) : intervalMs;
78
+ const last = out[out.length - 1];
79
+ if (last && last.name === name && Math.abs(last.start + last.ms - (s.timestamp + offset)) < 1e-6) {
80
+ last.ms += ms;
81
+ continue;
82
+ }
83
+ out.push({ name, start: s.timestamp + offset, ms });
84
+ }
85
+ return out;
86
+ }
87
+ export {
88
+ hottestFromCpuProfile,
89
+ hottestFromSelfProfile,
90
+ samplesFromSelfProfile
91
+ };
@@ -1,3 +1,5 @@
1
- export { Profiler, ProfilerResource, type SystemTiming } from './profiler.js';
1
+ export type { CaptureSummary, CaptureWarning, ChromeTrace, HeapStats, HotFunction, SpanStats, Stats, } from './capture/capture.js';
2
+ export type { CaptureOptions } from './capture/recorder.js';
3
+ export { type ClockInfo, clockInfo, defineSpan, internSpan, Profiler, type ProfilerOptions, ProfilerResource, ProfilerSettings, type ProfilerSettingsData, type ProfilerSink, type SpanAggregate, type SpanDef, type SystemTiming, spanCount, spanCovers, spanId, spanName, spanStats, spanTrack, TRACK, type TrackName, trackName, } from './profiler.js';
2
4
  export { defineSchedule, First, FixedUpdate, Last, PostUpdate, PreUpdate, Schedule, type ScheduleLabel, type ScheduleRunOptions, Startup, type SystemDescription, Update, } from './schedule.js';
3
5
  export { type Condition, condition, conditionLabel, defineSystem, defineSystemSet, type Label, not, SystemConfig, type SystemContext, type SystemDef, type SystemSetConfig, type SystemSetDef, type SystemSpec, } from './system.js';
@@ -1,4 +1,19 @@
1
- import { Profiler, ProfilerResource } from "./profiler.js";
1
+ import {
2
+ clockInfo,
3
+ defineSpan,
4
+ internSpan,
5
+ Profiler,
6
+ ProfilerResource,
7
+ ProfilerSettings,
8
+ spanCount,
9
+ spanCovers,
10
+ spanId,
11
+ spanName,
12
+ spanStats,
13
+ spanTrack,
14
+ TRACK,
15
+ trackName
16
+ } from "./profiler.js";
2
17
  import {
3
18
  defineSchedule,
4
19
  First,
@@ -26,14 +41,26 @@ export {
26
41
  PreUpdate,
27
42
  Profiler,
28
43
  ProfilerResource,
44
+ ProfilerSettings,
29
45
  Schedule,
30
46
  Startup,
31
47
  SystemConfig,
48
+ TRACK,
32
49
  Update,
50
+ clockInfo,
33
51
  condition,
34
52
  conditionLabel,
35
53
  defineSchedule,
54
+ defineSpan,
36
55
  defineSystem,
37
56
  defineSystemSet,
38
- not
57
+ internSpan,
58
+ not,
59
+ spanCount,
60
+ spanCovers,
61
+ spanId,
62
+ spanName,
63
+ spanStats,
64
+ spanTrack,
65
+ trackName
39
66
  };
@@ -1,3 +1,57 @@
1
+ import { ShardError } from '../error.js';
2
+ /** A span name. `defineSpan` registers nothing: the first `begin` (or `record`) interns it. */
3
+ export interface SpanDef {
4
+ readonly name: string;
5
+ /** The interned id, or -1 before first use. */
6
+ id: number;
7
+ }
8
+ /** A span by name, for code that times itself: `const ENCODE = defineSpan('terrain/encode')`. */
9
+ export declare function defineSpan(name: string): SpanDef;
10
+ /**
11
+ * Timeline tracks. Workers are `TRACK.worker + n` for the pool's n-th thread, so track ids fit a
12
+ * u8 with room for 251 workers.
13
+ */
14
+ export declare const TRACK: {
15
+ readonly main: 0;
16
+ readonly gpu: 1;
17
+ readonly async: 2;
18
+ readonly gc: 3;
19
+ readonly worker: 4;
20
+ };
21
+ export type TrackName = 'main' | 'gpu' | 'async' | 'gc' | 'worker';
22
+ export declare function trackName(track: number): TrackName;
23
+ /**
24
+ * Whether a budget key covers a span (0074, 0075): the key equals the name, or is a prefix of it
25
+ * ending at a `/`. `gpu:foliage` covers `gpu:foliage/place`, `render` covers `render/opaque`, and
26
+ * `render/` does too. `render` doesn't cover `renderer/x`.
27
+ */
28
+ export declare function spanCovers(key: string, name: string): boolean;
29
+ /** The id of a span name, interning it on first use. */
30
+ export declare function internSpan(name: string, track?: number): number;
31
+ /** How many span names are interned: ids run from 0 to this. Grows as new names are used. */
32
+ export declare function spanCount(): number;
33
+ /** A span's id, or -1 if no profiler has used the name yet. */
34
+ export declare function spanId(name: string): number;
35
+ /** A span's name by id. */
36
+ export declare function spanName(id: number): string;
37
+ /** The track a span was first used on. */
38
+ export declare function spanTrack(id: number): number;
39
+ /**
40
+ * Where a capture receives the timeline. Called in the hot path: implementations write into
41
+ * preallocated buffers.
42
+ */
43
+ export interface ProfilerSink {
44
+ /**
45
+ * A span: `start` on the profiler's clock, `ms` long, `depth` on the main track's stack. `frame`
46
+ * is the frame it belongs to when that isn't the one running (GPU timings land frames later), or
47
+ * -1.
48
+ */
49
+ event(id: number, track: number, depth: number, start: number, ms: number, frame: number): void;
50
+ /** A frame starts (`frame` is `Time.frame` as it starts). */
51
+ frameStart(frame: number, at: number): void;
52
+ /** The frame that started last ends: its CPU time is `ms`. */
53
+ frameEnd(frame: number, ms: number): void;
54
+ }
1
55
  export interface SystemTiming {
2
56
  /** Most recent run, in ms. */
3
57
  last: number;
@@ -7,13 +61,134 @@ export interface SystemTiming {
7
61
  /** Number of samples in the window. */
8
62
  samples: number;
9
63
  }
10
- /** Per-system CPU time over the last `window` runs, in a ring buffer. */
64
+ /** A span's aggregates over the window, as `perf.describe` reports them. */
65
+ export interface SpanAggregate extends SystemTiming {
66
+ span: string;
67
+ track: TrackName;
68
+ /** Median over the window, in ms. */
69
+ p50: number;
70
+ /** 95th percentile over the window, in ms. */
71
+ p95: number;
72
+ /** Under four steps of the clock: the numbers are mostly its resolution. */
73
+ coarse?: true;
74
+ }
75
+ export interface ProfilerSettingsData {
76
+ /** Off: spans, records and captures do nothing. Default true. */
77
+ enabled: boolean;
78
+ /** Runs each span's aggregates cover. Default 120. */
79
+ window: number;
80
+ }
81
+ export declare const ProfilerSettings: import("..").ResourceDef<ProfilerSettingsData>;
82
+ /** The clock spans are timed with: its smallest step, and whether the page is isolated. */
83
+ export interface ClockInfo {
84
+ resolutionMs: number;
85
+ /** False on a browser page that isn't cross-origin isolated (a coarsened clock). */
86
+ isolated: boolean;
87
+ }
88
+ export interface ProfilerOptions {
89
+ /** The clock, in ms. Default `performance.now`. */
90
+ now?: () => number;
91
+ /** Shared with `ProfilerSettings`, so toggling it there takes effect. */
92
+ settings?: ProfilerSettingsData;
93
+ }
94
+ /**
95
+ * The host clock's resolution: the smallest non-zero step `performance.now()` takes over a short
96
+ * spin. Measured once. Pages that aren't cross-origin isolated report a coarsened clock.
97
+ */
98
+ export declare function clockInfo(): ClockInfo;
99
+ /**
100
+ * Spans and their aggregates (0074): per-system, per-schedule and per-frame CPU time, GPU passes,
101
+ * worker jobs and async work, over the last `window` runs of each.
102
+ */
11
103
  export declare class Profiler {
12
- readonly window: number;
13
- private readonly series;
14
- constructor(window?: number);
15
- record(name: string, ms: number): void;
104
+ readonly settings: ProfilerSettingsData;
105
+ /** The clock, in ms. */
106
+ now: () => number;
107
+ /** Receives every span while a capture runs. */
108
+ sink: ProfilerSink | undefined;
109
+ /** Gets `perf/span-mismatch` and similar warnings; the app logs them. */
110
+ onWarning: ((warning: ShardError) => void) | undefined;
111
+ /** `Time.frame` of the frame running now (or that ran last). */
112
+ frame: number;
113
+ /** The last span `end` measured, in ms. */
114
+ lastMs: number;
115
+ /** What the renderer said about GPU timing: `unavailable` without timestamp queries. */
116
+ gpu: {
117
+ status: 'unknown' | 'available' | 'unavailable';
118
+ quantized: boolean;
119
+ };
120
+ private windowSize;
121
+ private series;
122
+ private readonly stackIds;
123
+ private readonly stackStarts;
124
+ private depth;
125
+ private frameStartAt;
126
+ private warned;
127
+ private readonly asyncIds;
128
+ private readonly asyncKeys;
129
+ private readonly asyncStarts;
130
+ private asyncCount;
131
+ private readonly frameSpan;
132
+ constructor(options?: ProfilerOptions | number);
133
+ get window(): number;
134
+ get enabled(): boolean;
135
+ /** Starts a span on the main track. Returns a token for `end`; -1 when the profiler is off. */
136
+ begin(span: SpanDef): number;
137
+ /** Ends the span `begin` returned `token` for, recording it. */
138
+ end(token: number): void;
139
+ /** Drops the span `token` and everything begun inside it, recording nothing (an exception unwound it). */
140
+ cancel(token: number): void;
141
+ /**
142
+ * Time measured elsewhere (a GPU pass, a worker job): its aggregate, and a timeline event on
143
+ * `track` that ended now, or started at `start`. `frame`: the frame it belongs to, when it lands
144
+ * later (GPU timings).
145
+ */
146
+ record(span: SpanDef | string, ms: number, track?: number, start?: number, frame?: number): void;
147
+ /** Only the aggregate (`render/<node>` sums its views into one sample a frame). */
148
+ sample(span: SpanDef | string, ms: number): void;
149
+ /** Only the timeline event, when a capture runs. */
150
+ event(span: SpanDef | string, track: number, start: number, ms: number, frame?: number): void;
151
+ /** Whether a capture is listening (callers skip work that only feeds the timeline). */
152
+ get capturing(): boolean;
153
+ /**
154
+ * Work that starts in one frame and ends in another (an asset load, a page fetch): an async span,
155
+ * matched to its `endAsync` by span and key.
156
+ */
157
+ beginAsync(span: SpanDef, key: number | string): void;
158
+ endAsync(span: SpanDef, key: number | string): void;
159
+ /**
160
+ * A frame starts: spans left open from before are dropped (and reported once), and the `frame`
161
+ * span begins. Returns its token for `endFrame`.
162
+ */
163
+ beginFrame(frame: number): number;
164
+ endFrame(token: number): void;
16
165
  timing(name: string): SystemTiming | undefined;
166
+ /** A span's samples over the window, in ring order (not by age); undefined if it never ran. */
167
+ samples(name: string): Float64Array | undefined;
168
+ /** Every span that ran, by name. */
17
169
  all(): Record<string, SystemTiming>;
170
+ /**
171
+ * How many samples a span has had since the profiler started (or its aggregates were reset), by
172
+ * id: 0 if none. Allocates nothing, for per-frame readers such as feature budgets (0075).
173
+ */
174
+ runsOf(id: number): number;
175
+ /**
176
+ * The span that was open around a span's latest sample (its parent on the main track), by id, or
177
+ * -1: what lets aggregates count time two covering keys share once (0075's slices).
178
+ */
179
+ parentOf(id: number): number;
180
+ /** A span's latest sample in ms, by id, or 0 if it has none. Allocates nothing. */
181
+ lastOf(id: number): number;
182
+ /** Names of every span with samples. */
183
+ names(): string[];
184
+ /** Forgets every aggregate (`perf.reset`). */
185
+ reset(): void;
186
+ private push;
187
+ private createSeries;
188
+ private resize;
189
+ /** Reports a problem once per span name. */
190
+ private warn;
18
191
  }
192
+ /** A span's aggregates with its p50, p95 and track, or undefined if it never ran. */
193
+ export declare function spanStats(profiler: Profiler, name: string): SpanAggregate | undefined;
19
194
  export declare const ProfilerResource: import("..").ResourceDef<Profiler>;