@playfast/reform-profiler 1.0.1 → 1.0.2
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 +1 -1
- package/src/index.ts +0 -5
- package/src/noop.ts +1 -6
- package/src/profileScene.ts +1 -18
- package/src/profiler.test.ts +0 -8
- package/src/react/index.ts +0 -4
- package/src/react/noop.ts +0 -3
- package/src/react/overlay.tsx +1 -16
- package/src/react/styles.ts +0 -3
- package/src/recorder.ts +1 -31
- package/src/registry.ts +3 -20
- package/src/report.ts +0 -7
- package/src/trace.ts +0 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-profiler",
|
|
3
3
|
"playbook": "./playbook",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Performance profiler for reform scenes — a recording Instrumentation, Chrome DevTools trace export, terminal reports, and a React overlay. Dev builds get the real recorder; prod builds resolve to no-op stubs.",
|
|
7
7
|
"keywords": [
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// @playfast/reform-profiler — the REAL entry, resolved under the `development`
|
|
2
|
-
// export condition (and unconditionally via `./recorder`, for tests and
|
|
3
|
-
// headless scripts whose runner doesn't set dev conditions). Production
|
|
4
|
-
// bundles resolve `.` to `./noop.ts` instead.
|
|
5
|
-
|
|
6
1
|
export {
|
|
7
2
|
makeProfiler,
|
|
8
3
|
type ProfileKind,
|
package/src/noop.ts
CHANGED
|
@@ -8,11 +8,7 @@ import type {
|
|
|
8
8
|
import type { ProfilerRegistration } from './registry'
|
|
9
9
|
import type { ChromeTrace } from './trace'
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
// `default` export condition. Same surface as `./index.ts`, zero recording:
|
|
13
|
-
// `profileScene` hands the scene back untouched, every reader yields empty
|
|
14
|
-
// data, and none of the recorder/trace/report machinery is reachable from a
|
|
15
|
-
// prod bundle. Types re-export from the real modules (erased at runtime).
|
|
11
|
+
// Production stub (`default` export condition): same surface, zero recording.
|
|
16
12
|
|
|
17
13
|
export type {
|
|
18
14
|
ProfileKind,
|
|
@@ -39,7 +35,6 @@ const emptySnapshot: ProfileSnapshot = {
|
|
|
39
35
|
dropped: 0,
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
/** The shared inert handle every stub call hands back. */
|
|
43
38
|
export const noopProfiler: ProfilerHandle = {
|
|
44
39
|
...noopInstrumentation,
|
|
45
40
|
label: 'noop',
|
package/src/profileScene.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { profiledScene, type Scene, type UiContract } from '@playfast/reform'
|
|
|
3
3
|
import { makeProfiler, type ProfilerHandle, type ProfilerOptionsExternalApi } from './recorder'
|
|
4
4
|
import { registerProfiler } from './registry'
|
|
5
5
|
|
|
6
|
-
/** A scene wired to record into `profiler`, plus the handle to read it back. */
|
|
7
6
|
export interface ProfiledScene<
|
|
8
7
|
C extends UiContract = UiContract,
|
|
9
8
|
S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
@@ -12,21 +11,6 @@ export interface ProfiledScene<
|
|
|
12
11
|
readonly profiler: ProfilerHandle
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Opt a CLOSED scene into profiling: every engine hook (events, reducers,
|
|
17
|
-
* states, calcs, queries, procedures) records into a fresh profiler, and hosts
|
|
18
|
-
* (React `Compose`, the proof engine) pick it up for render spans via
|
|
19
|
-
* `sceneInstrumentation`. The original scene is untouched — hand the returned
|
|
20
|
-
* `scene` to `<Reform/>`, `drive`, or `makeAppRuntime`.
|
|
21
|
-
*
|
|
22
|
-
* The profiler registers in the overlay registry while a runtime built from
|
|
23
|
-
* the scene is live (register on layer build, unregister on scope close), so
|
|
24
|
-
* `<ReformProfiler/>` shows one tab per live profiled runtime.
|
|
25
|
-
*
|
|
26
|
-
* In a production build (`default` export condition) this module is replaced
|
|
27
|
-
* by a passthrough stub — the scene comes back untouched and the handle is a
|
|
28
|
-
* shared no-op.
|
|
29
|
-
*/
|
|
30
14
|
export const profileScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
31
15
|
base: Scene<C, S>,
|
|
32
16
|
options: ProfilerOptionsExternalApi = {},
|
|
@@ -43,8 +27,7 @@ export const profileScene = <C extends UiContract, S extends ReadonlyArray<unkno
|
|
|
43
27
|
)
|
|
44
28
|
const wrapped = profiledScene(base, profiler)
|
|
45
29
|
return {
|
|
46
|
-
//
|
|
47
|
-
// services), so registration follows the runtime's real build/dispose.
|
|
30
|
+
// Lifecycle on first provide layer so register/unregister track runtime build/dispose.
|
|
48
31
|
scene: {
|
|
49
32
|
...wrapped,
|
|
50
33
|
provide: wrapped.provide.map((layer, index) =>
|
package/src/profiler.test.ts
CHANGED
|
@@ -24,8 +24,6 @@ import { formatReport } from './report'
|
|
|
24
24
|
|
|
25
25
|
const settle = (ms = 20) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
26
26
|
|
|
27
|
-
// ── recorder ──────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
27
|
test('aggregates count and time per name, per kind', () => {
|
|
30
28
|
const profiler = makeProfiler({ label: 'agg' })
|
|
31
29
|
profiler.eventDispatched('Bumped', 'High')
|
|
@@ -53,7 +51,6 @@ test('the ring buffer wraps: newest records survive, aggregates keep the full ta
|
|
|
53
51
|
const snapshot = profiler.snapshot()
|
|
54
52
|
expect(snapshot.records.map((record) => record.name)).toEqual(['c', 'd', 'e', 'f'])
|
|
55
53
|
expect(snapshot.dropped).toBe(2)
|
|
56
|
-
// Evicted records still count in the aggregates.
|
|
57
54
|
expect(snapshot.events.size).toBe(6)
|
|
58
55
|
expect(snapshot.events.get('a')?.count).toBe(1)
|
|
59
56
|
})
|
|
@@ -87,8 +84,6 @@ test('subscribers are notified once per burst (coalesced)', async () => {
|
|
|
87
84
|
expect(calls.count).toBe(1)
|
|
88
85
|
})
|
|
89
86
|
|
|
90
|
-
// ── trace export ──────────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
87
|
test('toChromeTrace emits metadata lanes, X spans, and i instants', () => {
|
|
93
88
|
const profiler = makeProfiler({ label: 'traced' })
|
|
94
89
|
profiler.eventDispatched('Bumped', 'High')
|
|
@@ -112,8 +107,6 @@ test('toChromeTrace emits metadata lanes, X spans, and i instants', () => {
|
|
|
112
107
|
expect(parsed).toEqual(trace)
|
|
113
108
|
})
|
|
114
109
|
|
|
115
|
-
// ── profileScene end-to-end over the headless app runtime ────────────────────
|
|
116
|
-
|
|
117
110
|
class Count extends State.make('profiler-count', S.Number) {}
|
|
118
111
|
class States extends StateGroup.make(Count) {}
|
|
119
112
|
class Bumped extends Event.make('ProfilerBumped', S.Struct({})) {}
|
|
@@ -168,7 +161,6 @@ test('profileScene records engine activity through makeAppRuntime and registers
|
|
|
168
161
|
await settle()
|
|
169
162
|
expect(listProfilers().map((entry) => entry.handle)).not.toContain(profiler)
|
|
170
163
|
|
|
171
|
-
// The base scene is untouched: no instrumentation carrier, nothing recorded.
|
|
172
164
|
const before = profiler.snapshot().events.get('ProfilerBumped')?.count
|
|
173
165
|
const plain = makeAppRuntime(baseScene)
|
|
174
166
|
plain.dispatch('High', Event.construct(Bumped, {}))
|
package/src/react/index.ts
CHANGED
|
@@ -1,5 +1 @@
|
|
|
1
|
-
// @playfast/reform-profiler/react — dev entry (`development` export condition).
|
|
2
|
-
// The floating profiler panel plus the browser trace download. The prod build
|
|
3
|
-
// resolves `./noop.ts` instead, so none of this reaches a production bundle.
|
|
4
|
-
|
|
5
1
|
export { downloadTrace, ReformProfiler } from './overlay'
|
package/src/react/noop.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import type { ProfilerHandle } from '../recorder'
|
|
3
3
|
|
|
4
|
-
// @playfast/reform-profiler/react — the PRODUCTION stub (`default` export
|
|
5
|
-
// condition). `<ReformProfiler/>` renders nothing; `downloadTrace` is inert.
|
|
6
|
-
|
|
7
4
|
export const ReformProfiler = (): ReactNode => null
|
|
8
5
|
|
|
9
6
|
export const downloadTrace = (_profiler: ProfilerHandle): void => {}
|
package/src/react/overlay.tsx
CHANGED
|
@@ -24,12 +24,7 @@ import {
|
|
|
24
24
|
tabStyle,
|
|
25
25
|
} from './styles'
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
// built on reform: the profiler must not profile itself. One floating panel per
|
|
29
|
-
// page (singleton election through the registry), one tab per live profiled
|
|
30
|
-
// runtime, per-kind aggregate tables, Chrome-trace export, clear.
|
|
31
|
-
|
|
32
|
-
/** Save the profiler's Chrome DevTools trace as a `.json` download (browser only). */
|
|
27
|
+
// Plain React (not reform) so the profiler does not profile itself.
|
|
33
28
|
export const downloadTrace = (profiler: ProfilerHandle): void => {
|
|
34
29
|
const blob = new Blob([traceJson(profiler)], { type: 'application/json' })
|
|
35
30
|
const url = URL.createObjectURL(blob)
|
|
@@ -40,8 +35,6 @@ export const downloadTrace = (profiler: ProfilerHandle): void => {
|
|
|
40
35
|
URL.revokeObjectURL(url)
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
// ── data shaping ─────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
38
|
interface Row {
|
|
46
39
|
readonly name: string
|
|
47
40
|
readonly stat: ProfileStat
|
|
@@ -62,7 +55,6 @@ const avgMs = (stat: ProfileStat): string =>
|
|
|
62
55
|
interface SectionProps {
|
|
63
56
|
readonly title: string
|
|
64
57
|
readonly rows: ReadonlyArray<Row>
|
|
65
|
-
/** Timed sections get total/avg columns; instant sections only a count. */
|
|
66
58
|
readonly timed: boolean
|
|
67
59
|
}
|
|
68
60
|
|
|
@@ -137,13 +129,6 @@ const ProfilerBody = ({ profiler }: ProfilerBodyProps): ReactNode => {
|
|
|
137
129
|
)
|
|
138
130
|
}
|
|
139
131
|
|
|
140
|
-
// ── the overlay ──────────────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* The profiler panel. Render it once anywhere in the page (outside `<Reform>` —
|
|
144
|
-
* it is not a reform composition); it shows one tab per live `profileScene`d
|
|
145
|
-
* runtime. Extra instances render nothing until the elected one unmounts.
|
|
146
|
-
*/
|
|
147
132
|
export const ReformProfiler = (): ReactNode => {
|
|
148
133
|
const owner = useRef({}).current
|
|
149
134
|
const [claimed, setClaimed] = useState(false)
|
package/src/react/styles.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
// Inline styles for the overlay panel — the profiler carries no CSS dependency
|
|
4
|
-
// and must not disturb the host page (fixed position, own stacking context).
|
|
5
|
-
|
|
6
3
|
interface Palette {
|
|
7
4
|
readonly bg: string
|
|
8
5
|
readonly panel: string
|
package/src/recorder.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { type Instrumentation, makeScheduler, type SpanEnd } from '@playfast/reform'
|
|
2
2
|
|
|
3
|
-
// The recording Instrumentation. One profiler per profiled scene: every engine
|
|
4
|
-
// hook appends a fixed-shape record into a ring buffer and folds it into a
|
|
5
|
-
// per-name aggregate, then coalesces subscriber notifications through core's
|
|
6
|
-
// microtask scheduler — so the overlay re-renders once per burst, not per event.
|
|
7
|
-
// All hot-path work is synchronous and allocation-light (one record object, one
|
|
8
|
-
// aggregate fold); the buffer is fixed-size so a long session cannot grow
|
|
9
|
-
// memory unboundedly (old records are evicted, aggregates keep the full tally).
|
|
10
|
-
|
|
11
|
-
/** What a record measures. `event`/`state` are instants; the rest are spans. */
|
|
12
3
|
export type ProfileKind =
|
|
13
4
|
| 'event'
|
|
14
5
|
| 'reducer'
|
|
@@ -21,23 +12,17 @@ export type ProfileKind =
|
|
|
21
12
|
|
|
22
13
|
export interface ProfileRecord {
|
|
23
14
|
readonly kind: ProfileKind
|
|
24
|
-
/** The definition name — event tag, reducer/calc/query/procedure/composition name. */
|
|
25
15
|
readonly name: string
|
|
26
|
-
/** Context: the trigger tag, `channel:tag`, priority, or frame event count. Empty when none. */
|
|
27
16
|
readonly detail: string
|
|
28
|
-
/** Start in ms since the page/process time origin (`performance.now()`). */
|
|
29
17
|
readonly start: number
|
|
30
|
-
/** Duration in ms; `0` for instant records. */
|
|
31
18
|
readonly dur: number
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
/** Aggregate for one name within a kind. Instant kinds carry `totalMs: 0`. */
|
|
35
21
|
export interface ProfileStat {
|
|
36
22
|
readonly count: number
|
|
37
23
|
readonly totalMs: number
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
/** A consistent copy of everything recorded so far. */
|
|
41
26
|
export interface ProfileSnapshot {
|
|
42
27
|
readonly events: ReadonlyMap<string, ProfileStat>
|
|
43
28
|
readonly reducers: ReadonlyMap<string, ProfileStat>
|
|
@@ -47,33 +32,20 @@ export interface ProfileSnapshot {
|
|
|
47
32
|
readonly procedures: ReadonlyMap<string, ProfileStat>
|
|
48
33
|
readonly renders: ReadonlyMap<string, ProfileStat>
|
|
49
34
|
readonly frames: ProfileStat
|
|
50
|
-
/** Raw records, oldest → newest, at most `capacity` of them. */
|
|
51
35
|
readonly records: ReadonlyArray<ProfileRecord>
|
|
52
|
-
/** Records evicted by the ring buffer (aggregates still include them). */
|
|
53
36
|
readonly dropped: number
|
|
54
37
|
}
|
|
55
38
|
|
|
56
|
-
// Boundary config object: optional fields are the public, JSON-like surface
|
|
57
|
-
// (`ExternalApi` postfix exempts them from `no-optional-fields`).
|
|
58
39
|
export interface ProfilerOptionsExternalApi {
|
|
59
|
-
/** Tab title in the overlay / report heading. Defaults to the scene's composition title. */
|
|
60
40
|
readonly label?: string
|
|
61
|
-
/** Ring-buffer size in records (default 50,000). Aggregates are unaffected. */
|
|
62
41
|
readonly capacity?: number
|
|
63
42
|
}
|
|
64
43
|
|
|
65
|
-
/**
|
|
66
|
-
* The live profiler: a recording {@link Instrumentation} plus the read side the
|
|
67
|
-
* overlay, trace export, and reports consume. `subscribe`/`getVersion` follow
|
|
68
|
-
* the `useSyncExternalStore` contract (notifications microtask-coalesced).
|
|
69
|
-
*/
|
|
70
44
|
export interface ProfilerHandle extends Instrumentation {
|
|
71
45
|
readonly label: string
|
|
72
46
|
readonly snapshot: () => ProfileSnapshot
|
|
73
|
-
/** Drop all records and aggregates (the overlay's Clear button). */
|
|
74
47
|
readonly clear: () => void
|
|
75
48
|
readonly subscribe: (listener: () => void) => () => void
|
|
76
|
-
/** Monotone change counter — bumped once per recorded entry and per `clear`. */
|
|
77
49
|
readonly getVersion: () => number
|
|
78
50
|
}
|
|
79
51
|
|
|
@@ -84,8 +56,7 @@ const DEFAULT_CAPACITY = 50_000
|
|
|
84
56
|
export const makeProfiler = (options: ProfilerOptionsExternalApi = {}): ProfilerHandle => {
|
|
85
57
|
const capacity = Math.max(1, options.capacity ?? DEFAULT_CAPACITY)
|
|
86
58
|
const label = options.label ?? 'reform'
|
|
87
|
-
//
|
|
88
|
-
// is the next slot). In-place-mutated latch object, like the hosts' `status`.
|
|
59
|
+
// Fixed ring: written % capacity is next slot; aggregates keep full tally when records evict.
|
|
89
60
|
const emptyBuffer = (): Array<ProfileRecord | undefined> =>
|
|
90
61
|
Array.from({ length: capacity }, (): ProfileRecord | undefined => undefined)
|
|
91
62
|
const cursor = { buffer: emptyBuffer(), written: 0, version: 0 }
|
|
@@ -168,7 +139,6 @@ export const makeProfiler = (options: ProfilerOptionsExternalApi = {}): Profiler
|
|
|
168
139
|
},
|
|
169
140
|
getVersion: () => cursor.version,
|
|
170
141
|
|
|
171
|
-
// ── Instrumentation ────────────────────────────────────────────────────
|
|
172
142
|
eventDispatched: (tag, priority) => instant({ kind: 'event', name: tag, detail: priority }),
|
|
173
143
|
reducerRun: (name, eventTag) => span({ kind: 'reducer', name, detail: eventTag }),
|
|
174
144
|
stateUpdated: (name) => instant({ kind: 'state', name, detail: '' }),
|
package/src/registry.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { GlobalValue, Option } from 'effect'
|
|
2
2
|
import type { ProfilerHandle } from './recorder'
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
// The overlay reads it to render one tab per profiled runtime; `profileScene`
|
|
6
|
-
// registers on layer build and unregisters when the runtime's scope closes.
|
|
7
|
-
// Kept in a `globalValue` so duplicated bundles / HMR share one registry.
|
|
4
|
+
// globalValue so duplicated bundles / HMR share one registry.
|
|
8
5
|
|
|
9
6
|
export interface ProfilerRegistration {
|
|
10
7
|
readonly label: string
|
|
@@ -12,11 +9,10 @@ export interface ProfilerRegistration {
|
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
interface RegistryState {
|
|
15
|
-
|
|
12
|
+
// Refcount: StrictMode double-build shares one entry.
|
|
16
13
|
readonly entries: Map<ProfilerHandle, { readonly registration: ProfilerRegistration; readonly count: number }>
|
|
17
14
|
readonly listeners: Set<() => void>
|
|
18
15
|
readonly version: { current: number }
|
|
19
|
-
/** The overlay instance currently elected to render the panel. */
|
|
20
16
|
readonly overlay: { owner: Option.Option<object> }
|
|
21
17
|
}
|
|
22
18
|
|
|
@@ -35,11 +31,6 @@ const notify = (): void => {
|
|
|
35
31
|
;[...state.listeners].forEach((listener) => listener())
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
/**
|
|
39
|
-
* Register a live profiler. Refcounted by handle: the same profiler built twice
|
|
40
|
-
* (React StrictMode) stays one tab, and disappears when the LAST build closes.
|
|
41
|
-
* Returns the matching unregister.
|
|
42
|
-
*/
|
|
43
34
|
export const registerProfiler = (registration: ProfilerRegistration): (() => void) => {
|
|
44
35
|
const existing = state.entries.get(registration.handle)
|
|
45
36
|
state.entries.set(registration.handle, {
|
|
@@ -63,11 +54,9 @@ export const registerProfiler = (registration: ProfilerRegistration): (() => voi
|
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
/** The live registrations, in registration order. */
|
|
67
57
|
export const listProfilers = (): ReadonlyArray<ProfilerRegistration> =>
|
|
68
58
|
[...state.entries.values()].map((entry) => entry.registration)
|
|
69
59
|
|
|
70
|
-
/** Subscribe to registry changes (register/unregister). Sync, rare. */
|
|
71
60
|
export const subscribeProfilers = (listener: () => void): (() => void) => {
|
|
72
61
|
state.listeners.add(listener)
|
|
73
62
|
return () => {
|
|
@@ -75,14 +64,9 @@ export const subscribeProfilers = (listener: () => void): (() => void) => {
|
|
|
75
64
|
}
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
/** Monotone registry change counter (`useSyncExternalStore` snapshot). */
|
|
79
67
|
export const profilersVersion = (): number => state.version.current
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
* Singleton election for the overlay panel: the first mounted `<ReformProfiler/>`
|
|
83
|
-
* claims rendering; later instances render nothing. Returns true when `owner`
|
|
84
|
-
* holds the claim (idempotent for the current claimant).
|
|
85
|
-
*/
|
|
69
|
+
// First mounted overlay claims render; later instances no-op until claim released.
|
|
86
70
|
export const claimOverlay = (owner: object): boolean => {
|
|
87
71
|
if (Option.isNone(state.overlay.owner)) {
|
|
88
72
|
state.overlay.owner = Option.some(owner)
|
|
@@ -91,7 +75,6 @@ export const claimOverlay = (owner: object): boolean => {
|
|
|
91
75
|
return Option.contains(state.overlay.owner, owner)
|
|
92
76
|
}
|
|
93
77
|
|
|
94
|
-
/** Release the panel claim so the next mounted instance can take over. */
|
|
95
78
|
export const releaseOverlay = (owner: object): void => {
|
|
96
79
|
if (Option.contains(state.overlay.owner, owner)) {
|
|
97
80
|
state.overlay.owner = Option.none()
|
package/src/report.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { ProfilerHandle, ProfileStat } from './recorder'
|
|
2
2
|
|
|
3
|
-
// Terminal report — the overlay's tables as aligned plain text, so a headless
|
|
4
|
-
// drive script can `console.log(formatReport(profiler))` after a scenario.
|
|
5
|
-
|
|
6
3
|
interface Row {
|
|
7
4
|
readonly name: string
|
|
8
5
|
readonly stat: ProfileStat
|
|
@@ -18,8 +15,6 @@ const sortedRows = (stats: ReadonlyMap<string, ProfileStat>): ReadonlyArray<Row>
|
|
|
18
15
|
const MS_DECIMALS = 2
|
|
19
16
|
const formatMs = (durationMs: number): string => durationMs.toFixed(MS_DECIMALS)
|
|
20
17
|
|
|
21
|
-
// Fixed column widths: fit the 'count'/'total ms'/'avg ms' headers plus room
|
|
22
|
-
// for six digits; the name column stretches to the longest name per section.
|
|
23
18
|
const COUNT_WIDTH = 7
|
|
24
19
|
const TOTAL_WIDTH = 9
|
|
25
20
|
const AVG_WIDTH = 8
|
|
@@ -28,7 +23,6 @@ const ruleOverhead = '── '.length + ' '.length
|
|
|
28
23
|
interface Section {
|
|
29
24
|
readonly title: string
|
|
30
25
|
readonly rows: ReadonlyArray<Row>
|
|
31
|
-
/** Timed sections get total/avg columns; instant sections only a count. */
|
|
32
26
|
readonly timed: boolean
|
|
33
27
|
}
|
|
34
28
|
|
|
@@ -53,7 +47,6 @@ const section = ({ title, rows, timed }: Section): ReadonlyArray<string> => {
|
|
|
53
47
|
]
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
/** The profile so far as aligned text tables, one section per non-empty kind. */
|
|
57
50
|
export const formatReport = (profiler: ProfilerHandle): string => {
|
|
58
51
|
const snapshot = profiler.snapshot()
|
|
59
52
|
const frameRows: ReadonlyArray<Row> =
|
package/src/trace.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { ProfileKind, ProfilerHandle } from './recorder'
|
|
2
2
|
|
|
3
|
-
// Chrome DevTools trace export — the Trace Event Format (`ph:'X'` complete
|
|
4
|
-
// events on named thread lanes, `ph:'i'` instants, `ph:'M'` metadata). The
|
|
5
|
-
// output loads in the Chrome DevTools Performance panel ("Load profile…") and
|
|
6
|
-
// in Perfetto. Platform-neutral: headless scripts `Bun.write('trace.json',
|
|
7
|
-
// traceJson(profiler))`; the browser download button lives in the react entry.
|
|
8
|
-
|
|
9
|
-
/** One Trace Event Format entry. Times are µs. */
|
|
10
3
|
export type ChromeTraceEvent =
|
|
11
4
|
| {
|
|
12
5
|
readonly ph: 'X'
|
|
@@ -42,7 +35,6 @@ export interface ChromeTrace {
|
|
|
42
35
|
readonly displayTimeUnit: 'ms'
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
// One thread lane per kind, so the Performance panel shows parallel tracks.
|
|
46
38
|
const lanes: Record<ProfileKind, { readonly tid: number; readonly title: string }> = {
|
|
47
39
|
frame: { tid: 1, title: 'frames' },
|
|
48
40
|
event: { tid: 2, title: 'events' },
|
|
@@ -59,7 +51,6 @@ const instantKinds: ReadonlySet<ProfileKind> = new Set(['event', 'state'])
|
|
|
59
51
|
const pid = 1
|
|
60
52
|
const US_PER_MS = 1000
|
|
61
53
|
|
|
62
|
-
/** Project the profiler's raw records into a Chrome DevTools trace object. */
|
|
63
54
|
export const toChromeTrace = (profiler: ProfilerHandle): ChromeTrace => {
|
|
64
55
|
const { records } = profiler.snapshot()
|
|
65
56
|
const meta: ReadonlyArray<ChromeTraceEvent> = [
|
|
@@ -87,7 +78,6 @@ export const toChromeTrace = (profiler: ProfilerHandle): ChromeTrace => {
|
|
|
87
78
|
return { traceEvents: [...meta, ...body], displayTimeUnit: 'ms' }
|
|
88
79
|
}
|
|
89
80
|
|
|
90
|
-
/** `toChromeTrace` as a JSON string, ready to write to a `.json` file. */
|
|
91
81
|
export const traceJson = (profiler: ProfilerHandle): string =>
|
|
92
82
|
// oxlint-disable-next-line reform-rules/no-json-parse-stringify -- Trace Event Format is a foreign wire format consumed by Chrome DevTools/Perfetto, not Schema-typed data
|
|
93
83
|
JSON.stringify(toChromeTrace(profiler))
|