@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.
@@ -1,401 +0,0 @@
1
- /**
2
- * JsTopController — singleton orchestrating the task-accounting engine.
3
- *
4
- * Lifecycle mirrors PerfMonitorController: the engine runs only while
5
- * someone is looking (snapshot subscribers — the HUD/modal — or a desktop
6
- * dashboard's silent remote sampling). Patches install once on first start
7
- * and stay installed; stopping just clears the `active` gate and the probe,
8
- * so a closed tool costs one boolean per schedule and nothing else.
9
- */
10
-
11
- import { AppState } from "react-native";
12
-
13
- import {
14
- BUCKET_MS,
15
- WINDOW_BUCKETS,
16
- SNAPSHOT_ROWS,
17
- type JsTopOriginDetail,
18
- type JsTopSnapshot,
19
- type JsTopStatus,
20
- type JsTopTiers,
21
- } from "../types";
22
- import { now } from "./now";
23
- import { setActive, isActive } from "./state";
24
- import { installTimerPatches } from "./patches/timers";
25
- import { installMicrotaskPatches } from "./patches/microtasks";
26
- import {
27
- busyHistory,
28
- resetAggregator,
29
- windowAttributedMs,
30
- windowBusyMs,
31
- windowResidualBuckets,
32
- } from "./aggregator";
33
- import {
34
- buildRows,
35
- getOriginDetailBase,
36
- resetOrigins,
37
- } from "./originRegistry";
38
- import { startBusyProbe, stopBusyProbe, suppressNextGap } from "./busyProbe";
39
- import { getOriginals } from "./state";
40
- import { loadJsTopPrefs, saveJsTopPrefs } from "../utils/preferences";
41
-
42
- declare const __DEV__: boolean;
43
-
44
- const NOTIFY_INTERVAL_MS = BUCKET_MS;
45
-
46
- type SnapshotSubscriber = (snapshot: JsTopSnapshot) => void;
47
- type EnabledSubscriber = (enabled: boolean) => void;
48
- type FreezeStatsFn = (now: number) => {
49
- count: number;
50
- worstMs: number;
51
- byOrigin: Record<string, { count: number; maxMs: number }>;
52
- };
53
-
54
- class JsTopControllerImpl {
55
- private enabled = false;
56
- private enabledLoaded = false;
57
- private paused = false;
58
- private remoteSampling = false;
59
- private engineRunning = false;
60
- private tiers: JsTopTiers = {
61
- timers: false,
62
- promises: false,
63
- bridge: false,
64
- longtask: false,
65
- };
66
- private notifyHandle: ReturnType<typeof setInterval> | null = null;
67
- private appStateSub: { remove(): void } | null = null;
68
- private snapshot: JsTopSnapshot = this.buildSnapshot();
69
- private subscribers = new Set<SnapshotSubscriber>();
70
- private enabledSubscribers = new Set<EnabledSubscriber>();
71
- /** Phase-2 seam: extra tier installers (promises/bridge/longtasks) register
72
- * here so the controller stays ignorant of their internals. */
73
- private tierInstallers: Array<() => Partial<JsTopTiers>> = [];
74
- private longtaskProvider: (() => JsTopSnapshot["longtasks"]) | null = null;
75
- private freezeStatsProvider: FreezeStatsFn | null = null;
76
- private clearHooks: Array<() => void> = [];
77
-
78
- constructor() {
79
- void loadJsTopPrefs().then((prefs) => {
80
- this.enabledLoaded = true;
81
- if (prefs.enabled && !this.enabled) this.setEnabled(true);
82
- });
83
- }
84
-
85
- // ── observation lifecycle ────────────────────────────────────────────────
86
-
87
- subscribe(fn: SnapshotSubscriber): () => void {
88
- this.subscribers.add(fn);
89
- this.syncEngineState();
90
- fn(this.snapshot);
91
- return () => {
92
- this.subscribers.delete(fn);
93
- this.syncEngineState();
94
- };
95
- }
96
-
97
- subscribeEnabled(fn: EnabledSubscriber): () => void {
98
- this.enabledSubscribers.add(fn);
99
- fn(this.enabled);
100
- return () => {
101
- this.enabledSubscribers.delete(fn);
102
- };
103
- }
104
-
105
- /** HUD visibility toggle (persisted). Not required for the engine to run —
106
- * the modal or a desktop watcher subscribing is what starts sampling. */
107
- setEnabled(on: boolean): void {
108
- if (this.enabled === on) return;
109
- this.enabled = on;
110
- if (this.enabledLoaded) void saveJsTopPrefs({ enabled: on });
111
- this.enabledSubscribers.forEach((fn) => {
112
- try {
113
- fn(on);
114
- } catch (err) {
115
- if (__DEV__) {
116
- // eslint-disable-next-line no-console
117
- console.error("[js-top] enabled subscriber threw", err);
118
- }
119
- }
120
- });
121
- this.syncEngineState();
122
- }
123
-
124
- isEnabled(): boolean {
125
- return this.enabled;
126
- }
127
-
128
- /** Silent sampling for a desktop viewer — engine runs, device HUD stays
129
- * hidden (same contract as perf-monitor's setRemoteSampling). */
130
- setRemoteSampling(on: boolean): void {
131
- if (this.remoteSampling === on) return;
132
- this.remoteSampling = on;
133
- this.syncEngineState();
134
- }
135
-
136
- isRemoteSampling(): boolean {
137
- return this.remoteSampling;
138
- }
139
-
140
- getStatus(): JsTopStatus {
141
- if (!this.engineRunning) return "disabled";
142
- return this.paused ? "paused" : "running";
143
- }
144
-
145
- // ── controls ─────────────────────────────────────────────────────────────
146
-
147
- pause(): void {
148
- if (this.paused) return;
149
- this.paused = true;
150
- setActive(false);
151
- stopBusyProbe();
152
- this.emitSnapshot();
153
- }
154
-
155
- resume(): void {
156
- if (!this.paused) return;
157
- this.paused = false;
158
- if (this.engineRunning) {
159
- setActive(true);
160
- startBusyProbe();
161
- }
162
- this.emitSnapshot();
163
- }
164
-
165
- clear(): void {
166
- resetOrigins();
167
- resetAggregator();
168
- for (const hook of this.clearHooks) {
169
- try {
170
- hook();
171
- } catch {
172
- // ignore
173
- }
174
- }
175
- this.emitSnapshot();
176
- }
177
-
178
- registerClearHook(fn: () => void): void {
179
- this.clearHooks.push(fn);
180
- }
181
-
182
- getSnapshot(): JsTopSnapshot {
183
- return this.snapshot;
184
- }
185
-
186
- /**
187
- * Full detail for one origin (tap-through view). Returns null when the
188
- * origin was evicted or never existed. The pinned "unattributed" row gets
189
- * a synthetic detail built from the probe residuals.
190
- */
191
- getOriginDetail(key: string): JsTopOriginDetail | null {
192
- const t = now();
193
- const busy = windowBusyMs(t);
194
- const freezeStats = this.freezeStatsProvider?.(Date.now());
195
- const freezes = freezeStats?.byOrigin[key];
196
- if (key === "unattributed") {
197
- const buckets = windowResidualBuckets(t);
198
- const windowMs =
199
- Math.round(buckets.reduce((sum, ms) => sum + ms, 0) * 10) / 10;
200
- return {
201
- key,
202
- label: "unattributed (events / React / native call-ins)",
203
- api: "unattributed",
204
- promoted: false,
205
- scheduleCount: 0,
206
- calls: 0,
207
- totalMs: 0,
208
- avgMs: 0,
209
- maxMs: 0,
210
- windowMs,
211
- pctOfBusy:
212
- busy > 0
213
- ? Math.min(100, Math.round((windowMs / busy) * 1000) / 10)
214
- : 0,
215
- lastSeenAgoMs: 0,
216
- buckets,
217
- freezes: freezes?.count ?? 0,
218
- worstFreezeMs: Math.round(freezes?.maxMs ?? 0),
219
- };
220
- }
221
- const base = getOriginDetailBase(key, t, busy);
222
- if (!base) return null;
223
- return {
224
- ...base,
225
- freezes: freezes?.count ?? 0,
226
- worstFreezeMs: Math.round(freezes?.maxMs ?? 0),
227
- };
228
- }
229
-
230
- /** Phase-2 registration seam for additional patch tiers. */
231
- registerTierInstaller(install: () => Partial<JsTopTiers>): void {
232
- this.tierInstallers.push(install);
233
- if (this.engineRunning) {
234
- Object.assign(this.tiers, this.runInstaller(install));
235
- }
236
- }
237
-
238
- registerLongtaskProvider(fn: () => JsTopSnapshot["longtasks"]): void {
239
- this.longtaskProvider = fn;
240
- }
241
-
242
- registerFreezeStatsProvider(fn: FreezeStatsFn): void {
243
- this.freezeStatsProvider = fn;
244
- }
245
-
246
- // ── engine ───────────────────────────────────────────────────────────────
247
-
248
- private shouldRun(): boolean {
249
- return this.subscribers.size > 0 || this.remoteSampling;
250
- }
251
-
252
- private syncEngineState(): void {
253
- const want = this.shouldRun();
254
- if (want && !this.engineRunning) this.startEngine();
255
- else if (!want && this.engineRunning) this.stopEngine();
256
- }
257
-
258
- private runInstaller(install: () => Partial<JsTopTiers>): Partial<JsTopTiers> {
259
- try {
260
- return install();
261
- } catch (err) {
262
- if (__DEV__) {
263
- // eslint-disable-next-line no-console
264
- console.error("[js-top] tier installer threw", err);
265
- }
266
- return {};
267
- }
268
- }
269
-
270
- private startEngine(): void {
271
- this.engineRunning = true;
272
- installTimerPatches();
273
- installMicrotaskPatches();
274
- this.tiers = {
275
- timers: true,
276
- promises: false,
277
- bridge: false,
278
- longtask: false,
279
- };
280
- for (const install of this.tierInstallers) {
281
- Object.assign(this.tiers, this.runInstaller(install));
282
- }
283
- setActive(!this.paused);
284
- if (!this.paused) startBusyProbe();
285
- try {
286
- this.appStateSub = AppState.addEventListener("change", () => {
287
- suppressNextGap();
288
- });
289
- } catch {
290
- this.appStateSub = null;
291
- }
292
- // The notify tick uses the pre-patch setInterval: the engine must never
293
- // list its own heartbeat as a task.
294
- this.notifyHandle = getOriginals().setInterval(
295
- () => this.emitSnapshot(),
296
- NOTIFY_INTERVAL_MS,
297
- );
298
- this.emitSnapshot();
299
- }
300
-
301
- private stopEngine(): void {
302
- this.engineRunning = false;
303
- setActive(false);
304
- stopBusyProbe();
305
- if (this.notifyHandle != null) {
306
- getOriginals().clearInterval(this.notifyHandle);
307
- this.notifyHandle = null;
308
- }
309
- if (this.appStateSub) {
310
- try {
311
- this.appStateSub.remove();
312
- } catch {
313
- // ignore
314
- }
315
- this.appStateSub = null;
316
- }
317
- }
318
-
319
- private buildSnapshot(): JsTopSnapshot {
320
- const t = now();
321
- const busy = windowBusyMs(t);
322
- const attributed = windowAttributedMs(t);
323
- const unattributed = Math.max(0, busy - attributed);
324
- const rows = buildRows(t, busy, SNAPSHOT_ROWS);
325
- if (unattributed >= 1) {
326
- rows.push({
327
- key: "unattributed",
328
- label: "unattributed (events / React / native call-ins)",
329
- api: "unattributed",
330
- windowMs: Math.round(unattributed * 10) / 10,
331
- pctOfBusy:
332
- busy > 0
333
- ? Math.min(100, Math.round((unattributed / busy) * 1000) / 10)
334
- : 0,
335
- totalMs: 0,
336
- calls: 0,
337
- avgMs: 0,
338
- maxMs: 0,
339
- lastSeenAgoMs: 0,
340
- freezes: 0,
341
- worstFreezeMs: 0,
342
- });
343
- rows.sort((a, b) => b.windowMs - a.windowMs || b.totalMs - a.totalMs);
344
- }
345
- // Decorate rows with freeze badges (unattributed freezes land on the
346
- // pinned "unattributed" row, whose key matches SYSTEM_FREEZE_KEY).
347
- const freezeStats = this.freezeStatsProvider?.(Date.now()) ?? {
348
- count: 0,
349
- worstMs: 0,
350
- byOrigin: {},
351
- };
352
- for (const row of rows) {
353
- const s = freezeStats.byOrigin[row.key];
354
- if (s) {
355
- row.freezes = s.count;
356
- row.worstFreezeMs = Math.round(s.maxMs);
357
- }
358
- }
359
- const windowSpanMs = WINDOW_BUCKETS * BUCKET_MS;
360
- return {
361
- ts: Date.now(),
362
- paused: this.paused,
363
- busyPct: Math.min(100, Math.round((busy / windowSpanMs) * 1000) / 10),
364
- busyHistory: busyHistory(t),
365
- tiers: { ...this.tiers },
366
- rows,
367
- totals: {
368
- attributedWindowMs: Math.round(attributed * 10) / 10,
369
- unattributedWindowMs: Math.round(unattributed * 10) / 10,
370
- windowMs: windowSpanMs,
371
- },
372
- freezeSummary: {
373
- count: freezeStats.count,
374
- worstMs: Math.round(freezeStats.worstMs),
375
- },
376
- longtasks: this.longtaskProvider ? this.longtaskProvider() : [],
377
- };
378
- }
379
-
380
- private emitSnapshot(): void {
381
- this.snapshot = this.buildSnapshot();
382
- this.subscribers.forEach((fn) => {
383
- try {
384
- fn(this.snapshot);
385
- } catch (err) {
386
- if (__DEV__) {
387
- // eslint-disable-next-line no-console
388
- console.error("[js-top] snapshot subscriber threw", err);
389
- }
390
- }
391
- });
392
- }
393
-
394
- /** Engine active state, for diagnostics/tests. */
395
- isEngineActive(): boolean {
396
- return isActive();
397
- }
398
- }
399
-
400
- export const JsTopController = new JsTopControllerImpl();
401
- export type { JsTopControllerImpl };