@playfast/reform-profiler 1.0.1 → 1.1.0

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 CHANGED
@@ -1,25 +1,28 @@
1
1
  {
2
2
  "name": "@playfast/reform-profiler",
3
- "playbook": "./playbook",
4
- "version": "1.0.1",
5
- "type": "module",
3
+ "version": "1.1.0",
6
4
  "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
5
  "keywords": [
8
- "reform",
6
+ "devtools",
9
7
  "effect",
10
- "profiler",
11
8
  "performance",
12
- "devtools"
9
+ "profiler",
10
+ "reform"
13
11
  ],
12
+ "bugs": {
13
+ "url": "https://github.com/playfast/reform/issues"
14
+ },
14
15
  "license": "MIT",
15
16
  "repository": {
16
17
  "type": "git",
17
18
  "url": "https://github.com/playfast/reform.git",
18
19
  "directory": "packages/reform-profiler"
19
20
  },
20
- "bugs": {
21
- "url": "https://github.com/playfast/reform/issues"
22
- },
21
+ "files": [
22
+ "src",
23
+ "README.md"
24
+ ],
25
+ "type": "module",
23
26
  "sideEffects": false,
24
27
  "exports": {
25
28
  "./package.json": "./package.json",
@@ -33,10 +36,9 @@
33
36
  },
34
37
  "./recorder": "./src/index.ts"
35
38
  },
36
- "files": [
37
- "src",
38
- "README.md"
39
- ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
40
42
  "scripts": {
41
43
  "clean": "rm -rf dist .tsbuildinfo",
42
44
  "check": "tsc --noEmit",
@@ -48,8 +50,8 @@
48
50
  "lint:fix": "oxlint --fix src"
49
51
  },
50
52
  "peerDependencies": {
51
- "effect": "*",
52
53
  "@playfast/reform": "*",
54
+ "effect": "*",
53
55
  "react": "^19.0.0"
54
56
  },
55
57
  "peerDependenciesMeta": {
@@ -57,7 +59,5 @@
57
59
  "optional": true
58
60
  }
59
61
  },
60
- "publishConfig": {
61
- "access": "public"
62
- }
62
+ "playbook": "./playbook"
63
63
  }
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,
@@ -12,7 +7,7 @@ export {
12
7
  type ProfileSnapshot,
13
8
  type ProfileStat,
14
9
  } from './recorder'
15
- export { type ProfiledScene, profileScene } from './profileScene'
10
+ export { type CapturedProfiledScene, type ProfiledScene, profileScene } from './profileScene'
16
11
  export { type ChromeTrace, type ChromeTraceEvent, toChromeTrace, traceJson } from './trace'
17
12
  export { formatReport } from './report'
18
13
  export {
package/src/noop.ts CHANGED
@@ -1,18 +1,11 @@
1
- import { noopInstrumentation, type Scene, type UiContract } from '@playfast/reform'
2
- import type { ProfiledScene } from './profileScene'
3
- import type {
4
- ProfilerHandle,
5
- ProfilerOptionsExternalApi,
6
- ProfileSnapshot,
7
- } from './recorder'
1
+ import { type CapturedScene, type Scene, type UiContract } from '@playfast/reform'
2
+ import { noopInstrumentation } from '@playfast/reform/internal'
3
+ import type { CapturedProfiledScene, ProfiledScene } from './profileScene'
4
+ import type { ProfilerHandle, ProfilerOptionsExternalApi, ProfileSnapshot } from './recorder'
8
5
  import type { ProfilerRegistration } from './registry'
9
6
  import type { ChromeTrace } from './trace'
10
7
 
11
- // @playfast/reform-profiler the PRODUCTION stub, resolved under the
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).
8
+ // Production stub (`default` export condition): same surface, zero recording.
16
9
 
17
10
  export type {
18
11
  ProfileKind,
@@ -22,7 +15,7 @@ export type {
22
15
  ProfileSnapshot,
23
16
  ProfileStat,
24
17
  } from './recorder'
25
- export type { ProfiledScene } from './profileScene'
18
+ export type { CapturedProfiledScene, ProfiledScene } from './profileScene'
26
19
  export type { ChromeTrace, ChromeTraceEvent } from './trace'
27
20
  export type { ProfilerRegistration } from './registry'
28
21
 
@@ -39,7 +32,6 @@ const emptySnapshot: ProfileSnapshot = {
39
32
  dropped: 0,
40
33
  }
41
34
 
42
- /** The shared inert handle every stub call hands back. */
43
35
  export const noopProfiler: ProfilerHandle = {
44
36
  ...noopInstrumentation,
45
37
  label: 'noop',
@@ -52,10 +44,45 @@ export const noopProfiler: ProfilerHandle = {
52
44
  export const makeProfiler = (_options: ProfilerOptionsExternalApi = {}): ProfilerHandle =>
53
45
  noopProfiler
54
46
 
55
- export const profileScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
47
+ const profileCapturedScene = <
48
+ C extends UiContract,
49
+ S extends ReadonlyArray<unknown>,
50
+ Services,
51
+ P,
52
+ N extends string,
53
+ Identity,
54
+ >(
55
+ base: CapturedScene<C, S, Services, P, N, Identity>,
56
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity> => ({
57
+ scene: base,
58
+ profiler: noopProfiler,
59
+ })
60
+
61
+ export function profileScene<
62
+ C extends UiContract,
63
+ S extends ReadonlyArray<unknown>,
64
+ Services,
65
+ P,
66
+ N extends string,
67
+ Identity,
68
+ >(
69
+ base: CapturedScene<C, S, Services, P, N, Identity>,
70
+ _options?: ProfilerOptionsExternalApi,
71
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity>
72
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
73
+ base: Scene<C, S>,
74
+ _options?: ProfilerOptionsExternalApi,
75
+ ): ProfiledScene<C, S>
76
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
56
77
  base: Scene<C, S>,
57
78
  _options: ProfilerOptionsExternalApi = {},
58
- ): ProfiledScene<C, S> => ({ scene: base, profiler: noopProfiler })
79
+ ): ProfiledScene<C, S> {
80
+ return base.capture<ProfiledScene<C, S>>(
81
+ <Services, P, N extends string, Identity>(
82
+ exactScene: CapturedScene<C, S, Services, P, N, Identity>,
83
+ ): ProfiledScene<C, S> => profileCapturedScene(exactScene),
84
+ )
85
+ }
59
86
 
60
87
  const emptyTrace: ChromeTrace = { traceEvents: [], displayTimeUnit: 'ms' }
61
88
 
@@ -66,11 +93,15 @@ export const traceJson = (_profiler: ProfilerHandle): string =>
66
93
 
67
94
  export const formatReport = (_profiler: ProfilerHandle): string => ''
68
95
 
69
- export const registerProfiler = (_registration: ProfilerRegistration): (() => void) => () => {}
96
+ export const registerProfiler =
97
+ (_registration: ProfilerRegistration): (() => void) =>
98
+ () => {}
70
99
 
71
100
  export const listProfilers = (): ReadonlyArray<ProfilerRegistration> => []
72
101
 
73
- export const subscribeProfilers = (_listener: () => void): (() => void) => () => {}
102
+ export const subscribeProfilers =
103
+ (_listener: () => void): (() => void) =>
104
+ () => {}
74
105
 
75
106
  export const profilersVersion = (): number => 0
76
107
 
@@ -1,9 +1,9 @@
1
1
  import { Effect, Layer } from 'effect'
2
- import { profiledScene, type Scene, type UiContract } from '@playfast/reform'
2
+ import { type CapturedScene, profiledScene, type Scene, type UiContract } from '@playfast/reform'
3
+ import { type AnySceneCapture, type SceneCapture } from '@playfast/reform/internal'
3
4
  import { makeProfiler, type ProfilerHandle, type ProfilerOptionsExternalApi } from './recorder'
4
5
  import { registerProfiler } from './registry'
5
6
 
6
- /** A scene wired to record into `profiler`, plus the handle to read it back. */
7
7
  export interface ProfiledScene<
8
8
  C extends UiContract = UiContract,
9
9
  S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
@@ -12,45 +12,94 @@ export interface ProfiledScene<
12
12
  readonly profiler: ProfilerHandle
13
13
  }
14
14
 
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
- export const profileScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
15
+ export interface CapturedProfiledScene<
16
+ C extends UiContract,
17
+ S extends ReadonlyArray<unknown>,
18
+ Services,
19
+ P,
20
+ N extends string,
21
+ Identity,
22
+ > extends ProfiledScene<C, S> {
23
+ readonly scene: CapturedScene<C, S, Services, P, N, Identity>
24
+ }
25
+
26
+ const makeLifecycle = (profiler: ProfilerHandle) =>
27
+ Layer.scopedDiscard(
28
+ Effect.acquireRelease(
29
+ Effect.sync(() => registerProfiler({ label: profiler.label, handle: profiler })),
30
+ (unregister) => Effect.sync(unregister),
31
+ ),
32
+ )
33
+
34
+ const withLifecycle = <
35
+ C extends UiContract,
36
+ S extends ReadonlyArray<unknown>,
37
+ Services,
38
+ P,
39
+ N extends string,
40
+ Identity,
41
+ >(
42
+ base: CapturedScene<C, S, Services, P, N, Identity>,
43
+ lifecycle: ReturnType<typeof makeLifecycle>,
44
+ ): CapturedScene<C, S, Services, P, N, Identity> => {
45
+ const captured: CapturedScene<C, S, Services, P, N, Identity> = {
46
+ kind: 'Scene',
47
+ composition: base.composition,
48
+ // Lifecycle on the first provide layer tracks the runtime's build/dispose scope.
49
+ provide: base.provide.map((layer, index) =>
50
+ index === 0 ? Layer.merge(layer, lifecycle) : layer,
51
+ ),
52
+ ...(base.boot === undefined ? {} : { boot: base.boot }),
53
+ ...(base.instrumentation === undefined ? {} : { instrumentation: base.instrumentation }),
54
+ capture: <Result>(capture: SceneCapture<C, S, Result>): Result => capture(captured),
55
+ captureAny: <Result>(capture: AnySceneCapture<Result>): Result => capture(captured),
56
+ }
57
+ return captured
58
+ }
59
+
60
+ const profileCapturedScene = <
61
+ C extends UiContract,
62
+ S extends ReadonlyArray<unknown>,
63
+ Services,
64
+ P,
65
+ N extends string,
66
+ Identity,
67
+ >(
68
+ base: CapturedScene<C, S, Services, P, N, Identity>,
69
+ profiler: ProfilerHandle,
70
+ lifecycle: ReturnType<typeof makeLifecycle>,
71
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity> => ({
72
+ scene: withLifecycle(profiledScene(base, profiler), lifecycle),
73
+ profiler,
74
+ })
75
+
76
+ export function profileScene<
77
+ C extends UiContract,
78
+ S extends ReadonlyArray<unknown>,
79
+ Services,
80
+ P,
81
+ N extends string,
82
+ Identity,
83
+ >(
84
+ base: CapturedScene<C, S, Services, P, N, Identity>,
85
+ options?: ProfilerOptionsExternalApi,
86
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity>
87
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
88
+ base: Scene<C, S>,
89
+ options?: ProfilerOptionsExternalApi,
90
+ ): ProfiledScene<C, S>
91
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
31
92
  base: Scene<C, S>,
32
93
  options: ProfilerOptionsExternalApi = {},
33
- ): ProfiledScene<C, S> => {
94
+ ): ProfiledScene<C, S> {
34
95
  const profiler = makeProfiler({
35
96
  label: options.label ?? base.composition.manifest.title,
36
97
  ...(options.capacity !== undefined ? { capacity: options.capacity } : {}),
37
98
  })
38
- const lifecycle = Layer.scopedDiscard(
39
- Effect.acquireRelease(
40
- Effect.sync(() => registerProfiler({ label: profiler.label, handle: profiler })),
41
- (unregister) => Effect.sync(unregister),
42
- ),
99
+ const lifecycle = makeLifecycle(profiler)
100
+ return base.capture<ProfiledScene<C, S>>(
101
+ <Services, P, N extends string, Identity>(
102
+ exactScene: CapturedScene<C, S, Services, P, N, Identity>,
103
+ ): ProfiledScene<C, S> => profileCapturedScene(exactScene, profiler, lifecycle),
43
104
  )
44
- const wrapped = profiledScene(base, profiler)
45
- return {
46
- // The lifecycle layer rides the first provide layer (`merge` keeps its
47
- // services), so registration follows the runtime's real build/dispose.
48
- scene: {
49
- ...wrapped,
50
- provide: wrapped.provide.map((layer, index) =>
51
- index === 0 ? Layer.merge(layer, lifecycle) : layer,
52
- ),
53
- },
54
- profiler,
55
- }
56
105
  }
@@ -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({})) {}
@@ -121,19 +114,25 @@ class Bump extends Reducer.make('ProfilerBump', { states: [Count], events: [Bump
121
114
  const BumpLive = Reducer.live(Bump, (n) => n + 1)
122
115
 
123
116
  class RootUi extends ui('ProfilerRoot')<{ props: { count: number } }>() {}
117
+
124
118
  class Root extends Composition.make('ProfilerRoot', {
125
119
  title: 'ProfilerRoot',
126
120
  states: [States],
127
121
  events: [Bumped],
128
122
  ui: RootUi,
129
- }) {}
123
+ })<Root>() {}
130
124
  const RootLive = Composition.live(Root, function* () {
131
125
  const count = yield* StateGroup.select(States, 'profiler-count')
132
126
  return mount({ props: { count }, slots: {} })
133
127
  })
134
128
 
135
129
  const app = RootLive.pipe(
136
- Layer.provideMerge(provide(RootUi, Ui.make(RootUi, () => null))),
130
+ Layer.provideMerge(
131
+ provide(
132
+ RootUi,
133
+ Ui.make(RootUi, () => null),
134
+ ),
135
+ ),
137
136
  Layer.provideMerge(
138
137
  Layer.mergeAll(BumpLive).pipe(
139
138
  Layer.provideMerge(Layer.mergeAll(Engine, StateGroup.live(States, { 'profiler-count': 0 }))),
@@ -168,7 +167,6 @@ test('profileScene records engine activity through makeAppRuntime and registers
168
167
  await settle()
169
168
  expect(listProfilers().map((entry) => entry.handle)).not.toContain(profiler)
170
169
 
171
- // The base scene is untouched: no instrumentation carrier, nothing recorded.
172
170
  const before = profiler.snapshot().events.get('ProfilerBumped')?.count
173
171
  const plain = makeAppRuntime(baseScene)
174
172
  plain.dispatch('High', Event.construct(Bumped, {}))
@@ -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 => {}
@@ -24,12 +24,7 @@ import {
24
24
  tabStyle,
25
25
  } from './styles'
26
26
 
27
- // <ReformProfiler/> the dev overlay. Plain React + useSyncExternalStore, NOT
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
 
@@ -105,7 +97,9 @@ const DroppedNote = ({ count }: DroppedNoteProps): ReactNode => {
105
97
  if (count === 0) {
106
98
  return null
107
99
  }
108
- return <div style={{ color: palette.dim }}>({count} early records evicted from the ring buffer)</div>
100
+ return (
101
+ <div style={{ color: palette.dim }}>({count} early records evicted from the ring buffer)</div>
102
+ )
109
103
  }
110
104
 
111
105
  interface ProfilerBodyProps {
@@ -137,13 +131,6 @@ const ProfilerBody = ({ profiler }: ProfilerBodyProps): ReactNode => {
137
131
  )
138
132
  }
139
133
 
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
134
  export const ReformProfiler = (): ReactNode => {
148
135
  const owner = useRef({}).current
149
136
  const [claimed, setClaimed] = useState(false)
@@ -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
- import { type Instrumentation, makeScheduler, type SpanEnd } from '@playfast/reform'
1
+ import { type Instrumentation, makeScheduler, type SpanEnd } from '@playfast/reform/internal'
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
- // Ring buffer: fixed slots + a monotone write counter (`written % capacity`
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 }
@@ -122,8 +93,7 @@ export const makeProfiler = (options: ProfilerOptionsExternalApi = {}): Profiler
122
93
  readonly detail: string
123
94
  }
124
95
 
125
- const instant = (site: Site): void =>
126
- record({ ...site, start: performance.now(), dur: 0 })
96
+ const instant = (site: Site): void => record({ ...site, start: performance.now(), dur: 0 })
127
97
 
128
98
  const span = (site: Site): SpanEnd => {
129
99
  const start = performance.now()
@@ -168,7 +138,6 @@ export const makeProfiler = (options: ProfilerOptionsExternalApi = {}): Profiler
168
138
  },
169
139
  getVersion: () => cursor.version,
170
140
 
171
- // ── Instrumentation ────────────────────────────────────────────────────
172
141
  eventDispatched: (tag, priority) => instant({ kind: 'event', name: tag, detail: priority }),
173
142
  reducerRun: (name, eventTag) => span({ kind: 'reducer', name, detail: eventTag }),
174
143
  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
- // Module-level registry of the profilers whose runtimes are currently live.
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,13 @@ export interface ProfilerRegistration {
12
9
  }
13
10
 
14
11
  interface RegistryState {
15
- /** Live registrations with a build refcount (StrictMode double-builds share one entry). */
16
- readonly entries: Map<ProfilerHandle, { readonly registration: ProfilerRegistration; readonly count: number }>
12
+ // Refcount: StrictMode double-build shares one entry.
13
+ readonly entries: Map<
14
+ ProfilerHandle,
15
+ { readonly registration: ProfilerRegistration; readonly count: number }
16
+ >
17
17
  readonly listeners: Set<() => void>
18
18
  readonly version: { current: number }
19
- /** The overlay instance currently elected to render the panel. */
20
19
  readonly overlay: { owner: Option.Option<object> }
21
20
  }
22
21
 
@@ -35,11 +34,6 @@ const notify = (): void => {
35
34
  ;[...state.listeners].forEach((listener) => listener())
36
35
  }
37
36
 
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
37
  export const registerProfiler = (registration: ProfilerRegistration): (() => void) => {
44
38
  const existing = state.entries.get(registration.handle)
45
39
  state.entries.set(registration.handle, {
@@ -63,11 +57,9 @@ export const registerProfiler = (registration: ProfilerRegistration): (() => voi
63
57
  }
64
58
  }
65
59
 
66
- /** The live registrations, in registration order. */
67
60
  export const listProfilers = (): ReadonlyArray<ProfilerRegistration> =>
68
61
  [...state.entries.values()].map((entry) => entry.registration)
69
62
 
70
- /** Subscribe to registry changes (register/unregister). Sync, rare. */
71
63
  export const subscribeProfilers = (listener: () => void): (() => void) => {
72
64
  state.listeners.add(listener)
73
65
  return () => {
@@ -75,14 +67,9 @@ export const subscribeProfilers = (listener: () => void): (() => void) => {
75
67
  }
76
68
  }
77
69
 
78
- /** Monotone registry change counter (`useSyncExternalStore` snapshot). */
79
70
  export const profilersVersion = (): number => state.version.current
80
71
 
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
- */
72
+ // First mounted overlay claims render; later instances no-op until claim released.
86
73
  export const claimOverlay = (owner: object): boolean => {
87
74
  if (Option.isNone(state.overlay.owner)) {
88
75
  state.overlay.owner = Option.some(owner)
@@ -91,7 +78,6 @@ export const claimOverlay = (owner: object): boolean => {
91
78
  return Option.contains(state.overlay.owner, owner)
92
79
  }
93
80
 
94
- /** Release the panel claim so the next mounted instance can take over. */
95
81
  export const releaseOverlay = (owner: object): void => {
96
82
  if (Option.contains(state.overlay.owner, owner)) {
97
83
  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,11 +51,17 @@ 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> = [
66
- { ph: 'M', name: 'process_name', ts: 0, pid, tid: 0, args: { name: `reform: ${profiler.label}` } },
57
+ {
58
+ ph: 'M',
59
+ name: 'process_name',
60
+ ts: 0,
61
+ pid,
62
+ tid: 0,
63
+ args: { name: `reform: ${profiler.label}` },
64
+ },
67
65
  ...Object.values(lanes).map(
68
66
  (lane): ChromeTraceEvent => ({
69
67
  ph: 'M',
@@ -80,14 +78,23 @@ export const toChromeTrace = (profiler: ProfilerHandle): ChromeTrace => {
80
78
  const cat = record.kind
81
79
  const tid = lanes[record.kind].tid
82
80
  const args = record.detail === '' ? {} : { detail: record.detail }
81
+ const complete: ChromeTraceEvent = {
82
+ ph: 'X',
83
+ name: record.name,
84
+ cat,
85
+ ts: startUs,
86
+ dur: record.dur * US_PER_MS,
87
+ pid,
88
+ tid,
89
+ args,
90
+ }
83
91
  return instantKinds.has(record.kind)
84
92
  ? { ph: 'i', name: record.name, cat, ts: startUs, pid, tid, s: 't', args }
85
- : { ph: 'X', name: record.name, cat, ts: startUs, dur: record.dur * US_PER_MS, pid, tid, args }
93
+ : complete
86
94
  })
87
95
  return { traceEvents: [...meta, ...body], displayTimeUnit: 'ms' }
88
96
  }
89
97
 
90
- /** `toChromeTrace` as a JSON string, ready to write to a `.json` file. */
91
98
  export const traceJson = (profiler: ProfilerHandle): string =>
92
99
  // 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
100
  JSON.stringify(toChromeTrace(profiler))