@barefootjs/jsx 0.10.0 → 0.11.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/dist/compiler.d.ts.map +1 -1
- package/dist/debug-profile.d.ts +115 -0
- package/dist/debug-profile.d.ts.map +1 -0
- package/dist/debug.d.ts +4 -3
- package/dist/debug.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +31 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1905 -207
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts +3 -3
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +4 -2
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +28 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts +2 -2
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/effects-and-on-mounts.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +5 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +29 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +26 -0
- package/dist/loop-destructure.d.ts.map +1 -0
- package/dist/profiler.d.ts +492 -0
- package/dist/profiler.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/debug-profile.test.ts +405 -0
- package/src/__tests__/expression-parser.test.ts +44 -1
- package/src/__tests__/profile-bfid-emission.test.ts +63 -0
- package/src/__tests__/profile-binding-ids.test.ts +123 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +80 -0
- package/src/__tests__/profile-loop-binding-ids.test.ts +106 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +153 -0
- package/src/__tests__/profile-turn-markers-branch.test.ts +83 -0
- package/src/__tests__/profile-turn-markers-delegation.test.ts +63 -0
- package/src/__tests__/profile-turn-markers.test.ts +54 -0
- package/src/__tests__/profiler-batch-advisor.test.ts +198 -0
- package/src/__tests__/profiler-coverage-conformance.test.ts +360 -0
- package/src/__tests__/profiler-e2e.test.ts +104 -0
- package/src/__tests__/profiler-hot-subscribers.test.ts +263 -0
- package/src/__tests__/profiler-wasted-re-runs.test.ts +147 -0
- package/src/__tests__/profiler.test.ts +408 -0
- package/src/__tests__/ssr-defaults.test.ts +24 -0
- package/src/compiler.ts +3 -0
- package/src/debug-profile.ts +543 -0
- package/src/debug.ts +192 -28
- package/src/expression-parser.ts +53 -0
- package/src/index.ts +72 -1
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +3 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +8 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +9 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +12 -8
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +10 -4
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/insert.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +6 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +14 -2
- package/src/ir-to-client-js/control-flow/stringify/event-listener.ts +5 -2
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +13 -11
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +19 -7
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +18 -13
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +18 -14
- package/src/ir-to-client-js/control-flow.ts +12 -6
- package/src/ir-to-client-js/emit-reactive.ts +18 -5
- package/src/ir-to-client-js/imports.ts +2 -0
- package/src/ir-to-client-js/index.ts +6 -1
- package/src/ir-to-client-js/phases/effects-and-on-mounts.ts +10 -4
- package/src/ir-to-client-js/phases/event-handlers.ts +6 -2
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +7 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +12 -6
- package/src/ir-to-client-js/types.ts +5 -0
- package/src/ir-to-client-js/utils.ts +37 -0
- package/src/jsx-to-ir.ts +2 -2
- package/src/loop-destructure.ts +170 -0
- package/src/profiler.ts +1488 -0
- package/src/ssr-defaults.ts +65 -0
- package/src/types.ts +8 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive performance profiler tests (#1690).
|
|
3
|
+
*
|
|
4
|
+
* Covers the run-free static analysis (SR5 budget, SR6 compile-diff), the SR4
|
|
5
|
+
* id parse/join, and `buildProfileReport` — the dynamic report assembled from a
|
|
6
|
+
* recorded SR2 event stream (the stream itself is produced by the CLI scenario
|
|
7
|
+
* driver, tested separately).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from 'bun:test'
|
|
11
|
+
import {
|
|
12
|
+
buildStaticBudget,
|
|
13
|
+
diffStaticBudget,
|
|
14
|
+
formatStaticBudget,
|
|
15
|
+
formatBudgetDiff,
|
|
16
|
+
buildProfileReport,
|
|
17
|
+
formatProfileReport,
|
|
18
|
+
parseProfilerId,
|
|
19
|
+
buildIdIndex,
|
|
20
|
+
joinProfilerEvents,
|
|
21
|
+
findUninstrumentedEffects,
|
|
22
|
+
} from '../profiler'
|
|
23
|
+
import { buildComponentAnalysis } from '../debug'
|
|
24
|
+
import type { ProfilerEvent } from '@barefootjs/shared'
|
|
25
|
+
|
|
26
|
+
const memoChainSource = `
|
|
27
|
+
'use client'
|
|
28
|
+
import { createSignal, createMemo, createEffect } from '@barefootjs/client'
|
|
29
|
+
|
|
30
|
+
export function Calc() {
|
|
31
|
+
const [count, setCount] = createSignal(0)
|
|
32
|
+
const a = createMemo(() => count() * 2)
|
|
33
|
+
const b = createMemo(() => a() + 1)
|
|
34
|
+
const c = createMemo(() => b() + 1)
|
|
35
|
+
createEffect(() => console.log(c()))
|
|
36
|
+
return <button onClick={() => setCount(n => n + 1)}>{c()}</button>
|
|
37
|
+
}
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
const counterSource = `
|
|
41
|
+
'use client'
|
|
42
|
+
import { createSignal } from '@barefootjs/client'
|
|
43
|
+
|
|
44
|
+
export function Counter() {
|
|
45
|
+
const [count, setCount] = createSignal(0)
|
|
46
|
+
return <button onClick={() => setCount(n => n + 1)}>Count: {count()}</button>
|
|
47
|
+
}
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
describe('buildStaticBudget (SR5)', () => {
|
|
51
|
+
test('counts reactive nodes and subscriptions', () => {
|
|
52
|
+
const b = buildStaticBudget(counterSource, 'Counter.tsx', 'Counter')
|
|
53
|
+
expect(b.kind).toBe('static-budget')
|
|
54
|
+
expect(b.componentName).toBe('Counter')
|
|
55
|
+
expect(b.signals).toBe(1)
|
|
56
|
+
expect(b.memos).toBe(0)
|
|
57
|
+
// The text binding `{count()}` subscribes to `count`.
|
|
58
|
+
expect(b.subscriptions).toBeGreaterThan(0)
|
|
59
|
+
expect(b.memoChainDepth).toBe(0)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('excludes event handlers from fan-out and subscriptions (they read, do not react)', () => {
|
|
63
|
+
// `count` is read by the text binding (reactive) AND the onClick handler
|
|
64
|
+
// (not reactive — runs outside any effect). Only the binding counts.
|
|
65
|
+
const src = `
|
|
66
|
+
'use client'
|
|
67
|
+
import { createSignal } from '@barefootjs/client'
|
|
68
|
+
export function C() {
|
|
69
|
+
const [count, setCount] = createSignal(0)
|
|
70
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
const b = buildStaticBudget(src, 'C.tsx', 'C')
|
|
74
|
+
const fan = b.fanOut.find(f => f.signal === 'count')!.subscribers
|
|
75
|
+
// The text binding subscribes; the handler does not.
|
|
76
|
+
expect(fan).toBe(1)
|
|
77
|
+
expect(b.subscriptions).toBe(1)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('measures the longest memo chain', () => {
|
|
81
|
+
const b = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
|
|
82
|
+
expect(b.memos).toBe(3)
|
|
83
|
+
expect(b.memoChainDepth).toBe(3)
|
|
84
|
+
expect(b.memoChainLongest).toEqual(['a', 'b', 'c'])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('reports per-signal fan-out, hottest first', () => {
|
|
88
|
+
const b = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
|
|
89
|
+
const count = b.fanOut.find(f => f.signal === 'count')
|
|
90
|
+
expect(count).toBeDefined()
|
|
91
|
+
// count fans out through a → b → c → effect/text, i.e. several subscribers.
|
|
92
|
+
expect(count!.subscribers).toBeGreaterThanOrEqual(3)
|
|
93
|
+
// Sorted descending.
|
|
94
|
+
for (let i = 1; i < b.fanOut.length; i++) {
|
|
95
|
+
expect(b.fanOut[i - 1].subscribers).toBeGreaterThanOrEqual(b.fanOut[i].subscribers)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('honors the fan-out threshold for the hot flag', () => {
|
|
100
|
+
const hot = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc', { fanOutThreshold: 1 })
|
|
101
|
+
expect(hot.fanOut.find(f => f.signal === 'count')!.hot).toBe(true)
|
|
102
|
+
const cold = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc', { fanOutThreshold: 999 })
|
|
103
|
+
expect(cold.fanOut.every(f => f.hot === false)).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('formats a human-readable budget', () => {
|
|
107
|
+
const out = formatStaticBudget(buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc'))
|
|
108
|
+
expect(out).toContain('static reactivity budget')
|
|
109
|
+
expect(out).toContain('memo-chain depth: 3')
|
|
110
|
+
expect(out).toContain('predictive only')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('flags a compound component whose consumers live in composed children', () => {
|
|
114
|
+
// Reactive state exists but nothing in *this* component reads it — the
|
|
115
|
+
// signal/memo only drive composed child components (the Select/Combobox
|
|
116
|
+
// shape). The single-component budget can't see across that boundary.
|
|
117
|
+
const compound = `
|
|
118
|
+
'use client'
|
|
119
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
120
|
+
import { Ctx } from './ctx'
|
|
121
|
+
export function Picker(props: { value?: string }) {
|
|
122
|
+
const [internal, setInternal] = createSignal('')
|
|
123
|
+
const isControlled = createMemo(() => props.value !== undefined)
|
|
124
|
+
return <Ctx value={{ internal, setInternal, isControlled }}>{props.children}</Ctx>
|
|
125
|
+
}
|
|
126
|
+
`
|
|
127
|
+
const b = buildStaticBudget(compound, 'Picker.tsx', 'Picker')
|
|
128
|
+
expect(b.signals).toBeGreaterThan(0)
|
|
129
|
+
expect(b.subscriptions).toBe(0)
|
|
130
|
+
expect(b.crossComponentOnly).toBe(true)
|
|
131
|
+
expect(formatStaticBudget(b)).toContain('compound')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('does not flag a self-contained component as compound', () => {
|
|
135
|
+
const b = buildStaticBudget(counterSource, 'Counter.tsx', 'Counter')
|
|
136
|
+
expect(b.crossComponentOnly).toBe(false)
|
|
137
|
+
expect(formatStaticBudget(b)).not.toContain('compound')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('a memo-only component whose memo is consumed in-component is not compound', () => {
|
|
141
|
+
// No signals, so signal `subscriptions`/fan-out are 0 — but the memo IS
|
|
142
|
+
// consumed by an in-component DOM binding, so this is self-contained, not a
|
|
143
|
+
// compound component. The flag must span memo consumers, not just signals.
|
|
144
|
+
const src = `
|
|
145
|
+
'use client'
|
|
146
|
+
import { createMemo } from '@barefootjs/client'
|
|
147
|
+
export function Label(props: { x?: number }) {
|
|
148
|
+
const label = createMemo(() => (props.x ?? 0) + 1)
|
|
149
|
+
return <div>{label()}</div>
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
const b = buildStaticBudget(src, 'Label.tsx', 'Label')
|
|
153
|
+
expect(b.memos).toBe(1)
|
|
154
|
+
expect(b.signals).toBe(0)
|
|
155
|
+
expect(b.subscriptions).toBe(0)
|
|
156
|
+
expect(b.crossComponentOnly).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('diffStaticBudget (SR6)', () => {
|
|
161
|
+
test('flags an added memo + deeper chain as a regression', () => {
|
|
162
|
+
const base = buildStaticBudget(counterSource, 'Counter.tsx', 'Counter')
|
|
163
|
+
const head = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
|
|
164
|
+
const diff = diffStaticBudget(base, head)
|
|
165
|
+
expect(diff.memos).toBe(3)
|
|
166
|
+
expect(diff.memoChainDepth).toBe(3)
|
|
167
|
+
expect(diff.regressed).toBe(true)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('reports no regression for identical compiles', () => {
|
|
171
|
+
const a = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
|
|
172
|
+
const b = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
|
|
173
|
+
const diff = diffStaticBudget(a, b)
|
|
174
|
+
expect(diff.regressed).toBe(false)
|
|
175
|
+
expect(formatBudgetDiff(diff)).toContain('no structural reactivity change')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('carries a "diff" kind discriminator (#1849 B2)', () => {
|
|
179
|
+
// The three JSON modes must be distinguishable: a zero-delta diff
|
|
180
|
+
// (signals: 0 = "no change") must not look like a static budget
|
|
181
|
+
// (signals: 0 = "no signals"). `kind` is the discriminator.
|
|
182
|
+
const a = buildStaticBudget(memoChainSource, 'Calc.tsx', 'Calc')
|
|
183
|
+
const diff = diffStaticBudget(a, a)
|
|
184
|
+
expect(diff.kind).toBe('diff')
|
|
185
|
+
expect(diff.signals).toBe(0)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('parseProfilerId (SR4)', () => {
|
|
190
|
+
test('splits <Component>#<kind>:<rest>', () => {
|
|
191
|
+
expect(parseProfilerId('Calc#memo:doubled')).toEqual({ component: 'Calc', kind: 'memo', rest: 'doubled' })
|
|
192
|
+
})
|
|
193
|
+
test('keeps the full rest for controlled-effect ids', () => {
|
|
194
|
+
expect(parseProfilerId('Calc#effect:controlled:setCount')).toEqual({
|
|
195
|
+
component: 'Calc', kind: 'effect', rest: 'controlled:setCount',
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
test('returns null for non-profiler strings', () => {
|
|
199
|
+
expect(parseProfilerId('not-an-id')).toBeNull()
|
|
200
|
+
expect(parseProfilerId('Calc#memo')).toBeNull()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('buildIdIndex + joinProfilerEvents (SR4 join)', () => {
|
|
205
|
+
const { graph } = buildComponentAnalysis(memoChainSource, 'Calc.tsx')
|
|
206
|
+
const index = buildIdIndex(graph)
|
|
207
|
+
|
|
208
|
+
const ev = (type: ProfilerEvent['type'], fields: Partial<ProfilerEvent> = {}): ProfilerEvent =>
|
|
209
|
+
({ type, seq: 0, turn: null, ...fields })
|
|
210
|
+
|
|
211
|
+
test('indexes signals and memos by their compiler id, with source loc', () => {
|
|
212
|
+
const sig = index.get('Calc#signal:count')
|
|
213
|
+
expect(sig).toMatchObject({ kind: 'signal', name: 'count' })
|
|
214
|
+
expect(sig!.loc.line).toBeGreaterThan(0)
|
|
215
|
+
|
|
216
|
+
const memo = index.get('Calc#memo:a')
|
|
217
|
+
expect(memo).toMatchObject({ kind: 'memo', name: 'a' })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('indexes the controlled-signal sync effect under its setter', () => {
|
|
221
|
+
expect(index.get('Calc#effect:controlled:setCount')).toMatchObject({ kind: 'effect' })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('resolves an event stream to source-mapped nodes', () => {
|
|
225
|
+
const events = [
|
|
226
|
+
ev('signalSet', { signal: 'Calc#signal:count' }),
|
|
227
|
+
ev('effectEnter', { subscriber: 'Calc#memo:a' }),
|
|
228
|
+
]
|
|
229
|
+
const { joined, unattributed } = joinProfilerEvents(events, index)
|
|
230
|
+
expect(joined[0].signal).toMatchObject({ kind: 'signal', name: 'count' })
|
|
231
|
+
expect(joined[1].subscriber).toMatchObject({ kind: 'memo', name: 'a' })
|
|
232
|
+
expect(unattributed).toHaveLength(0)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('surfaces unresolved ids as a coverage gap, never dropping events', () => {
|
|
236
|
+
const events = [
|
|
237
|
+
ev('effectEnter', { subscriber: 'Calc#effect:does-not-exist' }),
|
|
238
|
+
ev('effectExit', { subscriber: 'Calc#effect:does-not-exist', dur: 1 }),
|
|
239
|
+
]
|
|
240
|
+
const { joined, unattributed } = joinProfilerEvents(events, index)
|
|
241
|
+
expect(joined).toHaveLength(2) // events preserved
|
|
242
|
+
expect(joined[0].subscriber).toBeUndefined()
|
|
243
|
+
expect(unattributed).toEqual([{ id: 'Calc#effect:does-not-exist', count: 2 }])
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('routes anonymous runtime ids to diagnostics, not the actionable gap list (#1840)', () => {
|
|
247
|
+
const events = [
|
|
248
|
+
// Compiler id the IR can't place → actionable gap.
|
|
249
|
+
ev('effectEnter', { subscriber: 'Calc#effect:does-not-exist' }),
|
|
250
|
+
// Anonymous runtime bookkeeping ids (no compiler __bfId) → non-actionable.
|
|
251
|
+
ev('signalSet', { signal: 's9' }),
|
|
252
|
+
ev('signalSet', { signal: 's10' }),
|
|
253
|
+
ev('effectEnter', { subscriber: 'r10' }),
|
|
254
|
+
ev('effectEnter', { subscriber: 'e3' }),
|
|
255
|
+
]
|
|
256
|
+
const { joined, unattributed, diagnostics } = joinProfilerEvents(events, index)
|
|
257
|
+
expect(joined).toHaveLength(5) // nothing dropped
|
|
258
|
+
expect(unattributed.map(u => u.id)).toEqual(['Calc#effect:does-not-exist'])
|
|
259
|
+
expect(diagnostics.map(u => u.id).sort()).toEqual(['e3', 'r10', 's10', 's9'])
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('buildProfileReport (dynamic, SR1–SR4 + analyses)', () => {
|
|
264
|
+
const src = `
|
|
265
|
+
'use client'
|
|
266
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
267
|
+
export function Calc() {
|
|
268
|
+
const [count, setCount] = createSignal(0)
|
|
269
|
+
const a = createMemo(() => count() * 2)
|
|
270
|
+
return <button onClick={() => setCount(count() + 1)}>{a()}</button>
|
|
271
|
+
}
|
|
272
|
+
`
|
|
273
|
+
let n = 0
|
|
274
|
+
const ev = (type: ProfilerEvent['type'], f: Partial<ProfilerEvent> = {}): ProfilerEvent =>
|
|
275
|
+
({ type, seq: n++, turn: null, ...f })
|
|
276
|
+
|
|
277
|
+
test('assembles hot subscribers, batch advisor, and coverage from a stream', () => {
|
|
278
|
+
n = 0
|
|
279
|
+
const events: ProfilerEvent[] = [
|
|
280
|
+
ev('effectEnter', { subscriber: 'Calc#memo:a' }), // mount
|
|
281
|
+
ev('effectOutput', { subscriber: 'Calc#memo:a', changed: true }), // mount: real output
|
|
282
|
+
ev('turnBegin', { handlerId: 'Calc#handler:s0:click' }),
|
|
283
|
+
ev('effectEnter', { subscriber: 'Calc#memo:a', turn: 'Calc#handler:s0:click' }),
|
|
284
|
+
ev('effectExit', { subscriber: 'Calc#memo:a', dur: 2, turn: 'Calc#handler:s0:click' }),
|
|
285
|
+
ev('effectOutput', { subscriber: 'Calc#memo:a', changed: false, turn: 'Calc#handler:s0:click' }), // wasted
|
|
286
|
+
ev('turnEnd', {}),
|
|
287
|
+
]
|
|
288
|
+
const r = buildProfileReport({ source: src, filePath: 'Calc.tsx', scenario: 'auto', events })
|
|
289
|
+
expect(r.kind).toBe('profile')
|
|
290
|
+
expect(r.componentName).toBe('Calc')
|
|
291
|
+
expect(r.turns).toBe(1)
|
|
292
|
+
expect(r.hotSubscribers.subscribers[0].name).toBe('a')
|
|
293
|
+
expect(r.wastedReReruns.subscribers[0].name).toBe('a')
|
|
294
|
+
expect(r.wastedReReruns.subscribers[0].wastedRuns).toBe(1)
|
|
295
|
+
expect(r.coverage.handlersFired).toBe(1)
|
|
296
|
+
expect(r.coverage.handlersTotal).toBeGreaterThanOrEqual(1)
|
|
297
|
+
const out = formatProfileReport(r)
|
|
298
|
+
expect(out).toContain('Calc — profile')
|
|
299
|
+
expect(out).toContain('hot subscribers')
|
|
300
|
+
expect(out).toContain('wasted re-runs')
|
|
301
|
+
expect(out).toContain('coverage:')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('a zero-turn report directs to the right tool', () => {
|
|
305
|
+
n = 0
|
|
306
|
+
// No handler events at all → no turns, no handlers.
|
|
307
|
+
const noHandlerSrc = `
|
|
308
|
+
'use client'
|
|
309
|
+
import { createSignal } from '@barefootjs/client'
|
|
310
|
+
export function Disp() { const [v] = createSignal(1); return <div>{v()}</div> }
|
|
311
|
+
`
|
|
312
|
+
const events: ProfilerEvent[] = [ev('effectEnter', { subscriber: 'Disp#binding:s0' })]
|
|
313
|
+
const noHandlers = buildProfileReport({ source: noHandlerSrc, filePath: 'Disp.tsx', scenario: 'auto', events })
|
|
314
|
+
expect(noHandlers.turns).toBe(0)
|
|
315
|
+
expect(formatProfileReport(noHandlers)).toContain('no event handlers')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('coverage.diagnostics is a compact summary, not a full id array (#1849 B7)', () => {
|
|
319
|
+
n = 0
|
|
320
|
+
// A long tail of anonymous runtime bookkeeping ids (loop-generated binding
|
|
321
|
+
// ids on a grid component). JSON consumers get a count + a small sample
|
|
322
|
+
// instead of hundreds of `{id,count}` objects.
|
|
323
|
+
const events: ProfilerEvent[] = []
|
|
324
|
+
for (let i = 0; i < 50; i++) {
|
|
325
|
+
events.push(ev('signalSet', { signal: `s${i}` }))
|
|
326
|
+
}
|
|
327
|
+
const r = buildProfileReport({ source: src, filePath: 'Calc.tsx', scenario: 'auto', events })
|
|
328
|
+
expect(r.coverage.diagnostics.count).toBe(50)
|
|
329
|
+
expect(r.coverage.diagnostics.sample.length).toBeLessThanOrEqual(3)
|
|
330
|
+
expect(Array.isArray(r.coverage.diagnostics.sample)).toBe(true)
|
|
331
|
+
// The text report still summarizes by count.
|
|
332
|
+
expect(formatProfileReport(r)).toContain('50 anonymous runtime id(s)')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('omitting topN keeps the full subscriber list (the JSON path, #1849 B1)', () => {
|
|
336
|
+
n = 0
|
|
337
|
+
// Five distinct subscribers — more than a small `--top`. The CLI passes
|
|
338
|
+
// `topN: undefined` in JSON mode so the serialized list is never truncated;
|
|
339
|
+
// pinning the contract here guards that the data layer returns everything
|
|
340
|
+
// when no cap is requested.
|
|
341
|
+
const manySrc = `
|
|
342
|
+
'use client'
|
|
343
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
344
|
+
export function Many() {
|
|
345
|
+
const [count, setCount] = createSignal(0)
|
|
346
|
+
const a = createMemo(() => count() + 1)
|
|
347
|
+
const b = createMemo(() => count() + 2)
|
|
348
|
+
const c = createMemo(() => count() + 3)
|
|
349
|
+
const d = createMemo(() => count() + 4)
|
|
350
|
+
const e = createMemo(() => count() + 5)
|
|
351
|
+
return <button onClick={() => setCount(count() + 1)}>{a()}{b()}{c()}{d()}{e()}</button>
|
|
352
|
+
}
|
|
353
|
+
`
|
|
354
|
+
const events: ProfilerEvent[] = []
|
|
355
|
+
for (const name of ['a', 'b', 'c', 'd', 'e']) {
|
|
356
|
+
events.push(ev('effectEnter', { subscriber: `Many#memo:${name}` }))
|
|
357
|
+
events.push(ev('effectExit', { subscriber: `Many#memo:${name}`, dur: 1 }))
|
|
358
|
+
}
|
|
359
|
+
const full = buildProfileReport({ source: manySrc, filePath: 'Many.tsx', scenario: 'auto', events })
|
|
360
|
+
expect(full.hotSubscribers.subscribers).toHaveLength(5)
|
|
361
|
+
// With a cap the table truncates — what JSON mode deliberately avoids.
|
|
362
|
+
const capped = buildProfileReport({ source: manySrc, filePath: 'Many.tsx', scenario: 'auto', events, topN: 2 })
|
|
363
|
+
expect(capped.hotSubscribers.subscribers).toHaveLength(2)
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
describe('findUninstrumentedEffects (#1849 B6)', () => {
|
|
368
|
+
let seq = 0
|
|
369
|
+
const ev = (type: ProfilerEvent['type'], f: Partial<ProfilerEvent> = {}): ProfilerEvent =>
|
|
370
|
+
({ type, seq: seq++, turn: null, ...f })
|
|
371
|
+
|
|
372
|
+
const refEffectSrc = `
|
|
373
|
+
'use client'
|
|
374
|
+
import { createSignal, createEffect } from '@barefootjs/client'
|
|
375
|
+
export function C() {
|
|
376
|
+
const [open, setOpen] = createSignal(false)
|
|
377
|
+
createEffect(() => console.log(open())) // line 6 — top-level (instrumented)
|
|
378
|
+
const handleMount = (el) => {
|
|
379
|
+
createEffect(() => { el.dataset.state = open() ? 'open' : 'closed' }) // line 8 — nested
|
|
380
|
+
}
|
|
381
|
+
return <button ref={handleMount} onClick={() => setOpen(v => !v)}>{open()}</button>
|
|
382
|
+
}
|
|
383
|
+
`
|
|
384
|
+
|
|
385
|
+
test('returns createEffect sites the compiler did not instrument', () => {
|
|
386
|
+
// The top-level effect is on line 6 (instrumented); the ref-callback effect
|
|
387
|
+
// on line 8 is not. Subtracting the instrumented line yields the nested one.
|
|
388
|
+
const all = findUninstrumentedEffects(refEffectSrc, 'C.tsx', new Set())
|
|
389
|
+
expect(all.map(c => c.line)).toEqual([6, 8])
|
|
390
|
+
const candidates = findUninstrumentedEffects(refEffectSrc, 'C.tsx', new Set([6]))
|
|
391
|
+
expect(candidates).toEqual([{ file: 'C.tsx', line: 8 }])
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('buildProfileReport attaches candidates to an uninstrumented e<n> id', () => {
|
|
395
|
+
seq = 0
|
|
396
|
+
// Drive the nested effect under a runtime fallback id `e1` (no __bfId).
|
|
397
|
+
const events: ProfilerEvent[] = [
|
|
398
|
+
ev('effectEnter', { subscriber: 'e1' }),
|
|
399
|
+
ev('effectExit', { subscriber: 'e1', dur: 3 }),
|
|
400
|
+
]
|
|
401
|
+
const r = buildProfileReport({ source: refEffectSrc, filePath: 'C.tsx', scenario: 'auto', events })
|
|
402
|
+
const e1 = r.hotSubscribers.subscribers.find(s => s.subscriber === 'e1')!
|
|
403
|
+
expect(e1.resolution).toBe('uninstrumented')
|
|
404
|
+
// Only the uninstrumented (line 8) call is a candidate; the instrumented
|
|
405
|
+
// top-level effect (line 6) is excluded.
|
|
406
|
+
expect(e1.candidates).toEqual([{ file: 'C.tsx', line: 8 }])
|
|
407
|
+
})
|
|
408
|
+
})
|
|
@@ -58,6 +58,30 @@ describe('extractSsrDefaults', () => {
|
|
|
58
58
|
expect(defaults?.count).toEqual({ value: 99 })
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
+
test('seeds a bare-props prop a signal initializer reads (`props.initial`)', () => {
|
|
62
|
+
// The #1297 prop-derived seeding lowers `createSignal(props.initial ?? 0)`
|
|
63
|
+
// to a *bare scalar* recompute in the template (`my $count = ($initial
|
|
64
|
+
// // 0)`), so `$initial` must be a stash var or Perl strict aborts with
|
|
65
|
+
// `Global symbol "$initial" requires explicit package name`. The
|
|
66
|
+
// bare-props-arg form previously skipped all props; this regression
|
|
67
|
+
// guards that the referenced prop is now seeded (as undef → the
|
|
68
|
+
// recompute's `?? 0` supplies the real fallback).
|
|
69
|
+
const metadata = metadataFor(`
|
|
70
|
+
'use client'
|
|
71
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
72
|
+
function Counter(props: { initial?: number }) {
|
|
73
|
+
const [count, setCount] = createSignal(props.initial ?? 0)
|
|
74
|
+
const doubled = createMemo(() => count() * 2)
|
|
75
|
+
return <p>{count()}{doubled()}</p>
|
|
76
|
+
}
|
|
77
|
+
`)
|
|
78
|
+
|
|
79
|
+
const defaults = extractSsrDefaults(metadata)
|
|
80
|
+
expect(defaults?.count).toEqual({ value: 0 })
|
|
81
|
+
expect(defaults?.doubled).toEqual({ value: 0 })
|
|
82
|
+
expect(defaults?.initial).toEqual({ propName: 'initial', value: null })
|
|
83
|
+
})
|
|
84
|
+
|
|
61
85
|
test('memo derived from a signal evaluates through the chain', () => {
|
|
62
86
|
const metadata = metadataFor(`
|
|
63
87
|
'use client'
|
package/src/compiler.ts
CHANGED
|
@@ -251,6 +251,7 @@ function compileMultipleComponents(
|
|
|
251
251
|
options.localImportPrefixes,
|
|
252
252
|
undefined,
|
|
253
253
|
multiAdapterCaps,
|
|
254
|
+
options.profile,
|
|
254
255
|
) || undefined,
|
|
255
256
|
adapterTypes: adapterOutput.types || undefined,
|
|
256
257
|
})
|
|
@@ -719,6 +720,7 @@ export function compileJSX(
|
|
|
719
720
|
},
|
|
720
721
|
undefined,
|
|
721
722
|
adapterCaps,
|
|
723
|
+
options.profile,
|
|
722
724
|
)
|
|
723
725
|
errors.push(...componentIR.errors)
|
|
724
726
|
if (result.code) {
|
|
@@ -734,6 +736,7 @@ export function compileJSX(
|
|
|
734
736
|
options.localImportPrefixes,
|
|
735
737
|
undefined,
|
|
736
738
|
adapterCaps,
|
|
739
|
+
options.profile,
|
|
737
740
|
)
|
|
738
741
|
errors.push(...componentIR.errors)
|
|
739
742
|
if (clientJs) {
|