@buoy-gg/js-top 7.0.29 → 7.0.30
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/package.json +5 -6
- package/src/index.tsx +0 -69
- package/src/js-top/components/BusySparkline.tsx +0 -59
- package/src/js-top/components/JsTopHudOverlay.tsx +0 -78
- package/src/js-top/components/JsTopModal.tsx +0 -426
- package/src/js-top/components/TaskDetailView.tsx +0 -350
- package/src/js-top/components/TaskTable.tsx +0 -365
- package/src/js-top/engine/JsTopController.ts +0 -401
- package/src/js-top/engine/__tests__/engine.test.ts +0 -337
- package/src/js-top/engine/aggregator.ts +0 -148
- package/src/js-top/engine/busyProbe.ts +0 -100
- package/src/js-top/engine/frameStack.ts +0 -142
- package/src/js-top/engine/longtasks.ts +0 -137
- package/src/js-top/engine/now.ts +0 -33
- package/src/js-top/engine/originRegistry.ts +0 -495
- package/src/js-top/engine/patches/bridge.ts +0 -69
- package/src/js-top/engine/patches/microtasks.ts +0 -25
- package/src/js-top/engine/patches/promises.ts +0 -57
- package/src/js-top/engine/patches/timers.ts +0 -80
- package/src/js-top/engine/state.ts +0 -66
- package/src/js-top/engine/symbolicate.ts +0 -109
- package/src/js-top/sync/jsTopSyncAdapter.ts +0 -72
- package/src/js-top/types/index.ts +0 -144
- package/src/js-top/utils/preferences.ts +0 -35
- package/src/preset.tsx +0 -67
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the js-top engine's pure accounting core.
|
|
3
|
-
*
|
|
4
|
-
* Same self-contained harness pattern as perf-monitor's __tests__: the repo
|
|
5
|
-
* has no Jest, so run with `npx tsx <path>`; `__tests__` is excluded from
|
|
6
|
-
* the publish build.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
bookBusyRange,
|
|
11
|
-
bookAttributedRange,
|
|
12
|
-
resetAggregator,
|
|
13
|
-
windowBusyMs,
|
|
14
|
-
windowAttributedMs,
|
|
15
|
-
busyHistory,
|
|
16
|
-
} from "../aggregator";
|
|
17
|
-
import {
|
|
18
|
-
buoyJsTopScheduleOrigin,
|
|
19
|
-
buoyJsTopBookExclusive,
|
|
20
|
-
buildRows,
|
|
21
|
-
debugGetOrigin,
|
|
22
|
-
originCount,
|
|
23
|
-
parseStack,
|
|
24
|
-
resetOrigins,
|
|
25
|
-
OTHER_KEY,
|
|
26
|
-
} from "../originRegistry";
|
|
27
|
-
import { buoyJsTopWrapTask, buoyJsTopRunAs, debugDepth } from "../frameStack";
|
|
28
|
-
import { setNowForTests } from "../now";
|
|
29
|
-
import { setActive } from "../state";
|
|
30
|
-
import {
|
|
31
|
-
BUCKET_MS,
|
|
32
|
-
MAX_ORIGINS,
|
|
33
|
-
PROMOTE_MIN_CALLS,
|
|
34
|
-
WINDOW_BUCKETS,
|
|
35
|
-
} from "../../types";
|
|
36
|
-
|
|
37
|
-
// ----------------------------------------------------------------------------
|
|
38
|
-
// Tiny harness
|
|
39
|
-
// ----------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
type TestFn = () => void | Promise<void>;
|
|
42
|
-
interface TestCase {
|
|
43
|
-
name: string;
|
|
44
|
-
fn: TestFn;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const cases: TestCase[] = [];
|
|
48
|
-
let currentSuite = "";
|
|
49
|
-
|
|
50
|
-
function describe(name: string, body: () => void) {
|
|
51
|
-
const prev = currentSuite;
|
|
52
|
-
currentSuite = prev ? `${prev} › ${name}` : name;
|
|
53
|
-
body();
|
|
54
|
-
currentSuite = prev;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function it(name: string, fn: TestFn) {
|
|
58
|
-
cases.push({ name: `${currentSuite} › ${name}`, fn });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function expectEq(actual: unknown, expected: unknown, label = "") {
|
|
62
|
-
const a = JSON.stringify(actual);
|
|
63
|
-
const e = JSON.stringify(expected);
|
|
64
|
-
if (a !== e) {
|
|
65
|
-
throw new Error(`${label || "expectEq"}: expected ${e}, got ${a}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function expectClose(actual: number, expected: number, tol = 0.01, label = "") {
|
|
70
|
-
if (Math.abs(actual - expected) > tol) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`${label || "expectClose"}: expected ~${expected}, got ${actual}`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function expectTrue(cond: boolean, label = "") {
|
|
78
|
-
if (!cond) throw new Error(`${label || "expectTrue"}: condition false`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ----------------------------------------------------------------------------
|
|
82
|
-
// Controllable clock
|
|
83
|
-
// ----------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
let fakeNow = 0;
|
|
86
|
-
function advance(ms: number) {
|
|
87
|
-
fakeNow += ms;
|
|
88
|
-
}
|
|
89
|
-
function resetAll(startAt = 1_000_000) {
|
|
90
|
-
fakeNow = startAt;
|
|
91
|
-
setNowForTests(() => fakeNow);
|
|
92
|
-
resetOrigins();
|
|
93
|
-
resetAggregator();
|
|
94
|
-
setActive(true);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ----------------------------------------------------------------------------
|
|
98
|
-
// Aggregator
|
|
99
|
-
// ----------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
describe("aggregator", () => {
|
|
102
|
-
it("distributes a span across buckets and caps each at BUCKET_MS", () => {
|
|
103
|
-
resetAll();
|
|
104
|
-
const t0 = fakeNow;
|
|
105
|
-
// A 1000ms fully-busy span covers 4 buckets of 250 each.
|
|
106
|
-
bookBusyRange(t0, t0 + 1000);
|
|
107
|
-
expectClose(windowBusyMs(t0 + 1000), 1000, 1, "busy sum");
|
|
108
|
-
// Booking the same span again must not exceed the per-bucket cap.
|
|
109
|
-
bookBusyRange(t0, t0 + 1000);
|
|
110
|
-
expectClose(windowBusyMs(t0 + 1000), 1000, 1, "capped busy sum");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("windows slide: old buckets fall out after WINDOW_BUCKETS", () => {
|
|
114
|
-
resetAll();
|
|
115
|
-
const t0 = fakeNow;
|
|
116
|
-
bookBusyRange(t0, t0 + 100);
|
|
117
|
-
const later = t0 + (WINDOW_BUCKETS + 2) * BUCKET_MS;
|
|
118
|
-
expectClose(windowBusyMs(later), 0, 0.001, "expired window");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("attributed range books exclusive ms proportionally", () => {
|
|
122
|
-
resetAll();
|
|
123
|
-
const t0 = fakeNow;
|
|
124
|
-
// 500ms span but only 100ms exclusive → 100 attributed total.
|
|
125
|
-
bookAttributedRange(t0, t0 + 500, 100);
|
|
126
|
-
expectClose(windowAttributedMs(t0 + 500), 100, 1, "attributed sum");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("clamps pathological multi-minute spans to the history ring", () => {
|
|
130
|
-
resetAll();
|
|
131
|
-
const t0 = fakeNow;
|
|
132
|
-
bookBusyRange(t0 - 10 * 60 * 1000, t0);
|
|
133
|
-
// Booked at most one ring's worth; window sum is fully busy.
|
|
134
|
-
expectClose(
|
|
135
|
-
windowBusyMs(t0),
|
|
136
|
-
WINDOW_BUCKETS * BUCKET_MS,
|
|
137
|
-
BUCKET_MS,
|
|
138
|
-
"stall backfill",
|
|
139
|
-
);
|
|
140
|
-
expectTrue(busyHistory(t0).length > 0, "history present");
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// ----------------------------------------------------------------------------
|
|
145
|
-
// Origin registry
|
|
146
|
-
// ----------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
describe("originRegistry", () => {
|
|
149
|
-
it("cheap keys aggregate by api + fn name", () => {
|
|
150
|
-
resetAll();
|
|
151
|
-
function pollFeed() {}
|
|
152
|
-
const k1 = buoyJsTopScheduleOrigin("interval", pollFeed, fakeNow);
|
|
153
|
-
const k2 = buoyJsTopScheduleOrigin("interval", pollFeed, fakeNow);
|
|
154
|
-
expectEq(k1, k2, "same cheap key");
|
|
155
|
-
expectEq(k1, "interval|pollFeed", "key shape");
|
|
156
|
-
expectEq(debugGetOrigin(k1)?.scheduleCount, 2, "schedule count");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("promotes after PROMOTE_MIN_CALLS schedules", () => {
|
|
160
|
-
resetAll();
|
|
161
|
-
function hot() {}
|
|
162
|
-
let key = "";
|
|
163
|
-
for (let i = 0; i < PROMOTE_MIN_CALLS + 1; i++) {
|
|
164
|
-
key = buoyJsTopScheduleOrigin("timeout", hot, fakeNow);
|
|
165
|
-
advance(10);
|
|
166
|
-
}
|
|
167
|
-
// Cheap origin is promoted; refined key may or may not resolve under
|
|
168
|
-
// tsx/node stack formats, but the cheap origin must be flagged.
|
|
169
|
-
const cheap = debugGetOrigin("timeout|hot");
|
|
170
|
-
expectTrue(cheap?.promoted === true, "promoted flag");
|
|
171
|
-
expectTrue(key.length > 0, "key resolves");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("books exclusive time into calls/totals/max and the ring", () => {
|
|
175
|
-
resetAll();
|
|
176
|
-
function job() {}
|
|
177
|
-
const key = buoyJsTopScheduleOrigin("timeout", job, fakeNow);
|
|
178
|
-
buoyJsTopBookExclusive(key, 40, fakeNow, fakeNow + 50);
|
|
179
|
-
buoyJsTopBookExclusive(key, 10, fakeNow + 60, fakeNow + 70);
|
|
180
|
-
const o = debugGetOrigin(key);
|
|
181
|
-
expectEq(o?.calls, 2, "calls");
|
|
182
|
-
expectClose(o?.totalMs ?? 0, 50, 0.01, "totalMs");
|
|
183
|
-
expectClose(o?.maxMs ?? 0, 40, 0.01, "maxMs");
|
|
184
|
-
const rows = buildRows(fakeNow + 100, 1000, 10);
|
|
185
|
-
expectEq(rows[0]?.key, key, "row present");
|
|
186
|
-
expectClose(rows[0]?.windowMs ?? 0, 50, 1, "row windowMs");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("evicts the coldest origin into the pinned other row at MAX_ORIGINS", () => {
|
|
190
|
-
resetAll();
|
|
191
|
-
// Create a cold origin with booked history, then flood the registry.
|
|
192
|
-
function coldFn() {}
|
|
193
|
-
const coldKey = buoyJsTopScheduleOrigin("timeout", coldFn, fakeNow);
|
|
194
|
-
buoyJsTopBookExclusive(coldKey, 5, fakeNow, fakeNow + 5);
|
|
195
|
-
advance(WINDOW_BUCKETS * BUCKET_MS + 1000); // cold falls out of window
|
|
196
|
-
for (let i = 0; i < MAX_ORIGINS + 10; i++) {
|
|
197
|
-
// Distinct names → distinct cheap keys.
|
|
198
|
-
const fn = { name: `filler_${i}` };
|
|
199
|
-
const k = buoyJsTopScheduleOrigin("timeout", fn, fakeNow);
|
|
200
|
-
buoyJsTopBookExclusive(k, 1, fakeNow, fakeNow + 1);
|
|
201
|
-
advance(2);
|
|
202
|
-
}
|
|
203
|
-
expectTrue(originCount() <= MAX_ORIGINS + 1, "registry capped (+other)");
|
|
204
|
-
expectTrue(debugGetOrigin(coldKey) === undefined, "cold evicted");
|
|
205
|
-
const other = debugGetOrigin(OTHER_KEY);
|
|
206
|
-
expectTrue((other?.totalMs ?? 0) >= 5, "other absorbed totals");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("parses Hermes-style stacks and skips engine frames", () => {
|
|
210
|
-
const stack = [
|
|
211
|
-
"Error",
|
|
212
|
-
" at buoyJsTopScheduleOrigin (address at bundle.js:10:5)",
|
|
213
|
-
" at buoyJsTopSetTimeout (address at bundle.js:20:5)",
|
|
214
|
-
" at startPolling (address at http://localhost:8081/index.bundle?platform=ios:120233:22)",
|
|
215
|
-
" at anonymous (address at bundle.js:99:1)",
|
|
216
|
-
].join("\n");
|
|
217
|
-
const { caller, frames } = parseStack(stack);
|
|
218
|
-
expectEq(caller, "startPolling", "caller");
|
|
219
|
-
expectTrue(frames.length >= 1, "frames kept");
|
|
220
|
-
expectEq(frames[0]?.line, 120233, "top frame line");
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// ----------------------------------------------------------------------------
|
|
225
|
-
// Frame stack — exclusive-time math
|
|
226
|
-
// ----------------------------------------------------------------------------
|
|
227
|
-
|
|
228
|
-
describe("frameStack", () => {
|
|
229
|
-
it("books exclusive time for nested wrapped calls (no double count)", () => {
|
|
230
|
-
resetAll();
|
|
231
|
-
// Parent runs 100ms total; child (invoked synchronously inside, as a
|
|
232
|
-
// bridge call-in would invoke wrapped rAF callbacks) consumes 60ms.
|
|
233
|
-
const child = buoyJsTopWrapTask(function childWork() {
|
|
234
|
-
advance(60);
|
|
235
|
-
}, "raf");
|
|
236
|
-
buoyJsTopRunAs("bridge", "JSTimers.callTimers", () => {
|
|
237
|
-
advance(30);
|
|
238
|
-
child();
|
|
239
|
-
advance(10);
|
|
240
|
-
});
|
|
241
|
-
const childO = debugGetOrigin("raf|childWork");
|
|
242
|
-
const parentO = debugGetOrigin("bridge|JSTimers.callTimers");
|
|
243
|
-
expectClose(childO?.totalMs ?? 0, 60, 0.01, "child exclusive");
|
|
244
|
-
expectClose(parentO?.totalMs ?? 0, 40, 0.01, "parent exclusive");
|
|
245
|
-
// Union attributed globally = 100, not 160.
|
|
246
|
-
expectClose(windowAttributedMs(fakeNow), 100, 2, "attributed union");
|
|
247
|
-
expectEq(debugDepth(), 0, "stack drained");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("inactive engine wraps but books nothing; attribution starts on activation", () => {
|
|
251
|
-
resetAll();
|
|
252
|
-
setActive(false);
|
|
253
|
-
function pollFeedLoop() {
|
|
254
|
-
advance(30);
|
|
255
|
-
}
|
|
256
|
-
// Scheduled while off (the classic pre-open interval)…
|
|
257
|
-
const wrapped = buoyJsTopWrapTask(pollFeedLoop, "interval");
|
|
258
|
-
expectTrue(wrapped !== pollFeedLoop, "wrapper installed even while off");
|
|
259
|
-
wrapped();
|
|
260
|
-
expectTrue(
|
|
261
|
-
debugGetOrigin("interval|pollFeedLoop") === undefined,
|
|
262
|
-
"no booking while inactive",
|
|
263
|
-
);
|
|
264
|
-
// …attributes from the first fire after the engine turns on.
|
|
265
|
-
setActive(true);
|
|
266
|
-
wrapped();
|
|
267
|
-
expectClose(
|
|
268
|
-
debugGetOrigin("interval|pollFeedLoop")?.totalMs ?? 0,
|
|
269
|
-
30,
|
|
270
|
-
0.01,
|
|
271
|
-
"late-resolved attribution",
|
|
272
|
-
);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("survives clear(): live wrappers re-register their origin", () => {
|
|
276
|
-
resetAll();
|
|
277
|
-
function tickerWork() {
|
|
278
|
-
advance(10);
|
|
279
|
-
}
|
|
280
|
-
const wrapped = buoyJsTopWrapTask(tickerWork, "interval");
|
|
281
|
-
wrapped();
|
|
282
|
-
resetOrigins(); // what clear() does
|
|
283
|
-
wrapped();
|
|
284
|
-
expectClose(
|
|
285
|
-
debugGetOrigin("interval|tickerWork")?.totalMs ?? 0,
|
|
286
|
-
10,
|
|
287
|
-
0.01,
|
|
288
|
-
"re-registered after clear",
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("propagates errors while still booking the frame", () => {
|
|
293
|
-
resetAll();
|
|
294
|
-
const boom = buoyJsTopWrapTask(function boomTask() {
|
|
295
|
-
advance(25);
|
|
296
|
-
throw new Error("boom");
|
|
297
|
-
}, "timeout");
|
|
298
|
-
let threw = false;
|
|
299
|
-
try {
|
|
300
|
-
boom();
|
|
301
|
-
} catch (e) {
|
|
302
|
-
threw = (e as Error).message === "boom";
|
|
303
|
-
}
|
|
304
|
-
expectTrue(threw, "error propagated");
|
|
305
|
-
expectClose(
|
|
306
|
-
debugGetOrigin("timeout|boomTask")?.totalMs ?? 0,
|
|
307
|
-
25,
|
|
308
|
-
0.01,
|
|
309
|
-
"booked despite throw",
|
|
310
|
-
);
|
|
311
|
-
expectEq(debugDepth(), 0, "stack drained after throw");
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// ----------------------------------------------------------------------------
|
|
316
|
-
// Runner
|
|
317
|
-
// ----------------------------------------------------------------------------
|
|
318
|
-
|
|
319
|
-
async function main() {
|
|
320
|
-
let failed = 0;
|
|
321
|
-
for (const c of cases) {
|
|
322
|
-
try {
|
|
323
|
-
await c.fn();
|
|
324
|
-
// eslint-disable-next-line no-console
|
|
325
|
-
console.log(` ✓ ${c.name}`);
|
|
326
|
-
} catch (err) {
|
|
327
|
-
failed++;
|
|
328
|
-
// eslint-disable-next-line no-console
|
|
329
|
-
console.error(` ✗ ${c.name}\n ${(err as Error).message}`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// eslint-disable-next-line no-console
|
|
333
|
-
console.log(`\n${cases.length - failed}/${cases.length} passed`);
|
|
334
|
-
if (failed > 0) process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
void main();
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global per-bucket accounting: the engine's clock is divided into 250ms
|
|
3
|
-
* buckets (epochs). Each bucket tracks measured busy ms (from the probe) and
|
|
4
|
-
* attributed ms (from wrapped tasks); `unattributed = busy − attributed` per
|
|
5
|
-
* bucket is what keeps the table honest when work enters the thread through
|
|
6
|
-
* a path we can't wrap (bridgeless events, React commits, async/await).
|
|
7
|
-
*
|
|
8
|
-
* Rings are epoch-stamped parallel arrays: a slot is only valid when its
|
|
9
|
-
* stamp matches the epoch being read, so stale slots cost nothing to skip
|
|
10
|
-
* and nothing to zero eagerly.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { BUCKET_MS, HISTORY_BUCKETS, WINDOW_BUCKETS } from "../types";
|
|
14
|
-
|
|
15
|
-
const busyMs = new Float64Array(HISTORY_BUCKETS);
|
|
16
|
-
const attributedMs = new Float64Array(HISTORY_BUCKETS);
|
|
17
|
-
const longtaskMs = new Float64Array(HISTORY_BUCKETS);
|
|
18
|
-
const stamps = new Float64Array(HISTORY_BUCKETS).fill(-1);
|
|
19
|
-
|
|
20
|
-
export function epochOf(t: number): number {
|
|
21
|
-
return Math.floor(t / BUCKET_MS);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function slotFor(epoch: number): number {
|
|
25
|
-
const i = epoch % HISTORY_BUCKETS;
|
|
26
|
-
if (stamps[i] !== epoch) {
|
|
27
|
-
stamps[i] = epoch;
|
|
28
|
-
busyMs[i] = 0;
|
|
29
|
-
attributedMs[i] = 0;
|
|
30
|
-
longtaskMs[i] = 0;
|
|
31
|
-
}
|
|
32
|
-
return i;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Distribute a wall-clock span across the buckets it overlaps. Spans older
|
|
37
|
-
* than the history ring are clamped — a pathological hours-long gap books at
|
|
38
|
-
* most one ring's worth of buckets.
|
|
39
|
-
*/
|
|
40
|
-
function distribute(
|
|
41
|
-
start: number,
|
|
42
|
-
end: number,
|
|
43
|
-
add: (slot: number, ms: number) => void,
|
|
44
|
-
): void {
|
|
45
|
-
if (!(end > start)) return;
|
|
46
|
-
const clampedStart = Math.max(start, end - HISTORY_BUCKETS * BUCKET_MS);
|
|
47
|
-
let e = epochOf(clampedStart);
|
|
48
|
-
const lastEpoch = epochOf(end);
|
|
49
|
-
let cursor = clampedStart;
|
|
50
|
-
while (e <= lastEpoch) {
|
|
51
|
-
const bucketEnd = (e + 1) * BUCKET_MS;
|
|
52
|
-
const sliceEnd = Math.min(end, bucketEnd);
|
|
53
|
-
const ms = sliceEnd - cursor;
|
|
54
|
-
if (ms > 0) add(slotFor(e), ms);
|
|
55
|
-
cursor = sliceEnd;
|
|
56
|
-
e++;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Book probe-measured busy time over [start, end], capped per bucket. */
|
|
61
|
-
export function bookBusyRange(start: number, end: number): void {
|
|
62
|
-
distribute(start, end, (i, ms) => {
|
|
63
|
-
busyMs[i] = Math.min(BUCKET_MS, busyMs[i] + ms);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Book a task's exclusive ms, spread proportionally over its wall span.
|
|
69
|
-
* (Exclusive time isn't contiguous when children interleave; proportional
|
|
70
|
-
* spread is exact at 250ms-bucket granularity for all practical tasks.)
|
|
71
|
-
*/
|
|
72
|
-
export function bookAttributedRange(
|
|
73
|
-
start: number,
|
|
74
|
-
end: number,
|
|
75
|
-
exclusiveMs: number,
|
|
76
|
-
): void {
|
|
77
|
-
const span = end - start;
|
|
78
|
-
if (!(span > 0) || !(exclusiveMs > 0)) return;
|
|
79
|
-
const ratio = Math.min(1, exclusiveMs / span);
|
|
80
|
-
distribute(start, end, (i, ms) => {
|
|
81
|
-
attributedMs[i] = Math.min(BUCKET_MS, attributedMs[i] + ms * ratio);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Book a reported longtask span (for history display only). */
|
|
86
|
-
export function bookLongtaskRange(start: number, end: number): void {
|
|
87
|
-
distribute(start, end, (i, ms) => {
|
|
88
|
-
longtaskMs[i] = Math.min(BUCKET_MS, longtaskMs[i] + ms);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function sumWindow(arr: Float64Array, nowT: number, buckets: number): number {
|
|
93
|
-
const current = epochOf(nowT);
|
|
94
|
-
let sum = 0;
|
|
95
|
-
for (let e = current - buckets + 1; e <= current; e++) {
|
|
96
|
-
const i = e % HISTORY_BUCKETS;
|
|
97
|
-
if (i >= 0 && stamps[i] === e) sum += arr[i];
|
|
98
|
-
}
|
|
99
|
-
return sum;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function windowBusyMs(nowT: number): number {
|
|
103
|
-
return sumWindow(busyMs, nowT, WINDOW_BUCKETS);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function windowAttributedMs(nowT: number): number {
|
|
107
|
-
return sumWindow(attributedMs, nowT, WINDOW_BUCKETS);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Per-bucket UNATTRIBUTED ms (busy − attributed), oldest→newest over the
|
|
112
|
-
* rolling window — the "system" row's activity trend for the detail view.
|
|
113
|
-
*/
|
|
114
|
-
export function windowResidualBuckets(nowT: number): number[] {
|
|
115
|
-
const current = epochOf(nowT);
|
|
116
|
-
const out: number[] = [];
|
|
117
|
-
for (let e = current - WINDOW_BUCKETS + 1; e <= current; e++) {
|
|
118
|
-
const i = e % HISTORY_BUCKETS;
|
|
119
|
-
if (i >= 0 && stamps[i] === e) {
|
|
120
|
-
out.push(Math.max(0, Math.round((busyMs[i] - attributedMs[i]) * 10) / 10));
|
|
121
|
-
} else {
|
|
122
|
-
out.push(0);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Per-bucket busy%, oldest→newest, over the full 30s history ring. */
|
|
129
|
-
export function busyHistory(nowT: number): number[] {
|
|
130
|
-
const current = epochOf(nowT);
|
|
131
|
-
const out: number[] = [];
|
|
132
|
-
for (let e = current - HISTORY_BUCKETS + 1; e <= current; e++) {
|
|
133
|
-
const i = e % HISTORY_BUCKETS;
|
|
134
|
-
if (i >= 0 && stamps[i] === e) {
|
|
135
|
-
out.push(Math.round((busyMs[i] / BUCKET_MS) * 100));
|
|
136
|
-
} else {
|
|
137
|
-
out.push(0);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Trim the leading dead zone from before the engine started.
|
|
141
|
-
let firstLive = 0;
|
|
142
|
-
while (firstLive < out.length && out[firstLive] === 0) firstLive++;
|
|
143
|
-
return out.slice(Math.max(0, Math.min(firstLive, out.length - 1)));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function resetAggregator(): void {
|
|
147
|
-
stamps.fill(-1);
|
|
148
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Busy probe — measures true JS-thread occupancy from timer-gap inflation.
|
|
3
|
-
*
|
|
4
|
-
* A 16ms heartbeat (created with the PRE-PATCH setInterval so the probe never
|
|
5
|
-
* appears in its own table) can only fire when the thread yields; any gap
|
|
6
|
-
* beyond the calibrated idle baseline is time the thread was busy. This is
|
|
7
|
-
* the same physical signal as perf-monitor's stall-aware sampler, at higher
|
|
8
|
-
* frequency: multi-second stalls simply arrive as one huge gap and are booked
|
|
9
|
-
* as fully-busy buckets by bookBusyRange (the backfill falls out of the
|
|
10
|
-
* range-distribution math; no separate synthetic-sample pass needed).
|
|
11
|
-
*
|
|
12
|
-
* Calibration: the nominal 16ms is never what a real device delivers at
|
|
13
|
-
* idle. We track a rolling min gap (3 one-second slots) and subtract THAT,
|
|
14
|
-
* otherwise idle devices read 10–20% phantom busy.
|
|
15
|
-
*
|
|
16
|
-
* AppState transitions also pause JS timers, so the controller flags them
|
|
17
|
-
* via suppressNextGap() — same trick as PerfMonitorController; AppState
|
|
18
|
-
* events arrive on the JS thread, so a genuine stall can never set the flag.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { now } from "./now";
|
|
22
|
-
import { getOriginals } from "./state";
|
|
23
|
-
import { bookBusyRange } from "./aggregator";
|
|
24
|
-
|
|
25
|
-
const PROBE_INTERVAL_MS = 16;
|
|
26
|
-
/** Ignore gaps below this when calibrating (a busy tick isn't a baseline). */
|
|
27
|
-
const CALIBRATION_MAX_GAP_MS = 100;
|
|
28
|
-
const BASELINE_SLOTS = 3;
|
|
29
|
-
const MIN_BASELINE_MS = 8;
|
|
30
|
-
|
|
31
|
-
let handle: ReturnType<typeof setInterval> | null = null;
|
|
32
|
-
let lastAt = 0;
|
|
33
|
-
let suppressNext = false;
|
|
34
|
-
|
|
35
|
-
let slotMins: number[] = [];
|
|
36
|
-
let currentSlotMin = Infinity;
|
|
37
|
-
let currentSlotStart = 0;
|
|
38
|
-
let baseline = PROBE_INTERVAL_MS;
|
|
39
|
-
|
|
40
|
-
function buoyJsTopProbeTick(): void {
|
|
41
|
-
const t = now();
|
|
42
|
-
const gap = lastAt > 0 ? t - lastAt : 0;
|
|
43
|
-
lastAt = t;
|
|
44
|
-
if (suppressNext) {
|
|
45
|
-
// Backgrounding/foregrounding paused timers — not a stall. Reset
|
|
46
|
-
// calibration too: refresh-rate or thermal state may have changed.
|
|
47
|
-
suppressNext = false;
|
|
48
|
-
currentSlotMin = Infinity;
|
|
49
|
-
currentSlotStart = t;
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (gap <= 0) return;
|
|
53
|
-
|
|
54
|
-
if (gap < CALIBRATION_MAX_GAP_MS) {
|
|
55
|
-
if (gap < currentSlotMin) currentSlotMin = gap;
|
|
56
|
-
if (currentSlotStart === 0) currentSlotStart = t;
|
|
57
|
-
if (t - currentSlotStart >= 1000) {
|
|
58
|
-
slotMins.push(currentSlotMin);
|
|
59
|
-
if (slotMins.length > BASELINE_SLOTS) slotMins.shift();
|
|
60
|
-
baseline = Math.max(MIN_BASELINE_MS, Math.min(...slotMins));
|
|
61
|
-
currentSlotMin = Infinity;
|
|
62
|
-
currentSlotStart = t;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const busyExcess = gap - baseline;
|
|
67
|
-
if (busyExcess > 1) {
|
|
68
|
-
// The block ended when this tick finally fired — position it at the tail.
|
|
69
|
-
bookBusyRange(t - busyExcess, t);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function startBusyProbe(): void {
|
|
74
|
-
if (handle != null) return;
|
|
75
|
-
const originals = getOriginals();
|
|
76
|
-
lastAt = now();
|
|
77
|
-
suppressNext = false;
|
|
78
|
-
slotMins = [];
|
|
79
|
-
currentSlotMin = Infinity;
|
|
80
|
-
currentSlotStart = 0;
|
|
81
|
-
baseline = PROBE_INTERVAL_MS;
|
|
82
|
-
handle = originals.setInterval(buoyJsTopProbeTick, PROBE_INTERVAL_MS);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function stopBusyProbe(): void {
|
|
86
|
-
if (handle != null) {
|
|
87
|
-
getOriginals().clearInterval(handle);
|
|
88
|
-
handle = null;
|
|
89
|
-
}
|
|
90
|
-
lastAt = 0;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function suppressNextGap(): void {
|
|
94
|
-
suppressNext = true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Test-only. */
|
|
98
|
-
export function debugBaseline(): number {
|
|
99
|
-
return baseline;
|
|
100
|
-
}
|