@three-flatland/devtools 0.1.0-alpha.3 → 1.0.0-alpha.6
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 +1 -1
- package/dist/buffers-modal.d.ts +16 -52
- package/dist/buffers-modal.d.ts.map +1 -0
- package/dist/buffers-modal.js +459 -464
- package/dist/buffers-modal.js.map +1 -1
- package/dist/buffers-view.d.ts +22 -34
- package/dist/buffers-view.d.ts.map +1 -0
- package/dist/buffers-view.js +252 -240
- package/dist/buffers-view.js.map +1 -1
- package/dist/create-pane.d.ts +38 -37
- package/dist/create-pane.d.ts.map +1 -0
- package/dist/create-pane.js +197 -166
- package/dist/create-pane.js.map +1 -1
- package/dist/dashboard/app.tsx +2 -0
- package/dist/dashboard/export.ts +1 -3
- package/dist/dashboard/frame-cursor.ts +81 -0
- package/dist/dashboard/hooks.ts +10 -2
- package/dist/dashboard/index.html +61 -0
- package/dist/dashboard/panels/batches.tsx +5 -18
- package/dist/dashboard/panels/buffers.tsx +52 -26
- package/dist/dashboard/panels/env.tsx +5 -6
- package/dist/dashboard/panels/protocol-log.tsx +146 -58
- package/dist/dashboard/panels/registry.tsx +55 -69
- package/dist/dashboard/panels/scrubber.tsx +100 -0
- package/dist/dashboard/panels/stats.tsx +60 -32
- package/dist/dashboard/protocol-store.ts +25 -8
- package/dist/dashboard/vendor/hooks.module.d.ts +146 -0
- package/dist/dashboard/vendor/jsx-runtime.d.ts +55 -0
- package/dist/dashboard/vendor/jsx.d.ts +3123 -0
- package/dist/dashboard/vendor/preact.module.d.ts +397 -0
- package/dist/devtools-client.d.ts +312 -300
- package/dist/devtools-client.d.ts.map +1 -0
- package/dist/devtools-client.js +866 -838
- package/dist/devtools-client.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.js +5 -12
- package/dist/perf-trace.d.ts +6 -4
- package/dist/perf-trace.d.ts.map +1 -0
- package/dist/perf-trace.js +80 -66
- package/dist/perf-trace.js.map +1 -1
- package/dist/plugins.d.ts +6 -6
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +5 -5
- package/dist/plugins.js.map +1 -1
- package/dist/provider-switcher.d.ts +8 -15
- package/dist/provider-switcher.d.ts.map +1 -0
- package/dist/provider-switcher.js +68 -68
- package/dist/provider-switcher.js.map +1 -1
- package/dist/react/devtools-provider.d.ts +11 -10
- package/dist/react/devtools-provider.d.ts.map +1 -0
- package/dist/react/devtools-provider.js +60 -32
- package/dist/react/devtools-provider.js.map +1 -1
- package/dist/react/use-fps-graph.d.ts +7 -7
- package/dist/react/use-fps-graph.d.ts.map +1 -0
- package/dist/react/use-fps-graph.js +27 -21
- package/dist/react/use-fps-graph.js.map +1 -1
- package/dist/react/use-pane-button.d.ts +4 -4
- package/dist/react/use-pane-button.d.ts.map +1 -0
- package/dist/react/use-pane-button.js +35 -30
- package/dist/react/use-pane-button.js.map +1 -1
- package/dist/react/use-pane-folder.d.ts +5 -4
- package/dist/react/use-pane-folder.d.ts.map +1 -0
- package/dist/react/use-pane-folder.js +33 -20
- package/dist/react/use-pane-folder.js.map +1 -1
- package/dist/react/use-pane-input.d.ts +31 -30
- package/dist/react/use-pane-input.d.ts.map +1 -0
- package/dist/react/use-pane-input.js +46 -42
- package/dist/react/use-pane-input.js.map +1 -1
- package/dist/react/use-pane-radio-grid.d.ts +20 -20
- package/dist/react/use-pane-radio-grid.d.ts.map +1 -0
- package/dist/react/use-pane-radio-grid.js +72 -50
- package/dist/react/use-pane-radio-grid.js.map +1 -1
- package/dist/react/use-pane.d.ts +4 -4
- package/dist/react/use-pane.d.ts.map +1 -0
- package/dist/react/use-pane.js +52 -17
- package/dist/react/use-pane.js.map +1 -1
- package/dist/react.d.ts +10 -12
- package/dist/react.js +2 -12
- package/dist/registry-view.d.ts +8 -21
- package/dist/registry-view.d.ts.map +1 -0
- package/dist/registry-view.js +357 -338
- package/dist/registry-view.js.map +1 -1
- package/dist/relay.d.ts +16 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +189 -0
- package/dist/relay.js.map +1 -0
- package/dist/remote.d.ts +10 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +28 -0
- package/dist/remote.js.map +1 -0
- package/dist/stats-graph.d.ts +23 -31
- package/dist/stats-graph.d.ts.map +1 -0
- package/dist/stats-graph.js +323 -264
- package/dist/stats-graph.js.map +1 -1
- package/dist/stats-row.d.ts +8 -21
- package/dist/stats-row.d.ts.map +1 -0
- package/dist/stats-row.js +99 -101
- package/dist/stats-row.js.map +1 -1
- package/dist/theme.d.ts +3 -1
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +43 -54
- package/dist/theme.js.map +1 -1
- package/dist/vite-plugin.d.ts +7 -6
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +87 -78
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +33 -29
- package/dist/buffers-modal.cjs +0 -491
- package/dist/buffers-modal.cjs.map +0 -1
- package/dist/buffers-modal.d.cts +0 -62
- package/dist/buffers-view.cjs +0 -269
- package/dist/buffers-view.cjs.map +0 -1
- package/dist/buffers-view.d.cts +0 -39
- package/dist/create-pane.cjs +0 -203
- package/dist/create-pane.cjs.map +0 -1
- package/dist/create-pane.d.cts +0 -66
- package/dist/devtools-client.cjs +0 -870
- package/dist/devtools-client.cjs.map +0 -1
- package/dist/devtools-client.d.cts +0 -362
- package/dist/index.cjs +0 -42
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -8
- package/dist/index.js.map +0 -1
- package/dist/perf-trace.cjs +0 -103
- package/dist/perf-trace.cjs.map +0 -1
- package/dist/perf-trace.d.cts +0 -27
- package/dist/plugins.cjs +0 -44
- package/dist/plugins.cjs.map +0 -1
- package/dist/plugins.d.cts +0 -7
- package/dist/provider-switcher.cjs +0 -95
- package/dist/provider-switcher.cjs.map +0 -1
- package/dist/provider-switcher.d.cts +0 -18
- package/dist/react/devtools-provider.cjs +0 -59
- package/dist/react/devtools-provider.cjs.map +0 -1
- package/dist/react/devtools-provider.d.cts +0 -39
- package/dist/react/use-fps-graph.cjs +0 -49
- package/dist/react/use-fps-graph.cjs.map +0 -1
- package/dist/react/use-fps-graph.d.cts +0 -15
- package/dist/react/use-pane-button.cjs +0 -58
- package/dist/react/use-pane-button.cjs.map +0 -1
- package/dist/react/use-pane-button.d.cts +0 -10
- package/dist/react/use-pane-folder.cjs +0 -48
- package/dist/react/use-pane-folder.cjs.map +0 -1
- package/dist/react/use-pane-folder.d.cts +0 -15
- package/dist/react/use-pane-input.cjs +0 -70
- package/dist/react/use-pane-input.cjs.map +0 -1
- package/dist/react/use-pane-input.d.cts +0 -44
- package/dist/react/use-pane-radio-grid.cjs +0 -78
- package/dist/react/use-pane-radio-grid.cjs.map +0 -1
- package/dist/react/use-pane-radio-grid.d.cts +0 -35
- package/dist/react/use-pane.cjs +0 -45
- package/dist/react/use-pane.cjs.map +0 -1
- package/dist/react/use-pane.d.cts +0 -40
- package/dist/react.cjs +0 -50
- package/dist/react.cjs.map +0 -1
- package/dist/react.d.cts +0 -12
- package/dist/react.js.map +0 -1
- package/dist/registry-view.cjs +0 -371
- package/dist/registry-view.cjs.map +0 -1
- package/dist/registry-view.d.cts +0 -24
- package/dist/stats-graph.cjs +0 -297
- package/dist/stats-graph.cjs.map +0 -1
- package/dist/stats-graph.d.cts +0 -40
- package/dist/stats-row.cjs +0 -153
- package/dist/stats-row.cjs.map +0 -1
- package/dist/stats-row.d.cts +0 -24
- package/dist/theme.cjs +0 -260
- package/dist/theme.cjs.map +0 -1
- package/dist/theme.d.cts +0 -10
- package/dist/vite-plugin.cjs +0 -113
- package/dist/vite-plugin.cjs.map +0 -1
- package/dist/vite-plugin.d.cts +0 -9
|
@@ -28,6 +28,7 @@ import type { DebugMessage } from 'three-flatland/debug-protocol'
|
|
|
28
28
|
import { getClient } from '../client.js'
|
|
29
29
|
import { exportSession } from '../export.js'
|
|
30
30
|
import { useDevtoolsState } from '../hooks.js'
|
|
31
|
+
import { getFrameCursor, setFrameCursor } from '../frame-cursor.js'
|
|
31
32
|
import { getProtocolStore, type LogEntry } from '../protocol-store.js'
|
|
32
33
|
|
|
33
34
|
const ROW_HEIGHT = 22
|
|
@@ -64,8 +65,12 @@ export function ProtocolLog() {
|
|
|
64
65
|
const [hoveredId, setHoveredId] = useState<number | null>(null)
|
|
65
66
|
|
|
66
67
|
const [tail, setTail] = useState(true)
|
|
68
|
+
// Time-travel cursor (#29 Phase A): parked wins over tail — the log
|
|
69
|
+
// stops following new entries while the user inspects a past frame.
|
|
70
|
+
const frameCursor = getFrameCursor()
|
|
71
|
+
const effectiveTail = tail && frameCursor === null
|
|
67
72
|
const tailRef = useRef(true)
|
|
68
|
-
tailRef.current =
|
|
73
|
+
tailRef.current = effectiveTail
|
|
69
74
|
|
|
70
75
|
const scrollerRef = useRef<HTMLDivElement | null>(null)
|
|
71
76
|
const filterInputRef = useRef<HTMLInputElement | null>(null)
|
|
@@ -75,6 +80,62 @@ export function ProtocolLog() {
|
|
|
75
80
|
const pendingBumpRef = useRef(0)
|
|
76
81
|
const [viewport, setViewport] = useState({ scrollTop: 0, height: 0 })
|
|
77
82
|
|
|
83
|
+
// Parked cursor → scroll to the row whose frame is closest ≤ the
|
|
84
|
+
// cursor (nearest-frame semantics: data batches at ~4 Hz, engine
|
|
85
|
+
// frames at 60 Hz — most frames have no exact row). Only rows in the
|
|
86
|
+
// tail cache are scanned; deeper history scrolls in as it hydrates.
|
|
87
|
+
const parkedCursorRef = useRef<{ providerId: string | null; frame: number | null }>({
|
|
88
|
+
providerId: null,
|
|
89
|
+
frame: null,
|
|
90
|
+
})
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
// Key the parked cursor by provider AND frame: switching producers while
|
|
93
|
+
// both are parked on the same frame number must still re-scroll.
|
|
94
|
+
const sameCursor =
|
|
95
|
+
parkedCursorRef.current.providerId === activeProviderId && parkedCursorRef.current.frame === frameCursor
|
|
96
|
+
if (frameCursor === null || sameCursor) {
|
|
97
|
+
parkedCursorRef.current = { providerId: activeProviderId, frame: frameCursor }
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
parkedCursorRef.current = { providerId: activeProviderId, frame: frameCursor }
|
|
101
|
+
if (activeProviderId === null) return
|
|
102
|
+
const { total, ids } = store.statsFor(activeProviderId)
|
|
103
|
+
let matched = false
|
|
104
|
+
for (let i = 0; i < total; i++) {
|
|
105
|
+
const id = ids[total - 1 - i]
|
|
106
|
+
if (id === undefined) continue
|
|
107
|
+
const entry = store.peek(activeProviderId, id)
|
|
108
|
+
if (entry?.frame !== undefined && entry.frame <= frameCursor) {
|
|
109
|
+
const el = scrollerRef.current
|
|
110
|
+
if (el !== null) el.scrollTop = i * ROW_HEIGHT
|
|
111
|
+
matched = true
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!matched) {
|
|
116
|
+
// Older than the tail cache — locate the row through IDB, then
|
|
117
|
+
// scroll if the cursor hasn't moved on while we were reading.
|
|
118
|
+
const target = frameCursor
|
|
119
|
+
const provider = activeProviderId
|
|
120
|
+
void store
|
|
121
|
+
.queryFiltered(provider, (entry) => entry.frame !== undefined && entry.frame <= target)
|
|
122
|
+
.then((matchedIds) => {
|
|
123
|
+
if (parkedCursorRef.current.providerId !== provider || parkedCursorRef.current.frame !== target) return
|
|
124
|
+
const lastId = matchedIds[matchedIds.length - 1]
|
|
125
|
+
if (lastId === undefined) return
|
|
126
|
+
const { total: t, ids: allIds } = store.statsFor(provider)
|
|
127
|
+
for (let i = 0; i < t; i++) {
|
|
128
|
+
if (allIds[t - 1 - i] === lastId) {
|
|
129
|
+
const el = scrollerRef.current
|
|
130
|
+
if (el !== null) el.scrollTop = i * ROW_HEIGHT
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
+
}, [frameCursor, activeProviderId])
|
|
138
|
+
|
|
78
139
|
// Wire the raw-message tap → store. The store owns IDB persistence,
|
|
79
140
|
// counters, cache + eviction; this listener translates raw messages
|
|
80
141
|
// to log rows AND tags each with whichever provider was selected at
|
|
@@ -195,10 +256,7 @@ export function ProtocolLog() {
|
|
|
195
256
|
minBytes,
|
|
196
257
|
excluded: excludedTypes,
|
|
197
258
|
}
|
|
198
|
-
const hasActiveFilter = needle.length > 0
|
|
199
|
-
|| dirFilter !== null
|
|
200
|
-
|| minBytes > 0
|
|
201
|
-
|| excludedTypes.size > 0
|
|
259
|
+
const hasActiveFilter = needle.length > 0 || dirFilter !== null || minBytes > 0 || excludedTypes.size > 0
|
|
202
260
|
|
|
203
261
|
// Filter pipeline is authoritative — when a filter is active, we
|
|
204
262
|
// query IDB directly via the store's `queryFiltered` cursor. Query
|
|
@@ -223,23 +281,19 @@ export function ProtocolLog() {
|
|
|
223
281
|
const signal = { aborted: false }
|
|
224
282
|
const predicate = filterPredicateRef.current!
|
|
225
283
|
setFilterLoading(true)
|
|
226
|
-
const handle = (globalThis.setTimeout as unknown as (cb: () => void, ms: number) => number)(
|
|
227
|
-
() => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
234
|
-
150,
|
|
235
|
-
)
|
|
284
|
+
const handle = (globalThis.setTimeout as unknown as (cb: () => void, ms: number) => number)(() => {
|
|
285
|
+
void store.queryFiltered(activeProviderId, predicate, signal).then((result) => {
|
|
286
|
+
if (signal.aborted) return
|
|
287
|
+
setFilteredIds(result)
|
|
288
|
+
setFilterLoading(false)
|
|
289
|
+
})
|
|
290
|
+
}, 150)
|
|
236
291
|
return () => {
|
|
237
292
|
signal.aborted = true
|
|
238
293
|
clearTimeout(handle)
|
|
239
294
|
}
|
|
240
295
|
// Filter primitives as deps; `filterPredicateRef.current` is read
|
|
241
296
|
// inside but kept stable against renders via the ref.
|
|
242
|
-
|
|
243
297
|
}, [store, activeProviderId, hasActiveFilter, needle, dirFilter, minBytes, excludedTypes])
|
|
244
298
|
|
|
245
299
|
// Live-append: when a new message matches the active filter, extend
|
|
@@ -275,7 +329,10 @@ export function ProtocolLog() {
|
|
|
275
329
|
let visualIdx = 0
|
|
276
330
|
if (selectedId !== null) {
|
|
277
331
|
for (let i = 0; i < total; i++) {
|
|
278
|
-
if (ids[total - 1 - i] === selectedId) {
|
|
332
|
+
if (ids[total - 1 - i] === selectedId) {
|
|
333
|
+
visualIdx = i
|
|
334
|
+
break
|
|
335
|
+
}
|
|
279
336
|
}
|
|
280
337
|
}
|
|
281
338
|
const nextIdx = Math.max(0, Math.min(total - 1, visualIdx + delta))
|
|
@@ -294,7 +351,10 @@ export function ProtocolLog() {
|
|
|
294
351
|
}
|
|
295
352
|
const onKey = (e: KeyboardEvent) => {
|
|
296
353
|
if (e.key === 'Escape') {
|
|
297
|
-
if (selectedId !== null) {
|
|
354
|
+
if (selectedId !== null) {
|
|
355
|
+
setSelectedId(null)
|
|
356
|
+
e.preventDefault()
|
|
357
|
+
}
|
|
298
358
|
return
|
|
299
359
|
}
|
|
300
360
|
if (isTypingTarget(e.target)) return
|
|
@@ -325,8 +385,8 @@ export function ProtocolLog() {
|
|
|
325
385
|
|
|
326
386
|
// Window-scope prefetch (unfiltered case). When filter is active the
|
|
327
387
|
// bulk prefetch above covers it.
|
|
328
|
-
const startId = end > 0 && total > 0 ? ids[Math.max(0, total - end)] ?? 0 : 0
|
|
329
|
-
const endId = total > 0 ? ids[Math.max(0, total - 1 - start)] ?? 0 : 0
|
|
388
|
+
const startId = end > 0 && total > 0 ? (ids[Math.max(0, total - end)] ?? 0) : 0
|
|
389
|
+
const endId = total > 0 ? (ids[Math.max(0, total - 1 - start)] ?? 0) : 0
|
|
330
390
|
useEffect(() => {
|
|
331
391
|
if (hasActiveFilter) return
|
|
332
392
|
if (activeProviderId === null || startId === 0 || startId > endId) return
|
|
@@ -334,7 +394,10 @@ export function ProtocolLog() {
|
|
|
334
394
|
for (let i = start; i < end; i++) {
|
|
335
395
|
const id = ids[total - 1 - i]
|
|
336
396
|
if (id === undefined) continue
|
|
337
|
-
if (store.peek(activeProviderId, id) === null) {
|
|
397
|
+
if (store.peek(activeProviderId, id) === null) {
|
|
398
|
+
needsFetch = true
|
|
399
|
+
break
|
|
400
|
+
}
|
|
338
401
|
}
|
|
339
402
|
if (needsFetch) void store.prefetchRange(activeProviderId, startId, endId)
|
|
340
403
|
}, [store, activeProviderId, hasActiveFilter, startId, endId, start, end, total, ids])
|
|
@@ -351,16 +414,24 @@ export function ProtocolLog() {
|
|
|
351
414
|
// Selection follows the active provider. Swapping providers clears
|
|
352
415
|
// any cross-provider selection.
|
|
353
416
|
useEffect(() => {
|
|
354
|
-
if (selectedId === null || activeProviderId === null) {
|
|
417
|
+
if (selectedId === null || activeProviderId === null) {
|
|
418
|
+
setSelectedEntry(null)
|
|
419
|
+
return
|
|
420
|
+
}
|
|
355
421
|
const cached = store.peek(activeProviderId, selectedId)
|
|
356
|
-
if (cached !== null) {
|
|
422
|
+
if (cached !== null) {
|
|
423
|
+
setSelectedEntry(cached)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
357
426
|
let cancelled = false
|
|
358
427
|
void store.prefetchRange(activeProviderId, selectedId, selectedId).then(() => {
|
|
359
428
|
if (cancelled) return
|
|
360
429
|
const fetched = store.peek(activeProviderId, selectedId)
|
|
361
430
|
if (fetched !== null) setSelectedEntry(fetched)
|
|
362
431
|
})
|
|
363
|
-
return () => {
|
|
432
|
+
return () => {
|
|
433
|
+
cancelled = true
|
|
434
|
+
}
|
|
364
435
|
}, [store, activeProviderId, selectedId])
|
|
365
436
|
const selected = selectedEntry
|
|
366
437
|
|
|
@@ -418,11 +489,7 @@ export function ProtocolLog() {
|
|
|
418
489
|
<div class="protocol-filter-menu-wrap" ref={filterMenuRef}>
|
|
419
490
|
<button
|
|
420
491
|
type="button"
|
|
421
|
-
class={
|
|
422
|
-
(minBytes > 0 || excludedTypes.size > 0
|
|
423
|
-
? 'protocol-btn protocol-btn-on'
|
|
424
|
-
: 'protocol-btn')
|
|
425
|
-
}
|
|
492
|
+
class={minBytes > 0 || excludedTypes.size > 0 ? 'protocol-btn protocol-btn-on' : 'protocol-btn'}
|
|
426
493
|
onClick={() => setFilterMenuOpen((o) => !o)}
|
|
427
494
|
title="Type + size filters"
|
|
428
495
|
>
|
|
@@ -510,7 +577,17 @@ export function ProtocolLog() {
|
|
|
510
577
|
type="button"
|
|
511
578
|
class={cls}
|
|
512
579
|
style={{ transform: `translateY(${offsetTop}px)`, height: `${ROW_HEIGHT}px` }}
|
|
513
|
-
onClick={
|
|
580
|
+
onClick={
|
|
581
|
+
isPlaceholder
|
|
582
|
+
? undefined
|
|
583
|
+
: () => {
|
|
584
|
+
setSelectedId((x) => (x === id ? null : id))
|
|
585
|
+
// Entry point per the time-travel design:
|
|
586
|
+
// clicking a row parks the cursor at its frame.
|
|
587
|
+
const frame = e!.frame
|
|
588
|
+
if (frame !== undefined) setFrameCursor(frame)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
514
591
|
onMouseEnter={isPlaceholder ? undefined : () => setHoveredId(id)}
|
|
515
592
|
onMouseLeave={isPlaceholder ? undefined : () => setHoveredId((h) => (h === id ? null : h))}
|
|
516
593
|
title={isPlaceholder ? undefined : new Date(e!.at).toISOString()}
|
|
@@ -520,7 +597,7 @@ export function ProtocolLog() {
|
|
|
520
597
|
<span class="protocol-time">{isLoading ? 'loading…' : formatTime(e!.at)}</span>
|
|
521
598
|
<span class="protocol-frame">{!isPlaceholder && e!.frame !== undefined ? `#${e!.frame}` : ''}</span>
|
|
522
599
|
<span class="protocol-type">{isPlaceholder ? '' : e!.type}</span>
|
|
523
|
-
<span class="protocol-tag">{isPlaceholder ? '' : e!.tag ?? ''}</span>
|
|
600
|
+
<span class="protocol-tag">{isPlaceholder ? '' : (e!.tag ?? '')}</span>
|
|
524
601
|
<span class="protocol-size">{isPlaceholder ? '' : formatBytes(e!.bytes)}</span>
|
|
525
602
|
</button>
|
|
526
603
|
)
|
|
@@ -536,12 +613,9 @@ export function ProtocolLog() {
|
|
|
536
613
|
{selected.type}
|
|
537
614
|
{selected.tag !== undefined && <span class="protocol-tag"> · {selected.tag}</span>}
|
|
538
615
|
</span>
|
|
539
|
-
<button
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
onClick={() => setSelectedId(null)}
|
|
543
|
-
aria-label="Close detail"
|
|
544
|
-
>×</button>
|
|
616
|
+
<button type="button" class="protocol-btn" onClick={() => setSelectedId(null)} aria-label="Close detail">
|
|
617
|
+
×
|
|
618
|
+
</button>
|
|
545
619
|
</div>
|
|
546
620
|
<div class="protocol-detail-meta">
|
|
547
621
|
<span>{formatTime(selected.at)}</span>
|
|
@@ -612,7 +686,9 @@ function FilterMenu({
|
|
|
612
686
|
type="button"
|
|
613
687
|
class="protocol-btn protocol-filter-menu-clear"
|
|
614
688
|
onClick={() => setExcludedTypes(new Set())}
|
|
615
|
-
>
|
|
689
|
+
>
|
|
690
|
+
reset
|
|
691
|
+
</button>
|
|
616
692
|
)}
|
|
617
693
|
</div>
|
|
618
694
|
<ul class="protocol-filter-types">
|
|
@@ -622,11 +698,7 @@ function FilterMenu({
|
|
|
622
698
|
types.map((t) => (
|
|
623
699
|
<li key={t}>
|
|
624
700
|
<label class="protocol-filter-type">
|
|
625
|
-
<input
|
|
626
|
-
type="checkbox"
|
|
627
|
-
checked={!excludedTypes.has(t)}
|
|
628
|
-
onChange={() => toggle(t)}
|
|
629
|
-
/>
|
|
701
|
+
<input type="checkbox" checked={!excludedTypes.has(t)} onChange={() => toggle(t)} />
|
|
630
702
|
<span>{t}</span>
|
|
631
703
|
</label>
|
|
632
704
|
</li>
|
|
@@ -671,14 +743,29 @@ function estimateBytes(msg: DebugMessage): number {
|
|
|
671
743
|
const seen = new WeakSet<object>()
|
|
672
744
|
const walk = (v: unknown): void => {
|
|
673
745
|
if (v === null || v === undefined) return
|
|
674
|
-
if (typeof v === 'string') {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
746
|
+
if (typeof v === 'string') {
|
|
747
|
+
bytes += v.length
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
if (typeof v === 'number' || typeof v === 'boolean') {
|
|
751
|
+
bytes += 8
|
|
752
|
+
return
|
|
753
|
+
}
|
|
754
|
+
if (v instanceof ArrayBuffer) {
|
|
755
|
+
bytes += v.byteLength
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
if (ArrayBuffer.isView(v)) {
|
|
759
|
+
bytes += v.byteLength
|
|
760
|
+
return
|
|
761
|
+
}
|
|
678
762
|
if (typeof v === 'object') {
|
|
679
763
|
if (seen.has(v)) return
|
|
680
764
|
seen.add(v)
|
|
681
|
-
if (Array.isArray(v)) {
|
|
765
|
+
if (Array.isArray(v)) {
|
|
766
|
+
for (const x of v) walk(x)
|
|
767
|
+
return
|
|
768
|
+
}
|
|
682
769
|
for (const k in v as Record<string, unknown>) {
|
|
683
770
|
bytes += k.length
|
|
684
771
|
walk((v as Record<string, unknown>)[k])
|
|
@@ -703,11 +790,15 @@ function formatTime(at: number): string {
|
|
|
703
790
|
|
|
704
791
|
function stringify(msg: DebugMessage): string {
|
|
705
792
|
try {
|
|
706
|
-
return JSON.stringify(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
793
|
+
return JSON.stringify(
|
|
794
|
+
msg,
|
|
795
|
+
(_k, v) => {
|
|
796
|
+
if (v instanceof ArrayBuffer) return `[ArrayBuffer ${v.byteLength}B]`
|
|
797
|
+
if (ArrayBuffer.isView(v)) return `[${v.constructor.name} ${v.byteLength}B]`
|
|
798
|
+
return v as unknown
|
|
799
|
+
},
|
|
800
|
+
2
|
|
801
|
+
)
|
|
711
802
|
} catch {
|
|
712
803
|
return '[unserialisable]'
|
|
713
804
|
}
|
|
@@ -722,10 +813,7 @@ function stringify(msg: DebugMessage): string {
|
|
|
722
813
|
* arbitrary markup from message payloads.
|
|
723
814
|
*/
|
|
724
815
|
function highlightJson(json: string): string {
|
|
725
|
-
const escaped = json
|
|
726
|
-
.replace(/&/g, '&')
|
|
727
|
-
.replace(/</g, '<')
|
|
728
|
-
.replace(/>/g, '>')
|
|
816
|
+
const escaped = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
729
817
|
return escaped.replace(
|
|
730
818
|
/("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
|
|
731
819
|
(match) => {
|
|
@@ -738,6 +826,6 @@ function highlightJson(json: string): string {
|
|
|
738
826
|
cls = 'json-null'
|
|
739
827
|
}
|
|
740
828
|
return `<span class="${cls}">${match}</span>`
|
|
741
|
-
}
|
|
829
|
+
}
|
|
742
830
|
)
|
|
743
831
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
18
18
|
import type { RegistryEntrySnapshot } from '../../devtools-client.js'
|
|
19
19
|
import { getClient } from '../client.js'
|
|
20
|
+
import { getFrameCursor } from '../frame-cursor.js'
|
|
20
21
|
import { useDevtoolsState } from '../hooks.js'
|
|
21
22
|
|
|
22
23
|
const ROW_STRIDE = 8
|
|
@@ -43,7 +44,9 @@ export function RegistryPanel() {
|
|
|
43
44
|
// Opt into the unfiltered drain on mount; reset on unmount.
|
|
44
45
|
useEffect(() => {
|
|
45
46
|
client.setRegistry(null)
|
|
46
|
-
return () => {
|
|
47
|
+
return () => {
|
|
48
|
+
client.setRegistry([])
|
|
49
|
+
}
|
|
47
50
|
}, [client])
|
|
48
51
|
|
|
49
52
|
const entries = useMemo(() => {
|
|
@@ -53,15 +56,17 @@ export function RegistryPanel() {
|
|
|
53
56
|
}, [state.registry, state.registry.size])
|
|
54
57
|
|
|
55
58
|
const needle = filter.trim().toLowerCase()
|
|
56
|
-
const visible =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const visible =
|
|
60
|
+
needle.length > 0
|
|
61
|
+
? entries.filter(
|
|
62
|
+
(e) => e.name.toLowerCase().includes(needle) || (e.label?.toLowerCase().includes(needle) ?? false)
|
|
63
|
+
)
|
|
64
|
+
: entries
|
|
59
65
|
|
|
60
|
-
const effectiveSelected =
|
|
61
|
-
? selected
|
|
62
|
-
: (visible[0]?.name ?? null)
|
|
66
|
+
const effectiveSelected =
|
|
67
|
+
selected !== null && visible.some((e) => e.name === selected) ? selected : (visible[0]?.name ?? null)
|
|
63
68
|
|
|
64
|
-
const selectedEntry = effectiveSelected !== null ? state.registry.get(effectiveSelected) ?? null : null
|
|
69
|
+
const selectedEntry = effectiveSelected !== null ? (state.registry.get(effectiveSelected) ?? null) : null
|
|
65
70
|
|
|
66
71
|
return (
|
|
67
72
|
<section class="panel registry-panel">
|
|
@@ -76,6 +81,11 @@ export function RegistryPanel() {
|
|
|
76
81
|
/>
|
|
77
82
|
<span class="registry-count">{entries.length}</span>
|
|
78
83
|
</header>
|
|
84
|
+
{getFrameCursor() !== null ? (
|
|
85
|
+
<div class="registry-parked-note">
|
|
86
|
+
parked — showing the latest registry state (per-frame history needs the Phase C checkpoint snapshots)
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
79
89
|
<div class="registry-layout">
|
|
80
90
|
<ul class="registry-list">
|
|
81
91
|
{visible.length === 0 ? (
|
|
@@ -85,10 +95,7 @@ export function RegistryPanel() {
|
|
|
85
95
|
<li key={e.name}>
|
|
86
96
|
<button
|
|
87
97
|
type="button"
|
|
88
|
-
class={
|
|
89
|
-
'registry-row' +
|
|
90
|
-
(e.name === effectiveSelected ? ' registry-row-selected' : '')
|
|
91
|
-
}
|
|
98
|
+
class={'registry-row' + (e.name === effectiveSelected ? ' registry-row-selected' : '')}
|
|
92
99
|
onClick={() => setSelected(e.name)}
|
|
93
100
|
>
|
|
94
101
|
<span class="registry-kind">{e.kind}</span>
|
|
@@ -139,13 +146,7 @@ function RegistrySparkline({
|
|
|
139
146
|
if (ctx === null) return
|
|
140
147
|
drawVisualizer(ctx, w, h, entry)
|
|
141
148
|
}, [entry.version, entry.name, width, height])
|
|
142
|
-
return
|
|
143
|
-
<canvas
|
|
144
|
-
class="registry-sparkline"
|
|
145
|
-
ref={canvasRef}
|
|
146
|
-
style={{ width: `${width}px`, height: `${height}px` }}
|
|
147
|
-
/>
|
|
148
|
-
)
|
|
149
|
+
return <canvas class="registry-sparkline" ref={canvasRef} style={{ width: `${width}px`, height: `${height}px` }} />
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
function RegistryDetail({ entry }: { entry: RegistryEntrySnapshot }): preact.JSX.Element {
|
|
@@ -171,11 +172,7 @@ function RegistryDetail({ entry }: { entry: RegistryEntrySnapshot }): preact.JSX
|
|
|
171
172
|
const scrollGridToAxisIdx = (idx: number): void => {
|
|
172
173
|
const el = gridScrollerRef.current
|
|
173
174
|
if (el === null) return
|
|
174
|
-
const perAxisIdx =
|
|
175
|
-
entry.kind === 'float2' ? 2 :
|
|
176
|
-
entry.kind === 'float3' ? 3 :
|
|
177
|
-
entry.kind === 'float4' ? 4 :
|
|
178
|
-
1
|
|
175
|
+
const perAxisIdx = entry.kind === 'float2' ? 2 : entry.kind === 'float3' ? 3 : entry.kind === 'float4' ? 4 : 1
|
|
179
176
|
const scalarIdx = entry.kind === 'bits' ? idx : idx * perAxisIdx
|
|
180
177
|
const rowIdx = Math.floor(scalarIdx / ROW_STRIDE)
|
|
181
178
|
const top = rowIdx * SAMPLE_ROW_HEIGHT
|
|
@@ -270,9 +267,7 @@ function RegistryDetail({ entry }: { entry: RegistryEntrySnapshot }): preact.JSX
|
|
|
270
267
|
<span>v{version}</span>
|
|
271
268
|
<span>{sample.constructor.name}</span>
|
|
272
269
|
{stats.summary !== '' && <span>{stats.summary}</span>}
|
|
273
|
-
{cursorIdx !== null &&
|
|
274
|
-
<span class="registry-cursor-label">cursor {cursorIdx}</span>
|
|
275
|
-
)}
|
|
270
|
+
{cursorIdx !== null && <span class="registry-cursor-label">cursor {cursorIdx}</span>}
|
|
276
271
|
</div>
|
|
277
272
|
<div class="registry-viz-wrap">
|
|
278
273
|
<div
|
|
@@ -289,19 +284,19 @@ function RegistryDetail({ entry }: { entry: RegistryEntrySnapshot }): preact.JSX
|
|
|
289
284
|
</div>
|
|
290
285
|
</div>
|
|
291
286
|
<div class="registry-detail-body">
|
|
292
|
-
{sample.length === 0
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
287
|
+
{sample.length === 0 ? (
|
|
288
|
+
<div class="panel-empty">No sample yet.</div>
|
|
289
|
+
) : (
|
|
290
|
+
<SampleGrid
|
|
291
|
+
entry={entry}
|
|
292
|
+
stride={ROW_STRIDE}
|
|
293
|
+
float={isFloat}
|
|
294
|
+
pinnedIdx={pinnedIdx}
|
|
295
|
+
scrollerRef={gridScrollerRef}
|
|
296
|
+
onPinIdx={(idx) => setPinnedIdx(idx)}
|
|
297
|
+
onScrollIdx={(idx) => setPinnedIdx(idx)}
|
|
298
|
+
/>
|
|
299
|
+
)}
|
|
305
300
|
</div>
|
|
306
301
|
</>
|
|
307
302
|
)
|
|
@@ -328,11 +323,7 @@ function SampleGrid({
|
|
|
328
323
|
onScrollIdx: (i: number) => void
|
|
329
324
|
}): preact.JSX.Element {
|
|
330
325
|
const { kind, sample } = entry
|
|
331
|
-
const scalarsPerAxisIdx =
|
|
332
|
-
kind === 'float2' ? 2 :
|
|
333
|
-
kind === 'float3' ? 3 :
|
|
334
|
-
kind === 'float4' ? 4 :
|
|
335
|
-
1
|
|
326
|
+
const scalarsPerAxisIdx = kind === 'float2' ? 2 : kind === 'float3' ? 3 : kind === 'float4' ? 4 : 1
|
|
336
327
|
const cursorIdx = pinnedIdx
|
|
337
328
|
|
|
338
329
|
const [viewport, setViewport] = useState({ scrollTop: 0, height: 0 })
|
|
@@ -343,7 +334,7 @@ function SampleGrid({
|
|
|
343
334
|
setViewport((v) => ({ ...v, height: el.clientHeight }))
|
|
344
335
|
const obs = new ResizeObserver(() => {
|
|
345
336
|
const h = scrollerRef.current?.clientHeight ?? 0
|
|
346
|
-
setViewport((v) => v.height === h ? v : { ...v, height: h })
|
|
337
|
+
setViewport((v) => (v.height === h ? v : { ...v, height: h }))
|
|
347
338
|
})
|
|
348
339
|
obs.observe(el)
|
|
349
340
|
return () => obs.disconnect()
|
|
@@ -359,9 +350,7 @@ function SampleGrid({
|
|
|
359
350
|
if (height === 0) return
|
|
360
351
|
const centerRow = Math.floor((scrollTop + height / 2) / SAMPLE_ROW_HEIGHT)
|
|
361
352
|
const centerScalarIdx = centerRow * stride
|
|
362
|
-
const axisIdx = kind === 'bits'
|
|
363
|
-
? centerScalarIdx * 32
|
|
364
|
-
: Math.floor(centerScalarIdx / scalarsPerAxisIdx)
|
|
353
|
+
const axisIdx = kind === 'bits' ? centerScalarIdx * 32 : Math.floor(centerScalarIdx / scalarsPerAxisIdx)
|
|
365
354
|
if (axisIdx >= 0) onScrollIdx(axisIdx)
|
|
366
355
|
}
|
|
367
356
|
|
|
@@ -377,14 +366,12 @@ function SampleGrid({
|
|
|
377
366
|
for (let j = 0; j < stride && i + j < sample.length; j++) {
|
|
378
367
|
const scalarIdx = i + j
|
|
379
368
|
const v = sample[scalarIdx]!
|
|
380
|
-
const axisIdxForCell = kind === 'bits'
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
kind === 'bits'
|
|
369
|
+
const axisIdxForCell = kind === 'bits' ? scalarIdx * 32 : Math.floor(scalarIdx / scalarsPerAxisIdx)
|
|
370
|
+
const inCursor =
|
|
371
|
+
cursorIdx !== null &&
|
|
372
|
+
(kind === 'bits'
|
|
385
373
|
? cursorIdx >= axisIdxForCell && cursorIdx < axisIdxForCell + 32
|
|
386
|
-
: axisIdxForCell === cursorIdx
|
|
387
|
-
)
|
|
374
|
+
: axisIdxForCell === cursorIdx)
|
|
388
375
|
cells.push(
|
|
389
376
|
<span
|
|
390
377
|
class={`sample-cell${inCursor ? ' sample-cell-cursor' : ''}`}
|
|
@@ -392,7 +379,7 @@ function SampleGrid({
|
|
|
392
379
|
onClick={() => onPinIdx(axisIdxForCell)}
|
|
393
380
|
>
|
|
394
381
|
{float ? fmtFloatFixed(v) : v.toString()}
|
|
395
|
-
</span
|
|
382
|
+
</span>
|
|
396
383
|
)
|
|
397
384
|
}
|
|
398
385
|
rows.push(
|
|
@@ -406,13 +393,13 @@ function SampleGrid({
|
|
|
406
393
|
>
|
|
407
394
|
<span class="sample-index">{i}</span>
|
|
408
395
|
{cells}
|
|
409
|
-
</div
|
|
396
|
+
</div>
|
|
410
397
|
)
|
|
411
398
|
}
|
|
412
399
|
|
|
413
400
|
const onScroll = (e: Event): void => {
|
|
414
401
|
const el = e.currentTarget as HTMLDivElement
|
|
415
|
-
setViewport((v) => v.scrollTop === el.scrollTop ? v : { ...v, scrollTop: el.scrollTop })
|
|
402
|
+
setViewport((v) => (v.scrollTop === el.scrollTop ? v : { ...v, scrollTop: el.scrollTop }))
|
|
416
403
|
reportScrollIdx(el.scrollTop, el.clientHeight)
|
|
417
404
|
}
|
|
418
405
|
|
|
@@ -458,7 +445,10 @@ function computeStats(entry: RegistryEntrySnapshot): { summary: string } {
|
|
|
458
445
|
let ones = 0
|
|
459
446
|
for (let i = 0; i < data.length; i++) {
|
|
460
447
|
let w = data[i]! >>> 0
|
|
461
|
-
while (w !== 0) {
|
|
448
|
+
while (w !== 0) {
|
|
449
|
+
ones += w & 1
|
|
450
|
+
w >>>= 1
|
|
451
|
+
}
|
|
462
452
|
}
|
|
463
453
|
return { summary: `${ones} / ${count * 32} set` }
|
|
464
454
|
}
|
|
@@ -485,9 +475,7 @@ function computeStats(entry: RegistryEntrySnapshot): { summary: string } {
|
|
|
485
475
|
sum += v
|
|
486
476
|
}
|
|
487
477
|
const mean = sum / n
|
|
488
|
-
const f = (v: number) => kind === 'uint' || kind === 'int'
|
|
489
|
-
? Math.round(v).toString()
|
|
490
|
-
: v.toFixed(2)
|
|
478
|
+
const f = (v: number) => (kind === 'uint' || kind === 'int' ? Math.round(v).toString() : v.toFixed(2))
|
|
491
479
|
return { summary: `μ=${f(mean)} ${f(min)}–${f(max)}` }
|
|
492
480
|
}
|
|
493
481
|
|
|
@@ -502,7 +490,7 @@ function drawVisualizer(
|
|
|
502
490
|
w: number,
|
|
503
491
|
h: number,
|
|
504
492
|
entry: RegistryEntrySnapshot,
|
|
505
|
-
cursorIdx: number | null = null
|
|
493
|
+
cursorIdx: number | null = null
|
|
506
494
|
): void {
|
|
507
495
|
ctx.clearRect(0, 0, w, h)
|
|
508
496
|
const { kind, sample } = entry
|
|
@@ -551,7 +539,7 @@ function drawBars(
|
|
|
551
539
|
h: number,
|
|
552
540
|
sample: Float32Array | Int32Array | Uint32Array,
|
|
553
541
|
isSigned: boolean,
|
|
554
|
-
color: string
|
|
542
|
+
color: string
|
|
555
543
|
): void {
|
|
556
544
|
let min = Infinity
|
|
557
545
|
let max = -Infinity
|
|
@@ -564,9 +552,7 @@ function drawBars(
|
|
|
564
552
|
if (!Number.isFinite(min) || !Number.isFinite(max)) return
|
|
565
553
|
|
|
566
554
|
const zeroY = isSigned ? h / 2 : h
|
|
567
|
-
const scale = isSigned
|
|
568
|
-
? (h / 2) / Math.max(Math.abs(min), Math.abs(max) || 1)
|
|
569
|
-
: h / (max || 1)
|
|
555
|
+
const scale = isSigned ? h / 2 / Math.max(Math.abs(min), Math.abs(max) || 1) : h / (max || 1)
|
|
570
556
|
|
|
571
557
|
ctx.fillStyle = color
|
|
572
558
|
|
|
@@ -623,7 +609,7 @@ function drawBits(
|
|
|
623
609
|
h: number,
|
|
624
610
|
sample: Uint32Array,
|
|
625
611
|
count: number,
|
|
626
|
-
color: string
|
|
612
|
+
color: string
|
|
627
613
|
): void {
|
|
628
614
|
const totalBits = count * 32
|
|
629
615
|
if (totalBits === 0) return
|