@buoy-gg/js-top 7.0.29 → 7.0.31
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,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TaskDetailView — the "inspect process" screen behind a row tap.
|
|
3
|
-
*
|
|
4
|
-
* Shows everything the engine knows about one origin: a 5s activity bar
|
|
5
|
-
* chart (the per-origin bucket ring, rendered nowhere else), the full stat
|
|
6
|
-
* grid (schedules vs runs, avg/max/total, freezes), the captured scheduling
|
|
7
|
-
* site (caller + raw frames), and a plain-language explanation of what this
|
|
8
|
-
* origin type means. Data refreshes with the modal's snapshot tick — the
|
|
9
|
-
* parent re-renders us and we re-pull the detail.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { memo } from "react";
|
|
13
|
-
import { ScrollView, StyleSheet, Text, View } from "react-native";
|
|
14
|
-
import { macOSColors, night, useSafeAreaInsets } from "@buoy-gg/shared-ui";
|
|
15
|
-
import type { JsTopOriginDetail, TaskApi } from "../types";
|
|
16
|
-
import { badgeFor, displayName } from "./TaskTable";
|
|
17
|
-
|
|
18
|
-
const API_EXPLAINER: Record<string, string> = {
|
|
19
|
-
timeout:
|
|
20
|
-
"Work scheduled with setTimeout. Time shown is the callback's exclusive run time on the JS thread.",
|
|
21
|
-
interval:
|
|
22
|
-
"A repeating setInterval. One scheduling site, many runs — a hot interval is the classic silent JS-FPS killer.",
|
|
23
|
-
immediate:
|
|
24
|
-
"Work scheduled with setImmediate — runs at the end of the current event-loop turn.",
|
|
25
|
-
raf: "requestAnimationFrame work — runs once per frame while scheduled. Cost here directly trades against 60fps.",
|
|
26
|
-
microtask:
|
|
27
|
-
"queueMicrotask work — runs after the current task, before the thread yields.",
|
|
28
|
-
then: "Promise reaction (.then/.catch/.finally). Named by the function that attached the reaction. Note: async/await bodies bypass .then and land in the System row instead.",
|
|
29
|
-
bridge:
|
|
30
|
-
"A native→JS call-in over the legacy bridge (events, module callbacks), measured as its un-nested remainder.",
|
|
31
|
-
unattributed:
|
|
32
|
-
"Thread time the probe measured but no JS wrapper could claim: touch handlers, React commits, native call-ins, and async/await bodies. High here on New Architecture is normal — it's where UI work lives.",
|
|
33
|
-
other:
|
|
34
|
-
"Origins evicted from the table when the registry hit its cap — their totals are preserved here.",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function explainerFor(api: TaskApi): string {
|
|
38
|
-
return API_EXPLAINER[api] ?? API_EXPLAINER.other;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function fmt(n: number): string {
|
|
42
|
-
if (n >= 10_000) return `${Math.round(n / 1000)}k`;
|
|
43
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
44
|
-
if (n >= 100) return `${Math.round(n)}`;
|
|
45
|
-
return `${Math.round(n * 100) / 100}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function agoFmt(ms: number): string {
|
|
49
|
-
if (ms <= 0) return "—";
|
|
50
|
-
const sec = ms / 1000;
|
|
51
|
-
if (sec < 1) return "just now";
|
|
52
|
-
if (sec < 60) return `${Math.round(sec)}s ago`;
|
|
53
|
-
return `${Math.floor(sec / 60)}m ago`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function Stat({ label, value, accent }: { label: string; value: string; accent?: string }) {
|
|
57
|
-
return (
|
|
58
|
-
<View style={styles.stat}>
|
|
59
|
-
<Text style={styles.statLabel}>{label}</Text>
|
|
60
|
-
<Text style={[styles.statValue, accent ? { color: accent } : null]}>
|
|
61
|
-
{value}
|
|
62
|
-
</Text>
|
|
63
|
-
</View>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const TaskDetailView = memo(function TaskDetailView({
|
|
68
|
-
detail,
|
|
69
|
-
}: {
|
|
70
|
-
detail: JsTopOriginDetail;
|
|
71
|
-
}) {
|
|
72
|
-
const badge = badgeFor(detail.api);
|
|
73
|
-
const insets = useSafeAreaInsets({ minBottom: 12 });
|
|
74
|
-
const maxBucket = Math.max(1, ...detail.buckets);
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<ScrollView
|
|
78
|
-
style={styles.root}
|
|
79
|
-
contentContainerStyle={{ paddingBottom: insets.bottom }}
|
|
80
|
-
>
|
|
81
|
-
{/* Identity */}
|
|
82
|
-
<View style={styles.identityRow}>
|
|
83
|
-
<View
|
|
84
|
-
style={[
|
|
85
|
-
styles.badge,
|
|
86
|
-
{ borderColor: badge.color, backgroundColor: `${badge.color}1A` },
|
|
87
|
-
]}
|
|
88
|
-
>
|
|
89
|
-
<Text style={[styles.badgeText, { color: badge.color }]}>
|
|
90
|
-
{badge.text}
|
|
91
|
-
</Text>
|
|
92
|
-
</View>
|
|
93
|
-
<Text style={styles.name} numberOfLines={2}>
|
|
94
|
-
{displayName(detail)}
|
|
95
|
-
</Text>
|
|
96
|
-
</View>
|
|
97
|
-
<Text style={styles.explainer}>{explainerFor(detail.api)}</Text>
|
|
98
|
-
|
|
99
|
-
{/* 5s activity trend — the per-origin bucket ring */}
|
|
100
|
-
<Text style={styles.sectionTitle}>ACTIVITY · LAST 5S</Text>
|
|
101
|
-
<View style={styles.chartRow}>
|
|
102
|
-
{detail.buckets.map((ms, i) => (
|
|
103
|
-
<View key={`bk${i}`} style={styles.chartBarSlot}>
|
|
104
|
-
<View
|
|
105
|
-
style={[
|
|
106
|
-
styles.chartBar,
|
|
107
|
-
{
|
|
108
|
-
height: Math.max(2, (ms / maxBucket) * 44),
|
|
109
|
-
backgroundColor: badge.color,
|
|
110
|
-
},
|
|
111
|
-
]}
|
|
112
|
-
/>
|
|
113
|
-
</View>
|
|
114
|
-
))}
|
|
115
|
-
</View>
|
|
116
|
-
<View style={styles.chartLegend}>
|
|
117
|
-
<Text style={styles.chartLegendText}>
|
|
118
|
-
{fmt(detail.windowMs)} ms in window · {detail.pctOfBusy.toFixed(1)}%
|
|
119
|
-
of busy time
|
|
120
|
-
</Text>
|
|
121
|
-
<Text style={styles.chartLegendText}>peak {fmt(maxBucket)} ms/250ms</Text>
|
|
122
|
-
</View>
|
|
123
|
-
|
|
124
|
-
{/* Stats */}
|
|
125
|
-
<Text style={styles.sectionTitle}>STATS · SINCE START</Text>
|
|
126
|
-
<View style={styles.statGrid}>
|
|
127
|
-
<Stat label="RUNS" value={detail.calls > 0 ? fmt(detail.calls) : "—"} />
|
|
128
|
-
<Stat
|
|
129
|
-
label="SCHEDULED"
|
|
130
|
-
value={detail.scheduleCount > 0 ? fmt(detail.scheduleCount) : "—"}
|
|
131
|
-
/>
|
|
132
|
-
<Stat
|
|
133
|
-
label="AVG / RUN"
|
|
134
|
-
value={detail.calls > 0 ? `${fmt(detail.avgMs)} ms` : "—"}
|
|
135
|
-
/>
|
|
136
|
-
<Stat
|
|
137
|
-
label="MAX / RUN"
|
|
138
|
-
value={detail.maxMs > 0 ? `${fmt(detail.maxMs)} ms` : "—"}
|
|
139
|
-
accent={
|
|
140
|
-
detail.maxMs >= 50 ? macOSColors.semantic.warning : undefined
|
|
141
|
-
}
|
|
142
|
-
/>
|
|
143
|
-
<Stat
|
|
144
|
-
label="TOTAL"
|
|
145
|
-
value={detail.totalMs > 0 ? `${fmt(detail.totalMs)} ms` : "—"}
|
|
146
|
-
/>
|
|
147
|
-
<Stat label="LAST RUN" value={agoFmt(detail.lastSeenAgoMs)} />
|
|
148
|
-
<Stat
|
|
149
|
-
label="FREEZES · 60S"
|
|
150
|
-
value={detail.freezes > 0 ? `${detail.freezes}` : "0"}
|
|
151
|
-
accent={
|
|
152
|
-
detail.freezes > 0 ? macOSColors.semantic.error : undefined
|
|
153
|
-
}
|
|
154
|
-
/>
|
|
155
|
-
<Stat
|
|
156
|
-
label="WORST FREEZE"
|
|
157
|
-
value={detail.worstFreezeMs > 0 ? `${detail.worstFreezeMs} ms` : "—"}
|
|
158
|
-
accent={
|
|
159
|
-
detail.worstFreezeMs >= 100
|
|
160
|
-
? macOSColors.semantic.error
|
|
161
|
-
: detail.worstFreezeMs > 0
|
|
162
|
-
? macOSColors.semantic.warning
|
|
163
|
-
: undefined
|
|
164
|
-
}
|
|
165
|
-
/>
|
|
166
|
-
</View>
|
|
167
|
-
|
|
168
|
-
{/* Scheduling site */}
|
|
169
|
-
{(detail.caller || (detail.frames?.length ?? 0) > 0) && (
|
|
170
|
-
<>
|
|
171
|
-
<Text style={styles.sectionTitle}>SCHEDULED FROM</Text>
|
|
172
|
-
<View style={styles.siteCard}>
|
|
173
|
-
{detail.caller && (
|
|
174
|
-
<Text style={styles.siteCaller}>{detail.caller}()</Text>
|
|
175
|
-
)}
|
|
176
|
-
{detail.file && (
|
|
177
|
-
<Text style={styles.siteFile}>
|
|
178
|
-
{detail.file}
|
|
179
|
-
{detail.line != null ? `:${detail.line}` : ""}
|
|
180
|
-
</Text>
|
|
181
|
-
)}
|
|
182
|
-
{detail.frames?.map((f, i) => (
|
|
183
|
-
<Text key={`fr${i}`} style={styles.siteFrame} numberOfLines={1}>
|
|
184
|
-
{f.file}:{f.line}:{f.col}
|
|
185
|
-
</Text>
|
|
186
|
-
))}
|
|
187
|
-
<Text style={styles.siteNote}>
|
|
188
|
-
Frames are bundle offsets (Hermes) — the caller name above is the
|
|
189
|
-
readable part. Captured once when this origin got hot.
|
|
190
|
-
</Text>
|
|
191
|
-
</View>
|
|
192
|
-
</>
|
|
193
|
-
)}
|
|
194
|
-
{!detail.caller &&
|
|
195
|
-
(detail.frames?.length ?? 0) === 0 &&
|
|
196
|
-
detail.api !== "unattributed" &&
|
|
197
|
-
detail.api !== "other" && (
|
|
198
|
-
<>
|
|
199
|
-
<Text style={styles.sectionTitle}>SCHEDULED FROM</Text>
|
|
200
|
-
<Text style={styles.sitePending}>
|
|
201
|
-
{detail.promoted
|
|
202
|
-
? "Stack capture pending — next schedule from this site will record it."
|
|
203
|
-
: "Not hot enough yet — the scheduling stack is captured once an origin passes 25 schedules or 50ms total."}
|
|
204
|
-
</Text>
|
|
205
|
-
</>
|
|
206
|
-
)}
|
|
207
|
-
</ScrollView>
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const styles = StyleSheet.create({
|
|
212
|
-
root: {
|
|
213
|
-
flex: 1,
|
|
214
|
-
// Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
|
|
215
|
-
backgroundColor: "transparent",
|
|
216
|
-
paddingHorizontal: 12,
|
|
217
|
-
},
|
|
218
|
-
identityRow: {
|
|
219
|
-
flexDirection: "row",
|
|
220
|
-
alignItems: "center",
|
|
221
|
-
gap: 8,
|
|
222
|
-
paddingTop: 12,
|
|
223
|
-
},
|
|
224
|
-
badge: {
|
|
225
|
-
borderWidth: 1,
|
|
226
|
-
borderRadius: 3,
|
|
227
|
-
paddingHorizontal: 5,
|
|
228
|
-
paddingVertical: 2,
|
|
229
|
-
},
|
|
230
|
-
badgeText: {
|
|
231
|
-
fontSize: 9,
|
|
232
|
-
fontWeight: "700",
|
|
233
|
-
letterSpacing: 0.4,
|
|
234
|
-
},
|
|
235
|
-
name: {
|
|
236
|
-
flex: 1,
|
|
237
|
-
color: night.color.text,
|
|
238
|
-
fontSize: 15,
|
|
239
|
-
fontWeight: "600",
|
|
240
|
-
},
|
|
241
|
-
explainer: {
|
|
242
|
-
color: night.color.textTertiary,
|
|
243
|
-
fontSize: 11,
|
|
244
|
-
lineHeight: 15,
|
|
245
|
-
marginTop: 6,
|
|
246
|
-
marginBottom: 4,
|
|
247
|
-
},
|
|
248
|
-
sectionTitle: {
|
|
249
|
-
color: night.color.textTertiary,
|
|
250
|
-
fontSize: 9,
|
|
251
|
-
fontWeight: "700",
|
|
252
|
-
letterSpacing: 0.5,
|
|
253
|
-
marginTop: 14,
|
|
254
|
-
marginBottom: 6,
|
|
255
|
-
},
|
|
256
|
-
chartRow: {
|
|
257
|
-
flexDirection: "row",
|
|
258
|
-
alignItems: "flex-end",
|
|
259
|
-
height: 48,
|
|
260
|
-
gap: 2,
|
|
261
|
-
backgroundColor: night.color.surface,
|
|
262
|
-
borderRadius: 6,
|
|
263
|
-
borderWidth: 1,
|
|
264
|
-
borderColor: night.color.border,
|
|
265
|
-
paddingHorizontal: 6,
|
|
266
|
-
paddingTop: 2,
|
|
267
|
-
},
|
|
268
|
-
chartBarSlot: {
|
|
269
|
-
flex: 1,
|
|
270
|
-
alignItems: "center",
|
|
271
|
-
justifyContent: "flex-end",
|
|
272
|
-
},
|
|
273
|
-
chartBar: {
|
|
274
|
-
width: "70%",
|
|
275
|
-
borderTopLeftRadius: 2,
|
|
276
|
-
borderTopRightRadius: 2,
|
|
277
|
-
opacity: 0.9,
|
|
278
|
-
},
|
|
279
|
-
chartLegend: {
|
|
280
|
-
flexDirection: "row",
|
|
281
|
-
justifyContent: "space-between",
|
|
282
|
-
marginTop: 4,
|
|
283
|
-
},
|
|
284
|
-
chartLegendText: {
|
|
285
|
-
color: night.color.textTertiary,
|
|
286
|
-
fontSize: 10,
|
|
287
|
-
fontVariant: ["tabular-nums"],
|
|
288
|
-
},
|
|
289
|
-
statGrid: {
|
|
290
|
-
flexDirection: "row",
|
|
291
|
-
flexWrap: "wrap",
|
|
292
|
-
gap: 8,
|
|
293
|
-
},
|
|
294
|
-
stat: {
|
|
295
|
-
width: "23%",
|
|
296
|
-
minWidth: 76,
|
|
297
|
-
flexGrow: 1,
|
|
298
|
-
backgroundColor: night.color.surface,
|
|
299
|
-
borderWidth: 1,
|
|
300
|
-
borderColor: night.color.border,
|
|
301
|
-
borderRadius: 6,
|
|
302
|
-
paddingHorizontal: 8,
|
|
303
|
-
paddingVertical: 6,
|
|
304
|
-
},
|
|
305
|
-
statLabel: {
|
|
306
|
-
color: night.color.textTertiary,
|
|
307
|
-
fontSize: 8,
|
|
308
|
-
fontWeight: "700",
|
|
309
|
-
letterSpacing: 0.4,
|
|
310
|
-
marginBottom: 3,
|
|
311
|
-
},
|
|
312
|
-
statValue: {
|
|
313
|
-
color: night.color.text,
|
|
314
|
-
fontSize: 13,
|
|
315
|
-
fontWeight: "600",
|
|
316
|
-
fontVariant: ["tabular-nums"],
|
|
317
|
-
},
|
|
318
|
-
siteCard: {
|
|
319
|
-
backgroundColor: night.color.surface,
|
|
320
|
-
borderWidth: 1,
|
|
321
|
-
borderColor: night.color.border,
|
|
322
|
-
borderRadius: 6,
|
|
323
|
-
padding: 10,
|
|
324
|
-
gap: 3,
|
|
325
|
-
},
|
|
326
|
-
siteCaller: {
|
|
327
|
-
color: night.color.text,
|
|
328
|
-
fontSize: 13,
|
|
329
|
-
fontWeight: "600",
|
|
330
|
-
},
|
|
331
|
-
siteFile: {
|
|
332
|
-
color: night.color.info,
|
|
333
|
-
fontSize: 11,
|
|
334
|
-
},
|
|
335
|
-
siteFrame: {
|
|
336
|
-
color: night.color.textTertiary,
|
|
337
|
-
fontSize: 10,
|
|
338
|
-
fontVariant: ["tabular-nums"],
|
|
339
|
-
},
|
|
340
|
-
siteNote: {
|
|
341
|
-
color: night.color.textTertiary,
|
|
342
|
-
fontSize: 9,
|
|
343
|
-
opacity: 0.8,
|
|
344
|
-
marginTop: 4,
|
|
345
|
-
},
|
|
346
|
-
sitePending: {
|
|
347
|
-
color: night.color.textTertiary,
|
|
348
|
-
fontSize: 11,
|
|
349
|
-
},
|
|
350
|
-
});
|
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ranked task table — the "process list".
|
|
3
|
-
*
|
|
4
|
-
* Row layout (phone-first): the API is a small colored badge and the FULL
|
|
5
|
-
* width goes to the task name (the part that varies); numbers live on a
|
|
6
|
-
* second stats line instead of fixed-width columns, so nothing truncates:
|
|
7
|
-
*
|
|
8
|
-
* 1 [INTERVAL] hotIntervalWork 1240 ms 41%
|
|
9
|
-
* ▓▓▓▓▓░░░░░ 50 calls · avg 24.8 · max 31.0 · total 1.2k
|
|
10
|
-
*
|
|
11
|
-
* Sorting is a chip row above the list (column headers don't survive on a
|
|
12
|
-
* narrow screen). Rows keyed by origin key (never index: rows re-rank every
|
|
13
|
-
* tick) and memoized with a precise comparator so a tick only re-renders
|
|
14
|
-
* rows whose numbers moved.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { memo, useState } from "react";
|
|
18
|
-
import {
|
|
19
|
-
ScrollView,
|
|
20
|
-
StyleSheet,
|
|
21
|
-
Text,
|
|
22
|
-
TouchableOpacity,
|
|
23
|
-
View,
|
|
24
|
-
} from "react-native";
|
|
25
|
-
import { macOSColors, night } from "@buoy-gg/shared-ui";
|
|
26
|
-
import type { JsTopRow, TaskApi } from "../types";
|
|
27
|
-
|
|
28
|
-
export type SortKey = "windowMs" | "calls" | "avgMs" | "maxMs" | "totalMs";
|
|
29
|
-
|
|
30
|
-
const SORT_OPTIONS: { key: SortKey; title: string }[] = [
|
|
31
|
-
{ key: "windowMs", title: "RECENT" },
|
|
32
|
-
{ key: "calls", title: "CALLS" },
|
|
33
|
-
{ key: "avgMs", title: "AVG" },
|
|
34
|
-
{ key: "maxMs", title: "MAX" },
|
|
35
|
-
{ key: "totalMs", title: "TOTAL" },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
export const API_BADGE: Record<
|
|
39
|
-
string,
|
|
40
|
-
{ text: string; color: string; prefix?: string }
|
|
41
|
-
> = {
|
|
42
|
-
timeout: { text: "TIMEOUT", color: "#38BDF8", prefix: "setTimeout" },
|
|
43
|
-
interval: { text: "INTERVAL", color: "#22D3EE", prefix: "setInterval" },
|
|
44
|
-
immediate: { text: "IMMED", color: "#60A5FA", prefix: "setImmediate" },
|
|
45
|
-
raf: { text: "RAF", color: "#34D399", prefix: "requestAnimationFrame" },
|
|
46
|
-
microtask: { text: "MICRO", color: "#A78BFA", prefix: "queueMicrotask" },
|
|
47
|
-
then: { text: "PROMISE", color: "#BF5AF2", prefix: "Promise.then" },
|
|
48
|
-
bridge: { text: "BRIDGE", color: "#FBBF24", prefix: "bridge" },
|
|
49
|
-
unattributed: { text: "SYSTEM", color: "#8E8E93" },
|
|
50
|
-
other: { text: "OTHER", color: "#8E8E93", prefix: "other" },
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export function badgeFor(api: TaskApi) {
|
|
54
|
-
return API_BADGE[api] ?? API_BADGE.other;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The row label minus its API prefix — the badge already says the API. */
|
|
58
|
-
export function displayName(row: { api: TaskApi; label: string }): string {
|
|
59
|
-
if (row.api === "unattributed") return "events · React · native call-ins";
|
|
60
|
-
if (row.api === "other") return "evicted cold origins";
|
|
61
|
-
const prefix = badgeFor(row.api).prefix;
|
|
62
|
-
let name = row.label;
|
|
63
|
-
if (prefix && name.startsWith(prefix)) name = name.slice(prefix.length);
|
|
64
|
-
name = name.replace(/^[\s←@]+/, "").trim();
|
|
65
|
-
return name.length > 0 ? name : "anonymous";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function fmt(n: number): string {
|
|
69
|
-
if (n >= 10_000) return `${Math.round(n / 1000)}k`;
|
|
70
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
71
|
-
if (n >= 100) return `${Math.round(n)}`;
|
|
72
|
-
return `${Math.round(n * 10) / 10}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function statsLine(row: JsTopRow): string {
|
|
76
|
-
if (row.calls <= 0) return "measured by probe · not reachable from JS";
|
|
77
|
-
return `${fmt(row.calls)} calls · avg ${fmt(row.avgMs)} · max ${fmt(row.maxMs)} · total ${fmt(row.totalMs)} ms`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const TaskRow = memo(
|
|
81
|
-
function TaskRow({
|
|
82
|
-
row,
|
|
83
|
-
rank,
|
|
84
|
-
onPress,
|
|
85
|
-
}: {
|
|
86
|
-
row: JsTopRow;
|
|
87
|
-
rank: number;
|
|
88
|
-
onPress?: (row: JsTopRow) => void;
|
|
89
|
-
}) {
|
|
90
|
-
const badge = badgeFor(row.api);
|
|
91
|
-
const Wrapper = onPress ? TouchableOpacity : View;
|
|
92
|
-
return (
|
|
93
|
-
<Wrapper
|
|
94
|
-
style={rowStyles.row}
|
|
95
|
-
{...(onPress ? { onPress: () => onPress(row), activeOpacity: 0.6 } : {})}
|
|
96
|
-
>
|
|
97
|
-
<View style={rowStyles.topLine}>
|
|
98
|
-
<Text style={rowStyles.rank}>{rank}</Text>
|
|
99
|
-
<View
|
|
100
|
-
style={[
|
|
101
|
-
rowStyles.badge,
|
|
102
|
-
{ borderColor: badge.color, backgroundColor: `${badge.color}1A` },
|
|
103
|
-
]}
|
|
104
|
-
>
|
|
105
|
-
<Text style={[rowStyles.badgeText, { color: badge.color }]}>
|
|
106
|
-
{badge.text}
|
|
107
|
-
</Text>
|
|
108
|
-
</View>
|
|
109
|
-
<Text style={rowStyles.name} numberOfLines={1}>
|
|
110
|
-
{displayName(row)}
|
|
111
|
-
</Text>
|
|
112
|
-
{row.freezes > 0 && (
|
|
113
|
-
<Text
|
|
114
|
-
style={[
|
|
115
|
-
rowStyles.freezeBadge,
|
|
116
|
-
row.worstFreezeMs >= 100 && rowStyles.freezeBadgeSevere,
|
|
117
|
-
]}
|
|
118
|
-
>
|
|
119
|
-
⚠ ×{row.freezes}
|
|
120
|
-
</Text>
|
|
121
|
-
)}
|
|
122
|
-
<Text
|
|
123
|
-
style={[
|
|
124
|
-
rowStyles.windowMs,
|
|
125
|
-
row.api !== "unattributed" &&
|
|
126
|
-
row.pctOfBusy >= 25 && {
|
|
127
|
-
color:
|
|
128
|
-
row.pctOfBusy >= 50
|
|
129
|
-
? macOSColors.semantic.error
|
|
130
|
-
: macOSColors.semantic.warning,
|
|
131
|
-
},
|
|
132
|
-
]}
|
|
133
|
-
>
|
|
134
|
-
{fmt(row.windowMs)}
|
|
135
|
-
<Text style={rowStyles.unit}> ms</Text>
|
|
136
|
-
</Text>
|
|
137
|
-
<Text
|
|
138
|
-
style={[
|
|
139
|
-
rowStyles.pct,
|
|
140
|
-
row.api === "unattributed" && rowStyles.pctMuted,
|
|
141
|
-
]}
|
|
142
|
-
>
|
|
143
|
-
{Math.round(row.pctOfBusy)}%
|
|
144
|
-
</Text>
|
|
145
|
-
</View>
|
|
146
|
-
<View style={rowStyles.subLine}>
|
|
147
|
-
<View style={rowStyles.barTrack}>
|
|
148
|
-
<View
|
|
149
|
-
style={[
|
|
150
|
-
rowStyles.barFill,
|
|
151
|
-
{
|
|
152
|
-
width: `${Math.min(100, row.pctOfBusy)}%`,
|
|
153
|
-
backgroundColor: badge.color,
|
|
154
|
-
},
|
|
155
|
-
]}
|
|
156
|
-
/>
|
|
157
|
-
</View>
|
|
158
|
-
<Text style={rowStyles.stats} numberOfLines={1}>
|
|
159
|
-
{statsLine(row)}
|
|
160
|
-
</Text>
|
|
161
|
-
</View>
|
|
162
|
-
</Wrapper>
|
|
163
|
-
);
|
|
164
|
-
},
|
|
165
|
-
(prev, next) =>
|
|
166
|
-
prev.rank === next.rank &&
|
|
167
|
-
prev.onPress === next.onPress &&
|
|
168
|
-
prev.row.key === next.row.key &&
|
|
169
|
-
prev.row.windowMs === next.row.windowMs &&
|
|
170
|
-
prev.row.calls === next.row.calls &&
|
|
171
|
-
prev.row.maxMs === next.row.maxMs &&
|
|
172
|
-
prev.row.totalMs === next.row.totalMs &&
|
|
173
|
-
prev.row.pctOfBusy === next.row.pctOfBusy &&
|
|
174
|
-
prev.row.freezes === next.row.freezes &&
|
|
175
|
-
prev.row.worstFreezeMs === next.row.worstFreezeMs,
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
export function TaskTable({
|
|
179
|
-
rows,
|
|
180
|
-
onSelectRow,
|
|
181
|
-
}: {
|
|
182
|
-
rows: JsTopRow[];
|
|
183
|
-
/** Tap-through to the origin detail view. Rows are static when absent. */
|
|
184
|
-
onSelectRow?: (row: JsTopRow) => void;
|
|
185
|
-
}) {
|
|
186
|
-
const [sortKey, setSortKey] = useState<SortKey>("windowMs");
|
|
187
|
-
|
|
188
|
-
const sorted =
|
|
189
|
-
sortKey === "windowMs"
|
|
190
|
-
? rows
|
|
191
|
-
: [...rows].sort((a, b) => b[sortKey] - a[sortKey]);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<View style={styles.root}>
|
|
195
|
-
<View style={styles.sortRow}>
|
|
196
|
-
<Text style={styles.sortLabel}>SORT</Text>
|
|
197
|
-
{SORT_OPTIONS.map((o) => (
|
|
198
|
-
<TouchableOpacity
|
|
199
|
-
key={o.key}
|
|
200
|
-
onPress={() => setSortKey(o.key)}
|
|
201
|
-
style={[styles.sortChip, sortKey === o.key && styles.sortChipOn]}
|
|
202
|
-
>
|
|
203
|
-
<Text
|
|
204
|
-
style={[
|
|
205
|
-
styles.sortChipText,
|
|
206
|
-
sortKey === o.key && styles.sortChipTextOn,
|
|
207
|
-
]}
|
|
208
|
-
>
|
|
209
|
-
{o.title}
|
|
210
|
-
</Text>
|
|
211
|
-
</TouchableOpacity>
|
|
212
|
-
))}
|
|
213
|
-
</View>
|
|
214
|
-
<ScrollView style={styles.body}>
|
|
215
|
-
{sorted.map((row, i) => (
|
|
216
|
-
<TaskRow key={row.key} row={row} rank={i + 1} onPress={onSelectRow} />
|
|
217
|
-
))}
|
|
218
|
-
{sorted.length === 0 && (
|
|
219
|
-
<Text style={styles.empty}>
|
|
220
|
-
No tasks observed yet — interact with the app.
|
|
221
|
-
</Text>
|
|
222
|
-
)}
|
|
223
|
-
</ScrollView>
|
|
224
|
-
</View>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const styles = StyleSheet.create({
|
|
229
|
-
root: { flex: 1, minHeight: 0 },
|
|
230
|
-
sortRow: {
|
|
231
|
-
flexDirection: "row",
|
|
232
|
-
alignItems: "center",
|
|
233
|
-
gap: 6,
|
|
234
|
-
paddingHorizontal: 10,
|
|
235
|
-
paddingVertical: 6,
|
|
236
|
-
borderBottomWidth: 1,
|
|
237
|
-
borderBottomColor: night.color.border,
|
|
238
|
-
backgroundColor: night.color.surface,
|
|
239
|
-
},
|
|
240
|
-
sortLabel: {
|
|
241
|
-
color: night.color.textTertiary,
|
|
242
|
-
fontSize: 9,
|
|
243
|
-
fontWeight: "700",
|
|
244
|
-
letterSpacing: 0.5,
|
|
245
|
-
marginRight: 2,
|
|
246
|
-
},
|
|
247
|
-
sortChip: {
|
|
248
|
-
borderWidth: 1,
|
|
249
|
-
borderColor: night.color.border,
|
|
250
|
-
borderRadius: 4,
|
|
251
|
-
paddingHorizontal: 7,
|
|
252
|
-
paddingVertical: 3,
|
|
253
|
-
},
|
|
254
|
-
sortChipOn: {
|
|
255
|
-
borderColor: night.color.accentBorderStrong,
|
|
256
|
-
backgroundColor: night.color.accentSoft,
|
|
257
|
-
},
|
|
258
|
-
sortChipText: {
|
|
259
|
-
color: night.color.textTertiary,
|
|
260
|
-
fontSize: 9,
|
|
261
|
-
fontWeight: "600",
|
|
262
|
-
fontVariant: ["tabular-nums"],
|
|
263
|
-
},
|
|
264
|
-
sortChipTextOn: { color: night.color.accent },
|
|
265
|
-
body: { flex: 1 },
|
|
266
|
-
empty: {
|
|
267
|
-
color: night.color.textTertiary,
|
|
268
|
-
fontSize: 12,
|
|
269
|
-
textAlign: "center",
|
|
270
|
-
paddingVertical: 24,
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const rowStyles = StyleSheet.create({
|
|
275
|
-
row: {
|
|
276
|
-
paddingHorizontal: 10,
|
|
277
|
-
paddingVertical: 8,
|
|
278
|
-
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
279
|
-
borderBottomColor: night.color.border,
|
|
280
|
-
gap: 5,
|
|
281
|
-
},
|
|
282
|
-
topLine: {
|
|
283
|
-
flexDirection: "row",
|
|
284
|
-
alignItems: "center",
|
|
285
|
-
gap: 6,
|
|
286
|
-
},
|
|
287
|
-
rank: {
|
|
288
|
-
width: 16,
|
|
289
|
-
color: night.color.textTertiary,
|
|
290
|
-
fontSize: 10,
|
|
291
|
-
fontVariant: ["tabular-nums"],
|
|
292
|
-
},
|
|
293
|
-
badge: {
|
|
294
|
-
borderWidth: 1,
|
|
295
|
-
borderRadius: 3,
|
|
296
|
-
paddingHorizontal: 4,
|
|
297
|
-
paddingVertical: 1,
|
|
298
|
-
},
|
|
299
|
-
badgeText: {
|
|
300
|
-
fontSize: 8,
|
|
301
|
-
fontWeight: "700",
|
|
302
|
-
letterSpacing: 0.4,
|
|
303
|
-
},
|
|
304
|
-
name: {
|
|
305
|
-
flex: 1,
|
|
306
|
-
color: night.color.text,
|
|
307
|
-
fontSize: 13,
|
|
308
|
-
fontWeight: "500",
|
|
309
|
-
},
|
|
310
|
-
freezeBadge: {
|
|
311
|
-
color: macOSColors.semantic.warning,
|
|
312
|
-
fontSize: 10,
|
|
313
|
-
fontWeight: "700",
|
|
314
|
-
fontVariant: ["tabular-nums"],
|
|
315
|
-
},
|
|
316
|
-
freezeBadgeSevere: {
|
|
317
|
-
color: macOSColors.semantic.error,
|
|
318
|
-
},
|
|
319
|
-
windowMs: {
|
|
320
|
-
color: night.color.text,
|
|
321
|
-
fontSize: 13,
|
|
322
|
-
fontWeight: "700",
|
|
323
|
-
fontVariant: ["tabular-nums"],
|
|
324
|
-
},
|
|
325
|
-
unit: {
|
|
326
|
-
color: night.color.textTertiary,
|
|
327
|
-
fontSize: 10,
|
|
328
|
-
fontWeight: "400",
|
|
329
|
-
},
|
|
330
|
-
pct: {
|
|
331
|
-
width: 38,
|
|
332
|
-
textAlign: "right",
|
|
333
|
-
color: night.color.textSecondary,
|
|
334
|
-
fontSize: 12,
|
|
335
|
-
fontWeight: "600",
|
|
336
|
-
fontVariant: ["tabular-nums"],
|
|
337
|
-
},
|
|
338
|
-
pctMuted: {
|
|
339
|
-
color: night.color.textTertiary,
|
|
340
|
-
},
|
|
341
|
-
subLine: {
|
|
342
|
-
flexDirection: "row",
|
|
343
|
-
alignItems: "center",
|
|
344
|
-
gap: 8,
|
|
345
|
-
paddingLeft: 22,
|
|
346
|
-
},
|
|
347
|
-
barTrack: {
|
|
348
|
-
width: 72,
|
|
349
|
-
height: 4,
|
|
350
|
-
borderRadius: 2,
|
|
351
|
-
backgroundColor: night.color.fillSecondary,
|
|
352
|
-
overflow: "hidden",
|
|
353
|
-
},
|
|
354
|
-
barFill: {
|
|
355
|
-
height: 4,
|
|
356
|
-
borderRadius: 2,
|
|
357
|
-
opacity: 0.85,
|
|
358
|
-
},
|
|
359
|
-
stats: {
|
|
360
|
-
flex: 1,
|
|
361
|
-
color: night.color.textTertiary,
|
|
362
|
-
fontSize: 10,
|
|
363
|
-
fontVariant: ["tabular-nums"],
|
|
364
|
-
},
|
|
365
|
-
});
|