@buoy-gg/js-top 7.0.40 → 7.0.42
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/README.md +12 -67
- package/lib/commonjs/index.js +1 -97
- package/lib/commonjs/js-top/components/BusySparkline.js +1 -56
- package/lib/commonjs/js-top/components/JsTopHudOverlay.js +1 -79
- package/lib/commonjs/js-top/components/JsTopModal.js +1 -349
- package/lib/commonjs/js-top/components/TaskDetailView.js +1 -330
- package/lib/commonjs/js-top/components/TaskTable.js +1 -360
- package/lib/commonjs/js-top/engine/JsTopController.js +1 -350
- package/lib/commonjs/js-top/engine/aggregator.js +1 -148
- package/lib/commonjs/js-top/engine/busyProbe.js +1 -101
- package/lib/commonjs/js-top/engine/frameStack.js +1 -133
- package/lib/commonjs/js-top/engine/longtasks.js +1 -141
- package/lib/commonjs/js-top/engine/now.js +1 -38
- package/lib/commonjs/js-top/engine/originRegistry.js +1 -402
- package/lib/commonjs/js-top/engine/patches/bridge.js +1 -60
- package/lib/commonjs/js-top/engine/patches/microtasks.js +1 -27
- package/lib/commonjs/js-top/engine/patches/promises.js +1 -65
- package/lib/commonjs/js-top/engine/patches/timers.js +1 -66
- package/lib/commonjs/js-top/engine/state.js +1 -63
- package/lib/commonjs/js-top/engine/symbolicate.js +1 -95
- package/lib/commonjs/js-top/sync/jsTopSyncAdapter.js +1 -74
- package/lib/commonjs/js-top/types/index.js +1 -46
- package/lib/commonjs/js-top/utils/preferences.js +1 -31
- package/lib/commonjs/preset.js +1 -75
- package/lib/module/index.js +1 -47
- package/lib/module/js-top/components/BusySparkline.js +1 -52
- package/lib/module/js-top/components/JsTopHudOverlay.js +1 -75
- package/lib/module/js-top/components/JsTopModal.js +1 -345
- package/lib/module/js-top/components/TaskDetailView.js +1 -326
- package/lib/module/js-top/components/TaskTable.js +1 -353
- package/lib/module/js-top/engine/JsTopController.js +1 -347
- package/lib/module/js-top/engine/aggregator.js +1 -136
- package/lib/module/js-top/engine/busyProbe.js +1 -94
- package/lib/module/js-top/engine/frameStack.js +1 -126
- package/lib/module/js-top/engine/longtasks.js +1 -133
- package/lib/module/js-top/engine/now.js +1 -33
- package/lib/module/js-top/engine/originRegistry.js +1 -388
- package/lib/module/js-top/engine/patches/bridge.js +1 -56
- package/lib/module/js-top/engine/patches/microtasks.js +1 -23
- package/lib/module/js-top/engine/patches/promises.js +1 -62
- package/lib/module/js-top/engine/patches/timers.js +1 -62
- package/lib/module/js-top/engine/state.js +1 -54
- package/lib/module/js-top/engine/symbolicate.js +1 -91
- package/lib/module/js-top/sync/jsTopSyncAdapter.js +1 -70
- package/lib/module/js-top/types/index.js +1 -42
- package/lib/module/js-top/utils/preferences.js +1 -26
- package/lib/module/preset.js +1 -71
- package/lib/web/index.mjs +2 -0
- package/package.json +39 -5
- package/lib/commonjs/index.js.map +0 -1
- package/lib/commonjs/js-top/components/BusySparkline.js.map +0 -1
- package/lib/commonjs/js-top/components/JsTopHudOverlay.js.map +0 -1
- package/lib/commonjs/js-top/components/JsTopModal.js.map +0 -1
- package/lib/commonjs/js-top/components/TaskDetailView.js.map +0 -1
- package/lib/commonjs/js-top/components/TaskTable.js.map +0 -1
- package/lib/commonjs/js-top/engine/JsTopController.js.map +0 -1
- package/lib/commonjs/js-top/engine/aggregator.js.map +0 -1
- package/lib/commonjs/js-top/engine/busyProbe.js.map +0 -1
- package/lib/commonjs/js-top/engine/frameStack.js.map +0 -1
- package/lib/commonjs/js-top/engine/longtasks.js.map +0 -1
- package/lib/commonjs/js-top/engine/now.js.map +0 -1
- package/lib/commonjs/js-top/engine/originRegistry.js.map +0 -1
- package/lib/commonjs/js-top/engine/patches/bridge.js.map +0 -1
- package/lib/commonjs/js-top/engine/patches/microtasks.js.map +0 -1
- package/lib/commonjs/js-top/engine/patches/promises.js.map +0 -1
- package/lib/commonjs/js-top/engine/patches/timers.js.map +0 -1
- package/lib/commonjs/js-top/engine/state.js.map +0 -1
- package/lib/commonjs/js-top/engine/symbolicate.js.map +0 -1
- package/lib/commonjs/js-top/sync/jsTopSyncAdapter.js.map +0 -1
- package/lib/commonjs/js-top/types/index.js.map +0 -1
- package/lib/commonjs/js-top/utils/preferences.js.map +0 -1
- package/lib/commonjs/preset.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/js-top/components/BusySparkline.js.map +0 -1
- package/lib/module/js-top/components/JsTopHudOverlay.js.map +0 -1
- package/lib/module/js-top/components/JsTopModal.js.map +0 -1
- package/lib/module/js-top/components/TaskDetailView.js.map +0 -1
- package/lib/module/js-top/components/TaskTable.js.map +0 -1
- package/lib/module/js-top/engine/JsTopController.js.map +0 -1
- package/lib/module/js-top/engine/aggregator.js.map +0 -1
- package/lib/module/js-top/engine/busyProbe.js.map +0 -1
- package/lib/module/js-top/engine/frameStack.js.map +0 -1
- package/lib/module/js-top/engine/longtasks.js.map +0 -1
- package/lib/module/js-top/engine/now.js.map +0 -1
- package/lib/module/js-top/engine/originRegistry.js.map +0 -1
- package/lib/module/js-top/engine/patches/bridge.js.map +0 -1
- package/lib/module/js-top/engine/patches/microtasks.js.map +0 -1
- package/lib/module/js-top/engine/patches/promises.js.map +0 -1
- package/lib/module/js-top/engine/patches/timers.js.map +0 -1
- package/lib/module/js-top/engine/state.js.map +0 -1
- package/lib/module/js-top/engine/symbolicate.js.map +0 -1
- package/lib/module/js-top/sync/jsTopSyncAdapter.js.map +0 -1
- package/lib/module/js-top/types/index.js.map +0 -1
- package/lib/module/js-top/utils/preferences.js.map +0 -1
- package/lib/module/preset.js.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/js-top/components/BusySparkline.d.ts.map +0 -1
- package/lib/typescript/js-top/components/JsTopHudOverlay.d.ts.map +0 -1
- package/lib/typescript/js-top/components/JsTopModal.d.ts.map +0 -1
- package/lib/typescript/js-top/components/TaskDetailView.d.ts.map +0 -1
- package/lib/typescript/js-top/components/TaskTable.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/JsTopController.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/aggregator.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/busyProbe.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/frameStack.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/longtasks.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/now.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/originRegistry.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/patches/bridge.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/patches/microtasks.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/patches/promises.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/patches/timers.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/state.d.ts.map +0 -1
- package/lib/typescript/js-top/engine/symbolicate.d.ts.map +0 -1
- package/lib/typescript/js-top/sync/jsTopSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/js-top/types/index.d.ts.map +0 -1
- package/lib/typescript/js-top/utils/preferences.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
|
@@ -1,347 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* JsTopController — singleton orchestrating the task-accounting engine.
|
|
5
|
-
*
|
|
6
|
-
* Lifecycle mirrors PerfMonitorController: the engine runs only while
|
|
7
|
-
* someone is looking (snapshot subscribers — the HUD/modal — or a desktop
|
|
8
|
-
* dashboard's silent remote sampling). Patches install once on first start
|
|
9
|
-
* and stay installed; stopping just clears the `active` gate and the probe,
|
|
10
|
-
* so a closed tool costs one boolean per schedule and nothing else.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { AppState } from "react-native";
|
|
14
|
-
import { BUCKET_MS, WINDOW_BUCKETS, SNAPSHOT_ROWS } from "../types";
|
|
15
|
-
import { now } from "./now";
|
|
16
|
-
import { setActive, isActive } from "./state";
|
|
17
|
-
import { installTimerPatches } from "./patches/timers";
|
|
18
|
-
import { installMicrotaskPatches } from "./patches/microtasks";
|
|
19
|
-
import { busyHistory, resetAggregator, windowAttributedMs, windowBusyMs, windowResidualBuckets } from "./aggregator";
|
|
20
|
-
import { buildRows, getOriginDetailBase, resetOrigins } from "./originRegistry";
|
|
21
|
-
import { startBusyProbe, stopBusyProbe, suppressNextGap } from "./busyProbe";
|
|
22
|
-
import { getOriginals } from "./state";
|
|
23
|
-
import { loadJsTopPrefs, saveJsTopPrefs } from "../utils/preferences";
|
|
24
|
-
const NOTIFY_INTERVAL_MS = BUCKET_MS;
|
|
25
|
-
class JsTopControllerImpl {
|
|
26
|
-
enabled = false;
|
|
27
|
-
enabledLoaded = false;
|
|
28
|
-
paused = false;
|
|
29
|
-
remoteSampling = false;
|
|
30
|
-
engineRunning = false;
|
|
31
|
-
tiers = {
|
|
32
|
-
timers: false,
|
|
33
|
-
promises: false,
|
|
34
|
-
bridge: false,
|
|
35
|
-
longtask: false
|
|
36
|
-
};
|
|
37
|
-
notifyHandle = null;
|
|
38
|
-
appStateSub = null;
|
|
39
|
-
snapshot = this.buildSnapshot();
|
|
40
|
-
subscribers = new Set();
|
|
41
|
-
enabledSubscribers = new Set();
|
|
42
|
-
/** Phase-2 seam: extra tier installers (promises/bridge/longtasks) register
|
|
43
|
-
* here so the controller stays ignorant of their internals. */
|
|
44
|
-
tierInstallers = [];
|
|
45
|
-
longtaskProvider = null;
|
|
46
|
-
freezeStatsProvider = null;
|
|
47
|
-
clearHooks = [];
|
|
48
|
-
constructor() {
|
|
49
|
-
void loadJsTopPrefs().then(prefs => {
|
|
50
|
-
this.enabledLoaded = true;
|
|
51
|
-
if (prefs.enabled && !this.enabled) this.setEnabled(true);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── observation lifecycle ────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
subscribe(fn) {
|
|
58
|
-
this.subscribers.add(fn);
|
|
59
|
-
this.syncEngineState();
|
|
60
|
-
fn(this.snapshot);
|
|
61
|
-
return () => {
|
|
62
|
-
this.subscribers.delete(fn);
|
|
63
|
-
this.syncEngineState();
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
subscribeEnabled(fn) {
|
|
67
|
-
this.enabledSubscribers.add(fn);
|
|
68
|
-
fn(this.enabled);
|
|
69
|
-
return () => {
|
|
70
|
-
this.enabledSubscribers.delete(fn);
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** HUD visibility toggle (persisted). Not required for the engine to run —
|
|
75
|
-
* the modal or a desktop watcher subscribing is what starts sampling. */
|
|
76
|
-
setEnabled(on) {
|
|
77
|
-
if (this.enabled === on) return;
|
|
78
|
-
this.enabled = on;
|
|
79
|
-
if (this.enabledLoaded) void saveJsTopPrefs({
|
|
80
|
-
enabled: on
|
|
81
|
-
});
|
|
82
|
-
this.enabledSubscribers.forEach(fn => {
|
|
83
|
-
try {
|
|
84
|
-
fn(on);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
if (__DEV__) {
|
|
87
|
-
// eslint-disable-next-line no-console
|
|
88
|
-
console.error("[js-top] enabled subscriber threw", err);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
this.syncEngineState();
|
|
93
|
-
}
|
|
94
|
-
isEnabled() {
|
|
95
|
-
return this.enabled;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Silent sampling for a desktop viewer — engine runs, device HUD stays
|
|
99
|
-
* hidden (same contract as perf-monitor's setRemoteSampling). */
|
|
100
|
-
setRemoteSampling(on) {
|
|
101
|
-
if (this.remoteSampling === on) return;
|
|
102
|
-
this.remoteSampling = on;
|
|
103
|
-
this.syncEngineState();
|
|
104
|
-
}
|
|
105
|
-
isRemoteSampling() {
|
|
106
|
-
return this.remoteSampling;
|
|
107
|
-
}
|
|
108
|
-
getStatus() {
|
|
109
|
-
if (!this.engineRunning) return "disabled";
|
|
110
|
-
return this.paused ? "paused" : "running";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── controls ─────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
pause() {
|
|
116
|
-
if (this.paused) return;
|
|
117
|
-
this.paused = true;
|
|
118
|
-
setActive(false);
|
|
119
|
-
stopBusyProbe();
|
|
120
|
-
this.emitSnapshot();
|
|
121
|
-
}
|
|
122
|
-
resume() {
|
|
123
|
-
if (!this.paused) return;
|
|
124
|
-
this.paused = false;
|
|
125
|
-
if (this.engineRunning) {
|
|
126
|
-
setActive(true);
|
|
127
|
-
startBusyProbe();
|
|
128
|
-
}
|
|
129
|
-
this.emitSnapshot();
|
|
130
|
-
}
|
|
131
|
-
clear() {
|
|
132
|
-
resetOrigins();
|
|
133
|
-
resetAggregator();
|
|
134
|
-
for (const hook of this.clearHooks) {
|
|
135
|
-
try {
|
|
136
|
-
hook();
|
|
137
|
-
} catch {
|
|
138
|
-
// ignore
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
this.emitSnapshot();
|
|
142
|
-
}
|
|
143
|
-
registerClearHook(fn) {
|
|
144
|
-
this.clearHooks.push(fn);
|
|
145
|
-
}
|
|
146
|
-
getSnapshot() {
|
|
147
|
-
return this.snapshot;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Full detail for one origin (tap-through view). Returns null when the
|
|
152
|
-
* origin was evicted or never existed. The pinned "unattributed" row gets
|
|
153
|
-
* a synthetic detail built from the probe residuals.
|
|
154
|
-
*/
|
|
155
|
-
getOriginDetail(key) {
|
|
156
|
-
const t = now();
|
|
157
|
-
const busy = windowBusyMs(t);
|
|
158
|
-
const freezeStats = this.freezeStatsProvider?.(Date.now());
|
|
159
|
-
const freezes = freezeStats?.byOrigin[key];
|
|
160
|
-
if (key === "unattributed") {
|
|
161
|
-
const buckets = windowResidualBuckets(t);
|
|
162
|
-
const windowMs = Math.round(buckets.reduce((sum, ms) => sum + ms, 0) * 10) / 10;
|
|
163
|
-
return {
|
|
164
|
-
key,
|
|
165
|
-
label: "unattributed (events / React / native call-ins)",
|
|
166
|
-
api: "unattributed",
|
|
167
|
-
promoted: false,
|
|
168
|
-
scheduleCount: 0,
|
|
169
|
-
calls: 0,
|
|
170
|
-
totalMs: 0,
|
|
171
|
-
avgMs: 0,
|
|
172
|
-
maxMs: 0,
|
|
173
|
-
windowMs,
|
|
174
|
-
pctOfBusy: busy > 0 ? Math.min(100, Math.round(windowMs / busy * 1000) / 10) : 0,
|
|
175
|
-
lastSeenAgoMs: 0,
|
|
176
|
-
buckets,
|
|
177
|
-
freezes: freezes?.count ?? 0,
|
|
178
|
-
worstFreezeMs: Math.round(freezes?.maxMs ?? 0)
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const base = getOriginDetailBase(key, t, busy);
|
|
182
|
-
if (!base) return null;
|
|
183
|
-
return {
|
|
184
|
-
...base,
|
|
185
|
-
freezes: freezes?.count ?? 0,
|
|
186
|
-
worstFreezeMs: Math.round(freezes?.maxMs ?? 0)
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Phase-2 registration seam for additional patch tiers. */
|
|
191
|
-
registerTierInstaller(install) {
|
|
192
|
-
this.tierInstallers.push(install);
|
|
193
|
-
if (this.engineRunning) {
|
|
194
|
-
Object.assign(this.tiers, this.runInstaller(install));
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
registerLongtaskProvider(fn) {
|
|
198
|
-
this.longtaskProvider = fn;
|
|
199
|
-
}
|
|
200
|
-
registerFreezeStatsProvider(fn) {
|
|
201
|
-
this.freezeStatsProvider = fn;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ── engine ───────────────────────────────────────────────────────────────
|
|
205
|
-
|
|
206
|
-
shouldRun() {
|
|
207
|
-
return this.subscribers.size > 0 || this.remoteSampling;
|
|
208
|
-
}
|
|
209
|
-
syncEngineState() {
|
|
210
|
-
const want = this.shouldRun();
|
|
211
|
-
if (want && !this.engineRunning) this.startEngine();else if (!want && this.engineRunning) this.stopEngine();
|
|
212
|
-
}
|
|
213
|
-
runInstaller(install) {
|
|
214
|
-
try {
|
|
215
|
-
return install();
|
|
216
|
-
} catch (err) {
|
|
217
|
-
if (__DEV__) {
|
|
218
|
-
// eslint-disable-next-line no-console
|
|
219
|
-
console.error("[js-top] tier installer threw", err);
|
|
220
|
-
}
|
|
221
|
-
return {};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
startEngine() {
|
|
225
|
-
this.engineRunning = true;
|
|
226
|
-
installTimerPatches();
|
|
227
|
-
installMicrotaskPatches();
|
|
228
|
-
this.tiers = {
|
|
229
|
-
timers: true,
|
|
230
|
-
promises: false,
|
|
231
|
-
bridge: false,
|
|
232
|
-
longtask: false
|
|
233
|
-
};
|
|
234
|
-
for (const install of this.tierInstallers) {
|
|
235
|
-
Object.assign(this.tiers, this.runInstaller(install));
|
|
236
|
-
}
|
|
237
|
-
setActive(!this.paused);
|
|
238
|
-
if (!this.paused) startBusyProbe();
|
|
239
|
-
try {
|
|
240
|
-
this.appStateSub = AppState.addEventListener("change", () => {
|
|
241
|
-
suppressNextGap();
|
|
242
|
-
});
|
|
243
|
-
} catch {
|
|
244
|
-
this.appStateSub = null;
|
|
245
|
-
}
|
|
246
|
-
// The notify tick uses the pre-patch setInterval: the engine must never
|
|
247
|
-
// list its own heartbeat as a task.
|
|
248
|
-
this.notifyHandle = getOriginals().setInterval(() => this.emitSnapshot(), NOTIFY_INTERVAL_MS);
|
|
249
|
-
this.emitSnapshot();
|
|
250
|
-
}
|
|
251
|
-
stopEngine() {
|
|
252
|
-
this.engineRunning = false;
|
|
253
|
-
setActive(false);
|
|
254
|
-
stopBusyProbe();
|
|
255
|
-
if (this.notifyHandle != null) {
|
|
256
|
-
getOriginals().clearInterval(this.notifyHandle);
|
|
257
|
-
this.notifyHandle = null;
|
|
258
|
-
}
|
|
259
|
-
if (this.appStateSub) {
|
|
260
|
-
try {
|
|
261
|
-
this.appStateSub.remove();
|
|
262
|
-
} catch {
|
|
263
|
-
// ignore
|
|
264
|
-
}
|
|
265
|
-
this.appStateSub = null;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
buildSnapshot() {
|
|
269
|
-
const t = now();
|
|
270
|
-
const busy = windowBusyMs(t);
|
|
271
|
-
const attributed = windowAttributedMs(t);
|
|
272
|
-
const unattributed = Math.max(0, busy - attributed);
|
|
273
|
-
const rows = buildRows(t, busy, SNAPSHOT_ROWS);
|
|
274
|
-
if (unattributed >= 1) {
|
|
275
|
-
rows.push({
|
|
276
|
-
key: "unattributed",
|
|
277
|
-
label: "unattributed (events / React / native call-ins)",
|
|
278
|
-
api: "unattributed",
|
|
279
|
-
windowMs: Math.round(unattributed * 10) / 10,
|
|
280
|
-
pctOfBusy: busy > 0 ? Math.min(100, Math.round(unattributed / busy * 1000) / 10) : 0,
|
|
281
|
-
totalMs: 0,
|
|
282
|
-
calls: 0,
|
|
283
|
-
avgMs: 0,
|
|
284
|
-
maxMs: 0,
|
|
285
|
-
lastSeenAgoMs: 0,
|
|
286
|
-
freezes: 0,
|
|
287
|
-
worstFreezeMs: 0
|
|
288
|
-
});
|
|
289
|
-
rows.sort((a, b) => b.windowMs - a.windowMs || b.totalMs - a.totalMs);
|
|
290
|
-
}
|
|
291
|
-
// Decorate rows with freeze badges (unattributed freezes land on the
|
|
292
|
-
// pinned "unattributed" row, whose key matches SYSTEM_FREEZE_KEY).
|
|
293
|
-
const freezeStats = this.freezeStatsProvider?.(Date.now()) ?? {
|
|
294
|
-
count: 0,
|
|
295
|
-
worstMs: 0,
|
|
296
|
-
byOrigin: {}
|
|
297
|
-
};
|
|
298
|
-
for (const row of rows) {
|
|
299
|
-
const s = freezeStats.byOrigin[row.key];
|
|
300
|
-
if (s) {
|
|
301
|
-
row.freezes = s.count;
|
|
302
|
-
row.worstFreezeMs = Math.round(s.maxMs);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const windowSpanMs = WINDOW_BUCKETS * BUCKET_MS;
|
|
306
|
-
return {
|
|
307
|
-
ts: Date.now(),
|
|
308
|
-
paused: this.paused,
|
|
309
|
-
busyPct: Math.min(100, Math.round(busy / windowSpanMs * 1000) / 10),
|
|
310
|
-
busyHistory: busyHistory(t),
|
|
311
|
-
tiers: {
|
|
312
|
-
...this.tiers
|
|
313
|
-
},
|
|
314
|
-
rows,
|
|
315
|
-
totals: {
|
|
316
|
-
attributedWindowMs: Math.round(attributed * 10) / 10,
|
|
317
|
-
unattributedWindowMs: Math.round(unattributed * 10) / 10,
|
|
318
|
-
windowMs: windowSpanMs
|
|
319
|
-
},
|
|
320
|
-
freezeSummary: {
|
|
321
|
-
count: freezeStats.count,
|
|
322
|
-
worstMs: Math.round(freezeStats.worstMs)
|
|
323
|
-
},
|
|
324
|
-
longtasks: this.longtaskProvider ? this.longtaskProvider() : []
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
emitSnapshot() {
|
|
328
|
-
this.snapshot = this.buildSnapshot();
|
|
329
|
-
this.subscribers.forEach(fn => {
|
|
330
|
-
try {
|
|
331
|
-
fn(this.snapshot);
|
|
332
|
-
} catch (err) {
|
|
333
|
-
if (__DEV__) {
|
|
334
|
-
// eslint-disable-next-line no-console
|
|
335
|
-
console.error("[js-top] snapshot subscriber threw", err);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** Engine active state, for diagnostics/tests. */
|
|
342
|
-
isEngineActive() {
|
|
343
|
-
return isActive();
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
export const JsTopController = new JsTopControllerImpl();
|
|
347
|
-
//# sourceMappingURL=JsTopController.js.map
|
|
1
|
+
"use strict";import{AppState as m}from"react-native";import{BUCKET_MS as h,WINDOW_BUCKETS as S,SNAPSHOT_ROWS as M}from"../types";import{now as d}from"./now";import{setActive as u,isActive as w}from"./state";import{installTimerPatches as y}from"./patches/timers";import{installMicrotaskPatches as v}from"./patches/microtasks";import{busyHistory as E,resetAggregator as z,windowAttributedMs as P,windowBusyMs as c,windowResidualBuckets as R}from"./aggregator";import{buildRows as _,getOriginDetailBase as k,resetOrigins as O}from"./originRegistry";import{startBusyProbe as p,stopBusyProbe as b,suppressNextGap as I}from"./busyProbe";import{getOriginals as f}from"./state";import{loadJsTopPrefs as H,saveJsTopPrefs as T}from"../utils/preferences";const A=h;class B{enabled=false;enabledLoaded=false;paused=false;remoteSampling=false;engineRunning=false;tiers={timers:false,promises:false,bridge:false,longtask:false};notifyHandle=null;appStateSub=null;snapshot=this.buildSnapshot();subscribers=new Set;enabledSubscribers=new Set;tierInstallers=[];longtaskProvider=null;freezeStatsProvider=null;clearHooks=[];constructor(){void H().then(t=>{this.enabledLoaded=true;if(t.enabled&&!this.enabled)this.setEnabled(true)})}subscribe(t){this.subscribers.add(t);this.syncEngineState();t(this.snapshot);return()=>{this.subscribers.delete(t);this.syncEngineState()}}subscribeEnabled(t){this.enabledSubscribers.add(t);t(this.enabled);return()=>{this.enabledSubscribers.delete(t)}}setEnabled(t){if(this.enabled===t)return;this.enabled=t;if(this.enabledLoaded)void T({enabled:t});this.enabledSubscribers.forEach(e=>{try{e(t)}catch(i){if(__DEV__){console.error("[js-top] enabled subscriber threw",i)}}});this.syncEngineState()}isEnabled(){return this.enabled}setRemoteSampling(t){if(this.remoteSampling===t)return;this.remoteSampling=t;this.syncEngineState()}isRemoteSampling(){return this.remoteSampling}getStatus(){if(!this.engineRunning)return"disabled";return this.paused?"paused":"running"}pause(){if(this.paused)return;this.paused=true;u(false);b();this.emitSnapshot()}resume(){if(!this.paused)return;this.paused=false;if(this.engineRunning){u(true);p()}this.emitSnapshot()}clear(){O();z();for(const t of this.clearHooks){try{t()}catch{}}this.emitSnapshot()}registerClearHook(t){this.clearHooks.push(t)}getSnapshot(){return this.snapshot}getOriginDetail(t){const e=d();const i=c(e);const a=this.freezeStatsProvider?.(Date.now());const s=a?.byOrigin[t];if(t==="unattributed"){const l=R(e);const r=Math.round(l.reduce((n,g)=>n+g,0)*10)/10;return{key:t,label:"unattributed (events / React / native call-ins)",api:"unattributed",promoted:false,scheduleCount:0,calls:0,totalMs:0,avgMs:0,maxMs:0,windowMs:r,pctOfBusy:i>0?Math.min(100,Math.round(r/i*1e3)/10):0,lastSeenAgoMs:0,buckets:l,freezes:s?.count??0,worstFreezeMs:Math.round(s?.maxMs??0)}}const o=k(t,e,i);if(!o)return null;return{...o,freezes:s?.count??0,worstFreezeMs:Math.round(s?.maxMs??0)}}registerTierInstaller(t){this.tierInstallers.push(t);if(this.engineRunning){Object.assign(this.tiers,this.runInstaller(t))}}registerLongtaskProvider(t){this.longtaskProvider=t}registerFreezeStatsProvider(t){this.freezeStatsProvider=t}shouldRun(){return this.subscribers.size>0||this.remoteSampling}syncEngineState(){const t=this.shouldRun();if(t&&!this.engineRunning)this.startEngine();else if(!t&&this.engineRunning)this.stopEngine()}runInstaller(t){try{return t()}catch(e){if(__DEV__){console.error("[js-top] tier installer threw",e)}return{}}}startEngine(){this.engineRunning=true;y();v();this.tiers={timers:true,promises:false,bridge:false,longtask:false};for(const t of this.tierInstallers){Object.assign(this.tiers,this.runInstaller(t))}u(!this.paused);if(!this.paused)p();try{this.appStateSub=m.addEventListener("change",()=>{I()})}catch{this.appStateSub=null}this.notifyHandle=f().setInterval(()=>this.emitSnapshot(),A);this.emitSnapshot()}stopEngine(){this.engineRunning=false;u(false);b();if(this.notifyHandle!=null){f().clearInterval(this.notifyHandle);this.notifyHandle=null}if(this.appStateSub){try{this.appStateSub.remove()}catch{}this.appStateSub=null}}buildSnapshot(){const t=d();const e=c(t);const i=P(t);const a=Math.max(0,e-i);const s=_(t,e,M);if(a>=1){s.push({key:"unattributed",label:"unattributed (events / React / native call-ins)",api:"unattributed",windowMs:Math.round(a*10)/10,pctOfBusy:e>0?Math.min(100,Math.round(a/e*1e3)/10):0,totalMs:0,calls:0,avgMs:0,maxMs:0,lastSeenAgoMs:0,freezes:0,worstFreezeMs:0});s.sort((r,n)=>n.windowMs-r.windowMs||n.totalMs-r.totalMs)}const o=this.freezeStatsProvider?.(Date.now())??{count:0,worstMs:0,byOrigin:{}};for(const r of s){const n=o.byOrigin[r.key];if(n){r.freezes=n.count;r.worstFreezeMs=Math.round(n.maxMs)}}const l=S*h;return{ts:Date.now(),paused:this.paused,busyPct:Math.min(100,Math.round(e/l*1e3)/10),busyHistory:E(t),tiers:{...this.tiers},rows:s,totals:{attributedWindowMs:Math.round(i*10)/10,unattributedWindowMs:Math.round(a*10)/10,windowMs:l},freezeSummary:{count:o.count,worstMs:Math.round(o.worstMs)},longtasks:this.longtaskProvider?this.longtaskProvider():[]}}emitSnapshot(){this.snapshot=this.buildSnapshot();this.subscribers.forEach(t=>{try{t(this.snapshot)}catch(e){if(__DEV__){console.error("[js-top] snapshot subscriber threw",e)}}})}isEngineActive(){return w()}}export const JsTopController=new B;
|
|
@@ -1,136 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Global per-bucket accounting: the engine's clock is divided into 250ms
|
|
5
|
-
* buckets (epochs). Each bucket tracks measured busy ms (from the probe) and
|
|
6
|
-
* attributed ms (from wrapped tasks); `unattributed = busy − attributed` per
|
|
7
|
-
* bucket is what keeps the table honest when work enters the thread through
|
|
8
|
-
* a path we can't wrap (bridgeless events, React commits, async/await).
|
|
9
|
-
*
|
|
10
|
-
* Rings are epoch-stamped parallel arrays: a slot is only valid when its
|
|
11
|
-
* stamp matches the epoch being read, so stale slots cost nothing to skip
|
|
12
|
-
* and nothing to zero eagerly.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { BUCKET_MS, HISTORY_BUCKETS, WINDOW_BUCKETS } from "../types";
|
|
16
|
-
const busyMs = new Float64Array(HISTORY_BUCKETS);
|
|
17
|
-
const attributedMs = new Float64Array(HISTORY_BUCKETS);
|
|
18
|
-
const longtaskMs = new Float64Array(HISTORY_BUCKETS);
|
|
19
|
-
const stamps = new Float64Array(HISTORY_BUCKETS).fill(-1);
|
|
20
|
-
export function epochOf(t) {
|
|
21
|
-
return Math.floor(t / BUCKET_MS);
|
|
22
|
-
}
|
|
23
|
-
function slotFor(epoch) {
|
|
24
|
-
const i = epoch % HISTORY_BUCKETS;
|
|
25
|
-
if (stamps[i] !== epoch) {
|
|
26
|
-
stamps[i] = epoch;
|
|
27
|
-
busyMs[i] = 0;
|
|
28
|
-
attributedMs[i] = 0;
|
|
29
|
-
longtaskMs[i] = 0;
|
|
30
|
-
}
|
|
31
|
-
return i;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Distribute a wall-clock span across the buckets it overlaps. Spans older
|
|
36
|
-
* than the history ring are clamped — a pathological hours-long gap books at
|
|
37
|
-
* most one ring's worth of buckets.
|
|
38
|
-
*/
|
|
39
|
-
function distribute(start, end, add) {
|
|
40
|
-
if (!(end > start)) return;
|
|
41
|
-
const clampedStart = Math.max(start, end - HISTORY_BUCKETS * BUCKET_MS);
|
|
42
|
-
let e = epochOf(clampedStart);
|
|
43
|
-
const lastEpoch = epochOf(end);
|
|
44
|
-
let cursor = clampedStart;
|
|
45
|
-
while (e <= lastEpoch) {
|
|
46
|
-
const bucketEnd = (e + 1) * BUCKET_MS;
|
|
47
|
-
const sliceEnd = Math.min(end, bucketEnd);
|
|
48
|
-
const ms = sliceEnd - cursor;
|
|
49
|
-
if (ms > 0) add(slotFor(e), ms);
|
|
50
|
-
cursor = sliceEnd;
|
|
51
|
-
e++;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Book probe-measured busy time over [start, end], capped per bucket. */
|
|
56
|
-
export function bookBusyRange(start, end) {
|
|
57
|
-
distribute(start, end, (i, ms) => {
|
|
58
|
-
busyMs[i] = Math.min(BUCKET_MS, busyMs[i] + ms);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Book a task's exclusive ms, spread proportionally over its wall span.
|
|
64
|
-
* (Exclusive time isn't contiguous when children interleave; proportional
|
|
65
|
-
* spread is exact at 250ms-bucket granularity for all practical tasks.)
|
|
66
|
-
*/
|
|
67
|
-
export function bookAttributedRange(start, end, exclusiveMs) {
|
|
68
|
-
const span = end - start;
|
|
69
|
-
if (!(span > 0) || !(exclusiveMs > 0)) return;
|
|
70
|
-
const ratio = Math.min(1, exclusiveMs / span);
|
|
71
|
-
distribute(start, end, (i, ms) => {
|
|
72
|
-
attributedMs[i] = Math.min(BUCKET_MS, attributedMs[i] + ms * ratio);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Book a reported longtask span (for history display only). */
|
|
77
|
-
export function bookLongtaskRange(start, end) {
|
|
78
|
-
distribute(start, end, (i, ms) => {
|
|
79
|
-
longtaskMs[i] = Math.min(BUCKET_MS, longtaskMs[i] + ms);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
function sumWindow(arr, nowT, buckets) {
|
|
83
|
-
const current = epochOf(nowT);
|
|
84
|
-
let sum = 0;
|
|
85
|
-
for (let e = current - buckets + 1; e <= current; e++) {
|
|
86
|
-
const i = e % HISTORY_BUCKETS;
|
|
87
|
-
if (i >= 0 && stamps[i] === e) sum += arr[i];
|
|
88
|
-
}
|
|
89
|
-
return sum;
|
|
90
|
-
}
|
|
91
|
-
export function windowBusyMs(nowT) {
|
|
92
|
-
return sumWindow(busyMs, nowT, WINDOW_BUCKETS);
|
|
93
|
-
}
|
|
94
|
-
export function windowAttributedMs(nowT) {
|
|
95
|
-
return sumWindow(attributedMs, nowT, WINDOW_BUCKETS);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Per-bucket UNATTRIBUTED ms (busy − attributed), oldest→newest over the
|
|
100
|
-
* rolling window — the "system" row's activity trend for the detail view.
|
|
101
|
-
*/
|
|
102
|
-
export function windowResidualBuckets(nowT) {
|
|
103
|
-
const current = epochOf(nowT);
|
|
104
|
-
const out = [];
|
|
105
|
-
for (let e = current - WINDOW_BUCKETS + 1; e <= current; e++) {
|
|
106
|
-
const i = e % HISTORY_BUCKETS;
|
|
107
|
-
if (i >= 0 && stamps[i] === e) {
|
|
108
|
-
out.push(Math.max(0, Math.round((busyMs[i] - attributedMs[i]) * 10) / 10));
|
|
109
|
-
} else {
|
|
110
|
-
out.push(0);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Per-bucket busy%, oldest→newest, over the full 30s history ring. */
|
|
117
|
-
export function busyHistory(nowT) {
|
|
118
|
-
const current = epochOf(nowT);
|
|
119
|
-
const out = [];
|
|
120
|
-
for (let e = current - HISTORY_BUCKETS + 1; e <= current; e++) {
|
|
121
|
-
const i = e % HISTORY_BUCKETS;
|
|
122
|
-
if (i >= 0 && stamps[i] === e) {
|
|
123
|
-
out.push(Math.round(busyMs[i] / BUCKET_MS * 100));
|
|
124
|
-
} else {
|
|
125
|
-
out.push(0);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// Trim the leading dead zone from before the engine started.
|
|
129
|
-
let firstLive = 0;
|
|
130
|
-
while (firstLive < out.length && out[firstLive] === 0) firstLive++;
|
|
131
|
-
return out.slice(Math.max(0, Math.min(firstLive, out.length - 1)));
|
|
132
|
-
}
|
|
133
|
-
export function resetAggregator() {
|
|
134
|
-
stamps.fill(-1);
|
|
135
|
-
}
|
|
136
|
-
//# sourceMappingURL=aggregator.js.map
|
|
1
|
+
"use strict";import{BUCKET_MS as c,HISTORY_BUCKETS as i,WINDOW_BUCKETS as h}from"../types";const a=new Float64Array(i);const l=new Float64Array(i);const p=new Float64Array(i);const f=new Float64Array(i).fill(-1);export function epochOf(n){return Math.floor(n/c)}function d(n){const t=n%i;if(f[t]!==n){f[t]=n;a[t]=0;l[t]=0;p[t]=0}return t}function M(n,t,o){if(!(t>n))return;const r=Math.max(n,t-i*c);let e=epochOf(r);const s=epochOf(t);let u=r;while(e<=s){const b=(e+1)*c;const m=Math.min(t,b);const w=m-u;if(w>0)o(d(e),w);u=m;e++}}export function bookBusyRange(n,t){M(n,t,(o,r)=>{a[o]=Math.min(c,a[o]+r)})}export function bookAttributedRange(n,t,o){const r=t-n;if(!(r>0)||!(o>0))return;const e=Math.min(1,o/r);M(n,t,(s,u)=>{l[s]=Math.min(c,l[s]+u*e)})}export function bookLongtaskRange(n,t){M(n,t,(o,r)=>{p[o]=Math.min(c,p[o]+r)})}function x(n,t,o){const r=epochOf(t);let e=0;for(let s=r-o+1;s<=r;s++){const u=s%i;if(u>=0&&f[u]===s)e+=n[u]}return e}export function windowBusyMs(n){return x(a,n,h)}export function windowAttributedMs(n){return x(l,n,h)}export function windowResidualBuckets(n){const t=epochOf(n);const o=[];for(let r=t-h+1;r<=t;r++){const e=r%i;if(e>=0&&f[e]===r){o.push(Math.max(0,Math.round((a[e]-l[e])*10)/10))}else{o.push(0)}}return o}export function busyHistory(n){const t=epochOf(n);const o=[];for(let e=t-i+1;e<=t;e++){const s=e%i;if(s>=0&&f[s]===e){o.push(Math.round(a[s]/c*100))}else{o.push(0)}}let r=0;while(r<o.length&&o[r]===0)r++;return o.slice(Math.max(0,Math.min(r,o.length-1)))}export function resetAggregator(){f.fill(-1)}
|
|
@@ -1,94 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Busy probe — measures true JS-thread occupancy from timer-gap inflation.
|
|
5
|
-
*
|
|
6
|
-
* A 16ms heartbeat (created with the PRE-PATCH setInterval so the probe never
|
|
7
|
-
* appears in its own table) can only fire when the thread yields; any gap
|
|
8
|
-
* beyond the calibrated idle baseline is time the thread was busy. This is
|
|
9
|
-
* the same physical signal as perf-monitor's stall-aware sampler, at higher
|
|
10
|
-
* frequency: multi-second stalls simply arrive as one huge gap and are booked
|
|
11
|
-
* as fully-busy buckets by bookBusyRange (the backfill falls out of the
|
|
12
|
-
* range-distribution math; no separate synthetic-sample pass needed).
|
|
13
|
-
*
|
|
14
|
-
* Calibration: the nominal 16ms is never what a real device delivers at
|
|
15
|
-
* idle. We track a rolling min gap (3 one-second slots) and subtract THAT,
|
|
16
|
-
* otherwise idle devices read 10–20% phantom busy.
|
|
17
|
-
*
|
|
18
|
-
* AppState transitions also pause JS timers, so the controller flags them
|
|
19
|
-
* via suppressNextGap() — same trick as PerfMonitorController; AppState
|
|
20
|
-
* events arrive on the JS thread, so a genuine stall can never set the flag.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { now } from "./now";
|
|
24
|
-
import { getOriginals } from "./state";
|
|
25
|
-
import { bookBusyRange } from "./aggregator";
|
|
26
|
-
const PROBE_INTERVAL_MS = 16;
|
|
27
|
-
/** Ignore gaps below this when calibrating (a busy tick isn't a baseline). */
|
|
28
|
-
const CALIBRATION_MAX_GAP_MS = 100;
|
|
29
|
-
const BASELINE_SLOTS = 3;
|
|
30
|
-
const MIN_BASELINE_MS = 8;
|
|
31
|
-
let handle = null;
|
|
32
|
-
let lastAt = 0;
|
|
33
|
-
let suppressNext = false;
|
|
34
|
-
let slotMins = [];
|
|
35
|
-
let currentSlotMin = Infinity;
|
|
36
|
-
let currentSlotStart = 0;
|
|
37
|
-
let baseline = PROBE_INTERVAL_MS;
|
|
38
|
-
function buoyJsTopProbeTick() {
|
|
39
|
-
const t = now();
|
|
40
|
-
const gap = lastAt > 0 ? t - lastAt : 0;
|
|
41
|
-
lastAt = t;
|
|
42
|
-
if (suppressNext) {
|
|
43
|
-
// Backgrounding/foregrounding paused timers — not a stall. Reset
|
|
44
|
-
// calibration too: refresh-rate or thermal state may have changed.
|
|
45
|
-
suppressNext = false;
|
|
46
|
-
currentSlotMin = Infinity;
|
|
47
|
-
currentSlotStart = t;
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (gap <= 0) return;
|
|
51
|
-
if (gap < CALIBRATION_MAX_GAP_MS) {
|
|
52
|
-
if (gap < currentSlotMin) currentSlotMin = gap;
|
|
53
|
-
if (currentSlotStart === 0) currentSlotStart = t;
|
|
54
|
-
if (t - currentSlotStart >= 1000) {
|
|
55
|
-
slotMins.push(currentSlotMin);
|
|
56
|
-
if (slotMins.length > BASELINE_SLOTS) slotMins.shift();
|
|
57
|
-
baseline = Math.max(MIN_BASELINE_MS, Math.min(...slotMins));
|
|
58
|
-
currentSlotMin = Infinity;
|
|
59
|
-
currentSlotStart = t;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const busyExcess = gap - baseline;
|
|
63
|
-
if (busyExcess > 1) {
|
|
64
|
-
// The block ended when this tick finally fired — position it at the tail.
|
|
65
|
-
bookBusyRange(t - busyExcess, t);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
export function startBusyProbe() {
|
|
69
|
-
if (handle != null) return;
|
|
70
|
-
const originals = getOriginals();
|
|
71
|
-
lastAt = now();
|
|
72
|
-
suppressNext = false;
|
|
73
|
-
slotMins = [];
|
|
74
|
-
currentSlotMin = Infinity;
|
|
75
|
-
currentSlotStart = 0;
|
|
76
|
-
baseline = PROBE_INTERVAL_MS;
|
|
77
|
-
handle = originals.setInterval(buoyJsTopProbeTick, PROBE_INTERVAL_MS);
|
|
78
|
-
}
|
|
79
|
-
export function stopBusyProbe() {
|
|
80
|
-
if (handle != null) {
|
|
81
|
-
getOriginals().clearInterval(handle);
|
|
82
|
-
handle = null;
|
|
83
|
-
}
|
|
84
|
-
lastAt = 0;
|
|
85
|
-
}
|
|
86
|
-
export function suppressNextGap() {
|
|
87
|
-
suppressNext = true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Test-only. */
|
|
91
|
-
export function debugBaseline() {
|
|
92
|
-
return baseline;
|
|
93
|
-
}
|
|
94
|
-
//# sourceMappingURL=busyProbe.js.map
|
|
1
|
+
"use strict";import{now as c}from"./now";import{getOriginals as p}from"./state";import{bookBusyRange as I}from"./aggregator";const u=16;const S=100;const y=3;const M=8;let s=null;let o=0;let l=false;let r=[];let n=Infinity;let e=0;let f=u;function b(){const t=c();const i=o>0?t-o:0;o=t;if(l){l=false;n=Infinity;e=t;return}if(i<=0)return;if(i<S){if(i<n)n=i;if(e===0)e=t;if(t-e>=1e3){r.push(n);if(r.length>y)r.shift();f=Math.max(M,Math.min(...r));n=Infinity;e=t}}const a=i-f;if(a>1){I(t-a,t)}}export function startBusyProbe(){if(s!=null)return;const t=p();o=c();l=false;r=[];n=Infinity;e=0;f=u;s=t.setInterval(b,u)}export function stopBusyProbe(){if(s!=null){p().clearInterval(s);s=null}o=0}export function suppressNextGap(){l=true}export function debugBaseline(){return f}
|