sloptimize 0.3.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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +9 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/bin/sloptimize.mjs +346 -0
- package/docs/DESIGN-mecharoyale-v0.md +32 -0
- package/docs/INTEGRATION.md +230 -0
- package/docs/JITTER-AND-FOOTPRINTS.md +236 -0
- package/docs/SPEC-attach.md +176 -0
- package/docs/SPEC.md +845 -0
- package/docs/USAGE.md +227 -0
- package/hooks/hooks.json +16 -0
- package/mcp/server.mjs +127 -0
- package/package.json +64 -0
- package/skills/install/SKILL.md +143 -0
- package/skills/sloptimize/SKILL.md +74 -0
- package/src/attach.mjs +197 -0
- package/src/census.js +193 -0
- package/src/classify.js +70 -0
- package/src/footprint.js +170 -0
- package/src/history.js +273 -0
- package/src/index.js +8 -0
- package/src/inject-body.js +152 -0
- package/src/motion.js +345 -0
- package/src/panel.js +530 -0
- package/src/proposals.mjs +268 -0
- package/src/recorder.js +235 -0
- package/src/watch.mjs +242 -0
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# sloptimize — founding specification
|
|
2
|
+
|
|
3
|
+
Status: **v0 draft, pre-implementation.** This is the buildable contract
|
|
4
|
+
derived from the strategy work in the slopjs repo
|
|
5
|
+
(`docs/performance-strategy.md`, branch `ticket-ebf515b9`), which carries the
|
|
6
|
+
full evidence base and citations. Where this document and that one disagree,
|
|
7
|
+
this one wins — it is newer and more specific.
|
|
8
|
+
|
|
9
|
+
The one-sentence product: **give a coding agent the three verbs it measurably
|
|
10
|
+
cannot perform alone — measure, attribute, verify — against a running
|
|
11
|
+
three.js game, and make the loop terminate.**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 0. Design principles (the spec in miniature)
|
|
16
|
+
|
|
17
|
+
Every decision below derives from six principles. When a future feature
|
|
18
|
+
request conflicts with one, the principle wins or this section gets amended
|
|
19
|
+
first.
|
|
20
|
+
|
|
21
|
+
1. **The agent can never watch, so the game must remember.** Token cost and
|
|
22
|
+
loop latency make live observation impossible at any price. All evidence
|
|
23
|
+
is recorded before anyone asks, to disk, in schemas sized for a model's
|
|
24
|
+
context.
|
|
25
|
+
2. **Verdicts, not dashboards.** Every agent-facing surface is
|
|
26
|
+
non-interactive, machine-readable, and exit-code-meaningful. A number that
|
|
27
|
+
requires a human glance to interpret is dead weight.
|
|
28
|
+
3. **Attribution over aggregation.** A global counter ("940 draw calls") is
|
|
29
|
+
the starting point, never the answer. Every metric must resolve toward a
|
|
30
|
+
`track()` entity id, and through it toward a construction site in source.
|
|
31
|
+
4. **Never conclude past the evidence.** Truth grades on every measurement,
|
|
32
|
+
noise floors on every comparison, "no detectable change" as a first-class
|
|
33
|
+
result, and stated limits instead of degraded guesses.
|
|
34
|
+
5. **The ruler is not the agent's to hold.** Measurement runs outside the
|
|
35
|
+
agent's write path; what cannot be prevented (a local agent can edit
|
|
36
|
+
anything) is made expensive and visible instead.
|
|
37
|
+
6. **Own the protocol, borrow everything else.** three.js counts
|
|
38
|
+
(`renderer.info`), stats-gl times the GPU, Chromium runs the frames,
|
|
39
|
+
Chrome DevTools profiles functions, Spector captures draw state. sloptimize
|
|
40
|
+
owns identity-attached measurement and the loop — nothing else.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. Architecture
|
|
45
|
+
|
|
46
|
+
Five components, one product:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
game (browser) ──┐
|
|
50
|
+
│ in-page runtime: recorder, census, attribution
|
|
51
|
+
│ (src/, injected or imported; reads @slopjs/inspector ctx)
|
|
52
|
+
▼
|
|
53
|
+
vite plugin ────► .sloptimize/ on disk ◄──── CLI (bin/sloptimize.mjs)
|
|
54
|
+
(vite/plugin.js) the agent's reading room bench, check, attribute, doctor
|
|
55
|
+
▲
|
|
56
|
+
│ MCP server (mcp/) — live tools against the paused world
|
|
57
|
+
└ launched Chromium (bench only, optional)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **In-page runtime** — samples frames, maintains the ring buffer, detects
|
|
61
|
+
hitches, walks the scene for the census, executes bisection while paused.
|
|
62
|
+
- **Vite plugin** (`sloptimize/vite`) — dev-only (`apply: 'serve'`), exposes
|
|
63
|
+
`/__sloptimize/*` endpoints, lands payloads in `.sloptimize/`. Composes
|
|
64
|
+
with `@slopjs/inspector/vite`; requires it in v0 (see §2).
|
|
65
|
+
- **CLI** — `sloptimize check|bench|attribute|report|doctor`. The commands an
|
|
66
|
+
agent runs in its shell; also the hook's substrate.
|
|
67
|
+
- **MCP server** — optional, stdio, for the live paused-world tier.
|
|
68
|
+
- **Files** — the primary agent interface. File-first, tools-second, same as
|
|
69
|
+
slopjs: a hook or a `Read` needs no protocol.
|
|
70
|
+
|
|
71
|
+
### 1.1 Directory layout on disk (in the game project)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
.sloptimize/
|
|
75
|
+
profile.json rolling summary: current medians, p95, counters, regime
|
|
76
|
+
perf.jsonl hitch records, append-only
|
|
77
|
+
census.json per-entity cost census, from the last walk
|
|
78
|
+
bench/<name>.json one bench run, by snapshot name + timestamp
|
|
79
|
+
bench-history.jsonl the ledger: every bench comparison ever, append-only
|
|
80
|
+
goldens/<name>.png correctness-gate reference frames (gitignored)
|
|
81
|
+
budgets: in the game's source, via tune('perf.budget.*') — see §7
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`perf.jsonl` and `bench-history.jsonl` are **not** gitignored (they are the
|
|
85
|
+
audit trail); `goldens/` and `bench/` are.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 2. Platform contract with `@slopjs/inspector`
|
|
90
|
+
|
|
91
|
+
sloptimize peer-depends on the inspector and consumes only public or
|
|
92
|
+
to-be-published surfaces. v0 requires the inspector's vite plugin to be
|
|
93
|
+
present (it supplies the pause, the IDs, the bridge security model). The
|
|
94
|
+
**platform asks** — small additions the inspector must grow, to be proposed
|
|
95
|
+
as PRs there, listed here so the dependency is explicit:
|
|
96
|
+
|
|
97
|
+
| ask | why sloptimize needs it |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `onFrame(cb)` hook on the render interception (`src/discover.js` wrap) | the recorder's sampling point; called once per `renderer.render` with `{ renderer, scene, camera, timestamp }`. Without it we would re-wrap `render` and fight the inspector's own wrapper. |
|
|
100
|
+
| camera state included in `snapshot.capture()` / restored by `restore()` | identical workload requires identical view; today snapshots omit the camera. |
|
|
101
|
+
| a documented `captureFrame()` that works while paused | the correctness gate's golden frames; the inspector already solves `preserveDrawingBuffer` timing — expose it. |
|
|
102
|
+
| read access to the tracked registry (`all()`, `idOf`, `pathOf` — already exported) | census attribution to entity ids. Already public; listed for completeness. |
|
|
103
|
+
| `applyOp` visibility ops honored while paused with a forced re-render | measured bisection = toggle, re-render, read `renderer.info`. |
|
|
104
|
+
|
|
105
|
+
Until an ask lands upstream, the corresponding sloptimize feature ships
|
|
106
|
+
degraded and says so in `sloptimize doctor` (principle 4) — it does not
|
|
107
|
+
monkey-patch around the inspector.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 3. The flight recorder
|
|
112
|
+
|
|
113
|
+
### 3.1 Sampling
|
|
114
|
+
|
|
115
|
+
- Source: the `onFrame` hook (§2). Per frame, read `performance.now()` delta
|
|
116
|
+
between render calls **and** the wall time spent inside the render call
|
|
117
|
+
(wrap start/end), plus `renderer.info.render.{calls,triangles,points,lines}`,
|
|
118
|
+
`renderer.info.memory.{geometries,textures}`, `renderer.info.programs.length`.
|
|
119
|
+
- Cost budget: **≤ 0.2ms per frame, zero allocations on the steady path**
|
|
120
|
+
(pre-allocated ring slots; strings only at flush time). The recorder must
|
|
121
|
+
never be the hitch it reports.
|
|
122
|
+
- Ring buffer: 600 frames (~10s at 60fps) of full samples, in memory, always
|
|
123
|
+
on while the plugin is active — panel open or closed, paused or running.
|
|
124
|
+
Pause state is recorded per sample (`paused: true` frames are excluded from
|
|
125
|
+
budget checks but kept for continuity).
|
|
126
|
+
- Flush: every 2s (aligned with the inspector's refresh throttle), write
|
|
127
|
+
`.sloptimize/profile.json` — the rolling summary, not the ring.
|
|
128
|
+
- Per-frame strings the host may pass and the recorder stamps at mint:
|
|
129
|
+
`phase` (the host's moment axis — menu/boot/launch/match…) and `ctx` (the
|
|
130
|
+
host's canonical situation, §3.7). Both are strings the host caches and
|
|
131
|
+
hands along; the recorder allocates nothing for them.
|
|
132
|
+
|
|
133
|
+
### 3.2 `profile.json` (rolling summary)
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"at": "2026-08-23T12:00:00.000Z",
|
|
138
|
+
"regime": "hardware", // "hardware" | "software" | "unknown" — see §6.4
|
|
139
|
+
"window": { "frames": 600, "seconds": 10.4 },
|
|
140
|
+
"frame": { "medianMs": 8.1, "p95Ms": 15.2, "insideRenderMs": 4.2, "fps": 60 },
|
|
141
|
+
"render": { "calls": 940, "triangles": 1200000, "points": 0, "lines": 12 },
|
|
142
|
+
"memory": { "geometries": 312, "textures": 48, "programs": 17 },
|
|
143
|
+
"budgets": { "checked": 3, "breached": ["perf.budget.draw_calls"] },
|
|
144
|
+
"paused": false
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A field the runtime cannot measure is **absent**, never `0` (the inspector's
|
|
149
|
+
em-dash rule, inherited as JSON absence).
|
|
150
|
+
|
|
151
|
+
### 3.3 Hitch detection and `perf.jsonl`
|
|
152
|
+
|
|
153
|
+
A hitch is a non-paused frame whose delta exceeds
|
|
154
|
+
`max(2 × rolling median, budget frame ms × 1.5)` — relative and absolute
|
|
155
|
+
guards together, so a 30fps-by-design game does not report every frame. Per
|
|
156
|
+
hitch, append one record:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"type": "hitch",
|
|
161
|
+
"at": "2026-08-23T12:00:01.100Z",
|
|
162
|
+
"frame": 8412,
|
|
163
|
+
"frameMs": 41.0,
|
|
164
|
+
"medianMs": 8.1,
|
|
165
|
+
"insideRenderMs": 6.0,
|
|
166
|
+
"delta": {
|
|
167
|
+
"calls": +12, "triangles": +48000, "programs": +3, "textures": +2,
|
|
168
|
+
"geometries": 0
|
|
169
|
+
},
|
|
170
|
+
"world": {
|
|
171
|
+
"spawned": ["enemy_17", "enemy_18", "auto:mesh_1f2a"],
|
|
172
|
+
"removed": [],
|
|
173
|
+
"camera": { "position": [4.1, 12.0, -30.2], "quaternion": [0,0.7,0,0.7] },
|
|
174
|
+
"inputsHeld": ["KeyW", "Mouse0"]
|
|
175
|
+
},
|
|
176
|
+
"classification": {
|
|
177
|
+
"guess": "shader-compile",
|
|
178
|
+
"confidence": "medium",
|
|
179
|
+
"evidence": "programs +3 in the hitch frame"
|
|
180
|
+
},
|
|
181
|
+
"snapshotRef": "hitch-8412" // present iff auto-snapshot succeeded, §5
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Classification vocabulary (closed set, extensible only by spec change):
|
|
186
|
+
`shader-compile` (programs delta > 0), `texture-upload` (textures delta > 0,
|
|
187
|
+
programs 0), `spawn-burst` (spawned length above threshold),
|
|
188
|
+
`gc-or-upload-by-elimination` (no counter moved), `long-render`
|
|
189
|
+
(insideRenderMs dominates), `long-script` (frame delta dominates,
|
|
190
|
+
insideRenderMs small). Multiple guesses allowed, ranked. `confidence` is
|
|
191
|
+
`low | medium | high` and the `evidence` string is mandatory — a guess
|
|
192
|
+
without its reason is banned by principle 4.
|
|
193
|
+
|
|
194
|
+
Rate limit: at most 1 record per second and 500 per session; when the limit
|
|
195
|
+
truncates, the *last* record of the session says how many were dropped
|
|
196
|
+
(silence must mean nothing was dropped — the inspector's selection-block
|
|
197
|
+
rule, inherited).
|
|
198
|
+
|
|
199
|
+
### 3.5 Usermarks — the human's half of hitch detection
|
|
200
|
+
|
|
201
|
+
The automatic threshold cannot see "it feels wrong": steady-but-low fps,
|
|
202
|
+
micro-stutter under the hitch bar, jitter that only a hand on the mouse
|
|
203
|
+
notices. So the recorder exposes one more verb:
|
|
204
|
+
|
|
205
|
+
- `usermark({ windowMs = 5000, note, inputsHeld, world })` — freeze the
|
|
206
|
+
trailing window of the ring into one appended record: window frame count,
|
|
207
|
+
median/p95 over the window, and the **five worst frames ranked**, each with
|
|
208
|
+
its counter deltas and the same closed-vocabulary classification an
|
|
209
|
+
automatic hitch gets.
|
|
210
|
+
- The host binds it to a chord (the reference integration uses
|
|
211
|
+
**Ctrl+F11**) and shows a one-line confirmation. The press is the
|
|
212
|
+
human's timestamp; the ring is the evidence; the record is the labeled
|
|
213
|
+
training example an agent can read cold.
|
|
214
|
+
- Usermarks share `perf.jsonl` (type `"usermark"`), bypass the 1/s hitch
|
|
215
|
+
rate limit (a human press is already rate-limited by a human), and count
|
|
216
|
+
against the 500/session cap.
|
|
217
|
+
|
|
218
|
+
This section exists because the product's first field deployment asked for
|
|
219
|
+
exactly this, in the operator's words: "whenever a bottleneck or jitter is
|
|
220
|
+
detected, I can press a unique shortcut which will save the keyframe and use
|
|
221
|
+
all data recorded from the last 5 seconds to identify the bottleneck."
|
|
222
|
+
|
|
223
|
+
### 3.4 What the recorder does not do
|
|
224
|
+
|
|
225
|
+
No JS stack sampling (DevTools' job), no per-frame heap snapshots (cost), no
|
|
226
|
+
network waterfall. `performance.memory` (Chromium-only) is sampled at 1Hz
|
|
227
|
+
solely to support the `gc` classification, and its absence downgrades that
|
|
228
|
+
guess's confidence — it never blocks a record.
|
|
229
|
+
|
|
230
|
+
### 3.6 Coordinate continuity — jitter records
|
|
231
|
+
|
|
232
|
+
Hitches are about TIME. The second thing an operator feels is about SPACE:
|
|
233
|
+
"my player unit/camera's coordinate suddenly jumping instead of smoothly
|
|
234
|
+
transitioning at every frame". So the runtime ships a second detector,
|
|
235
|
+
`createMotionMonitor`, fed once per rendered frame with the world position
|
|
236
|
+
of each TRACK the host cares about — the reference integration feeds two:
|
|
237
|
+
|
|
238
|
+
- `unit` — the point the view is arranged around (the host's view pivot:
|
|
239
|
+
the eye on foot, the hull aboard, the spectated subject). Rotation-
|
|
240
|
+
invariant by construction, so a mouse flick cannot move it.
|
|
241
|
+
- `camera` — the eye itself, `follows: 'unit'`, with `reach` = its distance
|
|
242
|
+
to the pivot (the boom length), and HELD on every frame that carried look
|
|
243
|
+
or zoom input (a flick swings a boom metres in one frame — intended).
|
|
244
|
+
|
|
245
|
+
The test is a one-step constant-velocity prediction from the previous two
|
|
246
|
+
samples; the residual `r = p − p̂` is how far the point landed off its own
|
|
247
|
+
trajectory (½·a·dt² for smooth motion — millimetres). A residual is a
|
|
248
|
+
CANDIDATE when `|r| > max(floor, ratio × predicted travel)`; it becomes an
|
|
249
|
+
EVENT only when the next frame REVERSES it (compared as velocity anomalies,
|
|
250
|
+
so a jump landing in a 400ms stall frame is still confirmed by the 17ms
|
|
251
|
+
frame after it). A residual that is not reversed is a change of motion —
|
|
252
|
+
a dash, a boom easing back out — and is never reported. Consecutive events
|
|
253
|
+
(≤3 frames apart) fold into one burst: one event is a `snap`, two or more
|
|
254
|
+
an `oscillation`; the burst posts as ONE record when it closes (or every
|
|
255
|
+
2s while it continues).
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"type": "jitter",
|
|
260
|
+
"at": "2026-09-01T19:17:02.130Z",
|
|
261
|
+
"track": "unit",
|
|
262
|
+
"kind": "snap", // "snap" | "oscillation"
|
|
263
|
+
"frame": 8412,
|
|
264
|
+
"jump": [0.6, 0.3, 0], // the off-trajectory displacement, host units
|
|
265
|
+
"units": 0.671,
|
|
266
|
+
"travelUnits": 0.133, // what constant velocity predicted for this frame
|
|
267
|
+
"speed": 8.0, // host units per second before the jump
|
|
268
|
+
"dtMs": 16.7, "medianDtMs": 16.7,
|
|
269
|
+
"from": [12.1, 3.0, -4.2], "to": [12.83, 3.3, -4.2],
|
|
270
|
+
"frames": 41, "durationMs": 683, "amplitude": 0.3, // oscillation only
|
|
271
|
+
"coincident": ["camera"], // other tracks that jumped in the same frame
|
|
272
|
+
"reach": { "name": "boom", "before": 10.0, "after": 9.1 }, // if the host samples one
|
|
273
|
+
"classification": [ { "guess": "snap", "confidence": "high", "evidence": "…" } ],
|
|
274
|
+
"phase": "play", "build": "v1788289826333"
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Classification vocabulary (closed): `snap`, `oscillation`,
|
|
279
|
+
`long-frame-catch-up` (the jump landed in a frame ≥ the host's `longFrameMs`
|
|
280
|
+
— its sim's dt clamp, default the 100ms hitch bar — or ≥2× the median and
|
|
281
|
+
≥50ms: past the clamp the point moves by the clamp while the prediction
|
|
282
|
+
scales with the clock, so the residual is the stall's, not a jump's),
|
|
283
|
+
`follows-track` (the track jumped in the same frame as the one it declares
|
|
284
|
+
it follows — the passenger, not the cause), `reach-change` (the point's
|
|
285
|
+
distance to its anchor changed by ≈ the jump — a boom clamp or a zoom, not
|
|
286
|
+
a teleport). Explanations rank ahead of the kind. Every guess carries
|
|
287
|
+
evidence.
|
|
288
|
+
|
|
289
|
+
Host contract: the host says which frames are HELD (input-driven, paused)
|
|
290
|
+
and when the view was CUT on purpose (camera mode flip, spectate target
|
|
291
|
+
change, respawn, session boundary) — the detector never guesses intent.
|
|
292
|
+
Rate limits: one record per second PER TRACK (a unit that teleports takes
|
|
293
|
+
its camera with it in the same frame, and the camera's record is the one
|
|
294
|
+
that says so), 200 per session, drops counted onto the next record.
|
|
295
|
+
|
|
296
|
+
Stated limits: a pure rotation pop (a yaw snap) moves no coordinate and is
|
|
297
|
+
not detected; a snap that coincides with an equal-and-opposite velocity
|
|
298
|
+
change reads as a change of motion; the camera is judged only on frames
|
|
299
|
+
without look input, so a pop under a moving mouse is missed (the unit
|
|
300
|
+
track still sees it if the pivot moved).
|
|
301
|
+
|
|
302
|
+
### 3.7 Footprints, situation, and the issue catalogue
|
|
303
|
+
|
|
304
|
+
A ledger line is an OCCURRENCE. Two lines a day apart, on two builds, on
|
|
305
|
+
two players' machines, are very often the same thing happening again, and
|
|
306
|
+
a log that cannot say so cannot answer "how often does this happen" or
|
|
307
|
+
"what did we do about it". So every incident record carries a
|
|
308
|
+
**footprint**: the identity of its cause, apart from its occurrence.
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
"footprint": { "v": 1, "id": "a3f92c1d", "key": "jitter|unit|snap|play|snap|horizontal|ctx:hull=elong-x,squad=duo,stance=helm" }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `key` is readable and is exactly what was hashed: the record type, the
|
|
315
|
+
phase, the closed-vocabulary verdict, whatever identifies the SITE (a
|
|
316
|
+
hitch's minted `material@object` pairs, sorted and deduped; a jitter's
|
|
317
|
+
track, kind and dominant axis — vertical vs horizontal; a warm's tag and
|
|
318
|
+
kind), and the host's situation (below). Never the timestamp, the frame
|
|
319
|
+
number, the exact milliseconds or metres, the build, the machine.
|
|
320
|
+
- `id` is FNV-1a 32 of `v<version>:<key>`, 8 hex characters — a dedupe key
|
|
321
|
+
over thousands of causes, stable across runtimes, no dependency.
|
|
322
|
+
- `v` versions the derivation. A change to what enters a key is a new
|
|
323
|
+
version, never a silent reshuffle of old ids; a reader re-derives a
|
|
324
|
+
record stamped with an older version and keeps one stamped with the
|
|
325
|
+
current one (the writer's word stands).
|
|
326
|
+
|
|
327
|
+
**Situation.** Time is not a facet of the cause; the game's STATE is. A
|
|
328
|
+
hitch at the helm of a heavy machine with a copilot aboard in a firefight
|
|
329
|
+
is not the same issue as the same hitch on foot in an empty lobby, and no
|
|
330
|
+
profiler can know which facets matter for which game. So the host declares
|
|
331
|
+
them: `context()` returns a few LOW-CARDINALITY facets — categories, never
|
|
332
|
+
positions or counters — the runtime canonicalises them once a second
|
|
333
|
+
(`canonicalContext`: keys sorted, `k=v` pairs joined by `,`, separators
|
|
334
|
+
scrubbed) and hands the string to the recorder per frame (`frame({ ctx })`,
|
|
335
|
+
`usermark({ ctx })`, the motion monitor's `meta.ctx`), which stamps it on
|
|
336
|
+
the record as `ctx`; host-built records take the current one at post. The
|
|
337
|
+
reference integration's facets: `stance` (foot|helm|crew|dead), `hull`
|
|
338
|
+
(the frame's name), `squad` (solo|duo|trio), `view` (fp|tps), `combat`
|
|
339
|
+
(yes|no, a ten-second window after any hit).
|
|
340
|
+
|
|
341
|
+
**The catalogue.** `buildIssues(records, { fixes, from, to, now })` folds a
|
|
342
|
+
ledger by footprint: one row per id with `count`, `first`, `last`,
|
|
343
|
+
`lastAgoMs`, `builds` seen, `worst` (with its unit), the last verdict's
|
|
344
|
+
evidence, the parsed situation facets, and `fixes` — every fix record whose
|
|
345
|
+
`footprints` names the id. Robots' records (`automated: true`) are left out
|
|
346
|
+
unless asked for. A fix names what it addresses:
|
|
347
|
+
`sloptimize fix propose --footprints a3f92c1d,… --title "…"` (also on
|
|
348
|
+
`sloptimize fix`); the catalogue answers "which fixes were applied to this
|
|
349
|
+
issue" by that join, and the before/after windows of each say whether it
|
|
350
|
+
landed.
|
|
351
|
+
|
|
352
|
+
Surfaces: `sloptimize issues` (the catalogue; `--fp <id>` for one issue's
|
|
353
|
+
history), `sloptimize report` (the top five), every `watch` wake line
|
|
354
|
+
(`fp=a3f92c1d ×7` — the count is the ledger's, seeded at arm), and the
|
|
355
|
+
debugger's **Issues** tab: every incident type grouped by footprint, most
|
|
356
|
+
frequent first, with `×N` and `last 3h ago`; picking a row opens its
|
|
357
|
+
history — key, first and last seen, builds, worst, last verdict, and the
|
|
358
|
+
fixes applied to it, or the exact command that would record one.
|
|
359
|
+
|
|
360
|
+
Cloud path: the footprint is computed by the writer, so a service that
|
|
361
|
+
ingests many clients' records dedupes on `footprint.id` from day one; the
|
|
362
|
+
fold is the same code.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 4. Census and attribution
|
|
367
|
+
|
|
368
|
+
### 4.1 Static census (`census.json`)
|
|
369
|
+
|
|
370
|
+
Produced by a full scene walk on demand (`sloptimize attribute --static`, an
|
|
371
|
+
MCP call, or the panel) and after every `inspector check`-style scan; never
|
|
372
|
+
per-frame. Per tracked entity (and per auto-tracked node, flagged):
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"at": "2026-08-23T12:00:00.000Z",
|
|
377
|
+
"totals": { "calls": null, "meshes": 1240, "triangles": 1200000,
|
|
378
|
+
"uniqueMaterials": 41, "uniqueGeometries": 220,
|
|
379
|
+
"textureBytesEstimate": 182000000 },
|
|
380
|
+
"entities": [
|
|
381
|
+
{
|
|
382
|
+
"id": "gltf_town",
|
|
383
|
+
"meshes": 611,
|
|
384
|
+
"triangles": 604000,
|
|
385
|
+
"uniqueMaterials": 9,
|
|
386
|
+
"uniqueGeometries": 34,
|
|
387
|
+
"sharedGeometryGroups": [
|
|
388
|
+
{ "geometry": "auto:geo_a91c", "material": "auto:mat_2210",
|
|
389
|
+
"count": 500, "instanced": false }
|
|
390
|
+
],
|
|
391
|
+
"textureBytesEstimate": 96000000,
|
|
392
|
+
"castShadow": 611,
|
|
393
|
+
"visible": true,
|
|
394
|
+
"persistent": true
|
|
395
|
+
}
|
|
396
|
+
],
|
|
397
|
+
"hints": [
|
|
398
|
+
{
|
|
399
|
+
"kind": "instancing-candidate",
|
|
400
|
+
"entity": "gltf_town",
|
|
401
|
+
"detail": "500 meshes share one geometry+material and are not instanced",
|
|
402
|
+
"estimate": { "callsBefore": 940, "callsAfter": 441 },
|
|
403
|
+
"fix": "merge into one InstancedMesh at the construction site of gltf_town"
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`totals.calls` is `null` in the static census — draw calls are a runtime
|
|
410
|
+
fact, and the census does not pretend to know it (principle 4); `profile.json`
|
|
411
|
+
carries the measured number.
|
|
412
|
+
|
|
413
|
+
Hint vocabulary (closed set): `instancing-candidate`,
|
|
414
|
+
`material-dedup-candidate` (N materials with identical parameters),
|
|
415
|
+
`shadow-caster-light` (shadow-casting point/spot lights, with their map
|
|
416
|
+
cost), `undisposed-suspect` (geometries/textures count monotonically growing
|
|
417
|
+
across censuses), `oversized-texture` (dimension threshold),
|
|
418
|
+
`uncapped-pixel-ratio`. Texture bytes are labeled `Estimate` in the field
|
|
419
|
+
name because that is what they are (width×height×format guess, no VRAM
|
|
420
|
+
introspection in WebGL).
|
|
421
|
+
|
|
422
|
+
### 4.2 Measured bisection (`attribute_cost`)
|
|
423
|
+
|
|
424
|
+
Protocol, executed by the in-page runtime while **paused** (requires the
|
|
425
|
+
inspector's pause; refuses to run otherwise with a structured error):
|
|
426
|
+
|
|
427
|
+
1. Render K frames (default 5), record baseline `renderer.info` counters and
|
|
428
|
+
median inside-render ms.
|
|
429
|
+
2. For each candidate (default: top 20 census entities by triangles, or an
|
|
430
|
+
explicit id list): set the entity's root `visible = false` via the
|
|
431
|
+
inspector's own `applyOp`, render K frames, record, restore visibility.
|
|
432
|
+
3. Emit per-entity deltas: `calls`, `triangles`, `insideRenderMs` (the last
|
|
433
|
+
marked `noisy: true` when below the measured per-run spread).
|
|
434
|
+
4. Restore the world exactly (ops applied through the same `applyOp` path
|
|
435
|
+
means undo history stays coherent; the run ends with a verification that
|
|
436
|
+
post-state === pre-state, and reports if not).
|
|
437
|
+
|
|
438
|
+
Output ranks entities by measured contribution and carries the caveat as
|
|
439
|
+
data, not prose: `"sumsToBaseline": false` — hiding a shadow caster changes
|
|
440
|
+
shadow-map cost, batching state shifts; this is a **ranking instrument, not
|
|
441
|
+
an accounting identity**, and every consumer (skill, panel) must render that
|
|
442
|
+
caveat.
|
|
443
|
+
|
|
444
|
+
### 4.3 Attribution to source
|
|
445
|
+
|
|
446
|
+
Every census/bisection row carries the entity id; ids resolve to construction
|
|
447
|
+
sites through the inspector's existing writeback tooling (`locate`). The
|
|
448
|
+
hint's `fix` string names the site when `locate` finds one, and says
|
|
449
|
+
`site: unknown` when it does not — auto-tracked (`auto:`) entities always
|
|
450
|
+
say so, with the standing advice to `track()` them.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 5. Hitch reproduction
|
|
455
|
+
|
|
456
|
+
On a hitch, the recorder requests `snapshot.capture()` (with camera, per the
|
|
457
|
+
§2 ask) for the frame `k = 30` frames before the spike, using the snapshot
|
|
458
|
+
system's HMR-carry machinery, and saves it as `hitch-<frame>`. The record's
|
|
459
|
+
`snapshotRef` names it. Reproduction is then:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
sloptimize repro hitch-8412 # CLI wrapper, or the MCP equivalent:
|
|
463
|
+
snapshot.load('hitch-8412'); clock.step(35); # step across the hitch, reading counters
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Scope, stated as data:** the repro record carries
|
|
467
|
+
`"fidelity": "workload"` — snapshots hold tracked transforms, visibility,
|
|
468
|
+
tunables, camera; not physics internals, mixer time, RNG, or spawn queues. A
|
|
469
|
+
restore *looks* identical and *evolves* differently. This is strong for
|
|
470
|
+
draw/material/fill workloads and weak for simulation-CPU spikes, and the
|
|
471
|
+
skill says exactly that. Trajectory-fidelity replay (seeded RNG, fixed
|
|
472
|
+
timestep, keyframe chains) is **out of scope for v1** and enters the spec
|
|
473
|
+
only if workload repro is demonstrated insufficient on real cases.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 6. The bench
|
|
478
|
+
|
|
479
|
+
### 6.1 Contract
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
sloptimize bench --snapshot town-square [--frames 600] [--launch] [--json]
|
|
483
|
+
sloptimize bench --compare A B [--json] # or --compare last
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
A bench run: restore the named snapshot (camera included), pin `clock` to a
|
|
487
|
+
fixed 1/60s tick, run N frames, discard the first 120 (warmup: shader
|
|
488
|
+
compiles, JIT), record per-frame deltas and counters, repeat the whole run R
|
|
489
|
+
times (default 3), report per-metric median and p95 across runs plus the
|
|
490
|
+
inter-run spread — the **noise floor**.
|
|
491
|
+
|
|
492
|
+
### 6.2 Output (`bench/<name>.json`)
|
|
493
|
+
|
|
494
|
+
```json
|
|
495
|
+
{
|
|
496
|
+
"name": "town-square",
|
|
497
|
+
"at": "2026-08-23T12:05:00.000Z",
|
|
498
|
+
"regime": "hardware",
|
|
499
|
+
"environment": { "launched": true, "headless": "new", "gpu": "ANGLE (Apple M2)",
|
|
500
|
+
"configHash": "sha256:9f2c…" },
|
|
501
|
+
"frames": 600, "runs": 3,
|
|
502
|
+
"frame": { "medianMs": 9.1, "p95Ms": 14.8, "noiseFloorMs": 0.9 },
|
|
503
|
+
"render": { "calls": 24, "triangles": 1200000, "programs": 14 },
|
|
504
|
+
"grade": { "counters": "exact", "timing": "hardware" }
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`configHash` covers snapshot content, frame count, runs, viewport, and
|
|
509
|
+
pixelRatio — two reports compare only if hashes match, and `--compare`
|
|
510
|
+
refuses otherwise (exit 3).
|
|
511
|
+
|
|
512
|
+
### 6.3 Comparison verdicts
|
|
513
|
+
|
|
514
|
+
`--compare` emits exactly one verdict per metric and one overall:
|
|
515
|
+
|
|
516
|
+
- `improved` / `regressed`: the delta exceeds the pooled noise floor.
|
|
517
|
+
- `no-detectable-change`: inside the noise floor. **This is a result**, not a
|
|
518
|
+
failure, and the skill maps it to "revert".
|
|
519
|
+
- Counters compare exactly (no noise floor — they are deterministic).
|
|
520
|
+
- Overall verdict additionally reports **distance-to-budget** for every
|
|
521
|
+
breached budget: `"frame p95 14.8ms; budget 16.7ms: inside"` — because
|
|
522
|
+
agents satisfice, and "improved" without "are we there" invites stopping
|
|
523
|
+
early.
|
|
524
|
+
|
|
525
|
+
Exit codes: `0` improved-or-inside-budgets, `1` regressed, `2`
|
|
526
|
+
no-detectable-change, `3` incomparable, `4` environment failure (no browser,
|
|
527
|
+
no snapshot). The codes are API; the skill depends on them.
|
|
528
|
+
|
|
529
|
+
### 6.4 Truth grades and regimes
|
|
530
|
+
|
|
531
|
+
- `counters` grade — draw calls, triangles, programs, census: **exact on any
|
|
532
|
+
renderer including SwiftShader.** CI-safe.
|
|
533
|
+
- `timing` grade — frame ms, inside-render ms, GPU ms: meaningful only in
|
|
534
|
+
`regime: "hardware"`. Regime detection: `WEBGL_debug_renderer_info`
|
|
535
|
+
unmasked renderer string matched against software rasterizers (SwiftShader,
|
|
536
|
+
llvmpipe); unknown → `"unknown"` and timing is reported but flagged. A
|
|
537
|
+
timing comparison across regimes is refused (exit 3). **An unlabeled
|
|
538
|
+
number is a lie waiting for a model to believe it.**
|
|
539
|
+
- GPU ms (optional): via `stats-gl`'s timer-query mechanism when the optional
|
|
540
|
+
peer is installed and the extension exists — Chromium-only, and reported as
|
|
541
|
+
absent elsewhere, never estimated.
|
|
542
|
+
|
|
543
|
+
### 6.5 The launched browser (`--launch`)
|
|
544
|
+
|
|
545
|
+
- Default: bench against the already-open dev tab through the plugin bridge
|
|
546
|
+
(the developer's real GPU, zero new dependencies).
|
|
547
|
+
- `--launch`: spawn Chromium for an unattended run — required for `/loop`
|
|
548
|
+
and CI. Binary discovery order: `SLOPTIMIZE_BROWSER` env var →
|
|
549
|
+
`playwright-core`'s managed Chromium if the optional peer is installed →
|
|
550
|
+
well-known system paths (the inspector's `verify-zero-config.mjs` CDP
|
|
551
|
+
approach, productized). No browser found → exit 4 with the install hint.
|
|
552
|
+
- A launched browser uses a fresh profile, fixed 1280×720 viewport,
|
|
553
|
+
`--disable-extensions`, pinned `devicePixelRatio: 1` — reproducibility
|
|
554
|
+
beats realism for verdicts.
|
|
555
|
+
- The whole launcher is **Chromium-only by design**, stated in `doctor`.
|
|
556
|
+
|
|
557
|
+
### 6.6 The ledger (`bench-history.jsonl`)
|
|
558
|
+
|
|
559
|
+
Every `--compare` appends one line, machine-written only:
|
|
560
|
+
|
|
561
|
+
```json
|
|
562
|
+
{
|
|
563
|
+
"at": "2026-08-23T12:06:00.000Z",
|
|
564
|
+
"snapshot": "town-square",
|
|
565
|
+
"change": { "ref": "git:1a2b3c4", "description": "instance gltf_town rocks" },
|
|
566
|
+
"before": { "calls": 940, "p95Ms": 21.4 },
|
|
567
|
+
"after": { "calls": 24, "p95Ms": 9.1 },
|
|
568
|
+
"verdict": "improved",
|
|
569
|
+
"budgets": { "perf.budget.draw_calls": "inside", "perf.budget.frame_ms_p95": "inside" },
|
|
570
|
+
"configHash": "sha256:9f2c…"
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The ledger is the loop's memory (§8): an iteration reads it before acting,
|
|
575
|
+
so a reverted strategy is never retried and progress is auditable after the
|
|
576
|
+
fact. It is append-only by contract; the CLI has no delete/edit verb.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## 7. Budgets and `sloptimize check`
|
|
581
|
+
|
|
582
|
+
Budgets are declared in game source through the inspector's own `tune()`:
|
|
583
|
+
|
|
584
|
+
```js
|
|
585
|
+
tune('perf.budget.draw_calls', 300, { min: 1, max: 10000 });
|
|
586
|
+
tune('perf.budget.frame_ms_p95', 16.7, { min: 1, max: 100 });
|
|
587
|
+
tune('perf.budget.triangles', 2_000_000, { min: 1, max: 100_000_000 });
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Rationale: budgets live in **reviewed source** (a diff to a budget is loud in
|
|
591
|
+
a PR — the anti-gaming posture, §9), are visible in the inspector's Tunables
|
|
592
|
+
panel like any other tunable, and need no new config format.
|
|
593
|
+
|
|
594
|
+
```
|
|
595
|
+
sloptimize check # reads profile.json (or runs a bench with --bench)
|
|
596
|
+
budgets: 3 checked, 1 breached
|
|
597
|
+
perf.budget.draw_calls 940 / 300 ← over by 3.1×
|
|
598
|
+
exit 1
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Exit `0` when all budgets pass, `1` on any breach, `4` when no measurement
|
|
602
|
+
exists to check against (never a silent pass). `--counters-only` restricts to
|
|
603
|
+
the exact grade for CI on GPU-less machines. In a project with no
|
|
604
|
+
`perf.budget.*` tunables, `check` reports "no budgets declared" and exits 0
|
|
605
|
+
with a warning — budgets are opt-in, but their absence is said out loud.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## 8. Agent integration — the whole point
|
|
610
|
+
|
|
611
|
+
### 8.1 Surfaces, in order of preference
|
|
612
|
+
|
|
613
|
+
1. **Files** (`.sloptimize/*`) — read with the agent's own Read tool. No
|
|
614
|
+
protocol, no server needed. The primary interface.
|
|
615
|
+
2. **CLI** — every command supports `--json` and meaningful exits. What the
|
|
616
|
+
agent runs in Bash, what hooks wrap, what CI calls.
|
|
617
|
+
3. **Hook** — `sloptimize hook-status` prints a ≤5-line block for
|
|
618
|
+
`UserPromptSubmit`: current regime, p95 vs budget, breached budgets, last
|
|
619
|
+
hitch classification. Installed alongside the inspector's selection hook;
|
|
620
|
+
silent (exit 0, no output) when nothing is running or nothing is breached
|
|
621
|
+
— ambient perf the way selection is ambient, and only when it matters.
|
|
622
|
+
4. **MCP** (optional) — the live tier: `get_profile`, `get_census`,
|
|
623
|
+
`attribute_cost`, `run_bench`, `get_ledger`, `repro_hitch`. Same
|
|
624
|
+
structured-error-never-hang contract as the inspector's server.
|
|
625
|
+
|
|
626
|
+
### 8.1.1 The push channel — auto mode
|
|
627
|
+
|
|
628
|
+
The surfaces above are pull: the agent reads when it acts or when a prompt
|
|
629
|
+
hook fires. The first field deployment closed the loop the other direction
|
|
630
|
+
— "auto identifies bottleneck, sends signal to claude code, I just play":
|
|
631
|
+
|
|
632
|
+
- The RECORDER is already the auto-detector (§3.3); no keypress is involved
|
|
633
|
+
in a hitch record existing.
|
|
634
|
+
- A session-side WATCHER (Claude Code's Monitor primitive, a 20s poll over
|
|
635
|
+
`perf.jsonl` with a byte cursor) turns each new meaningful record — an
|
|
636
|
+
auto hitch ≥100ms, any usermark, a coordinate jitter (§3.6) — into an
|
|
637
|
+
agent wake event carrying the classification line. The agent starts the
|
|
638
|
+
§8.2 playbook unprompted.
|
|
639
|
+
- `perf.jsonl`'s record schema is therefore a PUSH CONTRACT, not just an
|
|
640
|
+
audit trail: fields added to records surface directly in wake events.
|
|
641
|
+
|
|
642
|
+
Usermarks (§3.5) become the OPTIONAL manual channel of the same pipe:
|
|
643
|
+
auto mode covers everything over the threshold; Ctrl+F11 covers what only a
|
|
644
|
+
human can feel below it. Both arrive the same way and neither requires the
|
|
645
|
+
operator to type anything.
|
|
646
|
+
|
|
647
|
+
### 8.2 Doctrine (SKILL.md + CLAUDE.md fragment)
|
|
648
|
+
|
|
649
|
+
Ships in-package (`skills/sloptimize/SKILL.md`) and is scaffolded into
|
|
650
|
+
projects the same way the inspector's skill is. The rules it must contain,
|
|
651
|
+
verbatim in spirit:
|
|
652
|
+
|
|
653
|
+
1. **Never claim a performance fix without a bench comparison.** A fix
|
|
654
|
+
without a `bench --compare` verdict is a hypothesis.
|
|
655
|
+
2. The playbook: read `perf.jsonl` → classify → `attribute` → **one change**
|
|
656
|
+
→ `bench --compare` → report the verdict *and* distance-to-budget.
|
|
657
|
+
3. Map verdicts mechanically: improved → commit; no-detectable-change →
|
|
658
|
+
revert; regressed → revert. No exceptions without the human.
|
|
659
|
+
4. Read the ledger before optimizing — do not retry a reverted strategy.
|
|
660
|
+
5. Trust grades: never quote a `timing` number from a `software` regime;
|
|
661
|
+
never present census estimates as measurements.
|
|
662
|
+
6. Escalation seams: CPU-bound in script → Chrome DevTools MCP trace (which
|
|
663
|
+
function); need per-draw GL state → Spector capture (which command).
|
|
664
|
+
sloptimize answers *which entity, which frame, which workload* — only.
|
|
665
|
+
|
|
666
|
+
### 8.3 The `/loop` iteration contract
|
|
667
|
+
|
|
668
|
+
The self-iteration mode this product exists for. One iteration:
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
1. sloptimize check --json → all inside? stop: done.
|
|
672
|
+
2. read .sloptimize/bench-history.jsonl (ledger) + perf.jsonl + census.json
|
|
673
|
+
3. pick ONE change, excluding strategies the ledger shows reverted
|
|
674
|
+
4. apply the change (ordinary code edit)
|
|
675
|
+
5. sloptimize bench --snapshot <canonical> --launch --compare last --json
|
|
676
|
+
6. correctness gate (§8.4): fails → revert, record, count a strike
|
|
677
|
+
7. verdict improved → commit (message carries the ledger line)
|
|
678
|
+
otherwise → revert
|
|
679
|
+
8. stop conditions: budgets pass | 3 consecutive no-detectable-change |
|
|
680
|
+
2 correctness strikes | iteration cap reached. Else loop.
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Every step is a command or a file read; no step requires judgment about
|
|
684
|
+
*whether* to proceed — only step 3 requires intelligence, which is the
|
|
685
|
+
division of labor: **the tool decides what is true; the agent decides what
|
|
686
|
+
to try.** The canonical bench snapshot is chosen by the human once (a
|
|
687
|
+
representative heavy scene) and named in the skill — the agent must not
|
|
688
|
+
invent its own benchmark workload (LM-generated perf tests miss the real
|
|
689
|
+
regression roughly half the time in the literature).
|
|
690
|
+
|
|
691
|
+
### 8.4 The correctness gate
|
|
692
|
+
|
|
693
|
+
`sloptimize gate --snapshot <name>`: restore snapshot, `clock.step(1)`,
|
|
694
|
+
capture the frame, perceptual-diff (SSIM, threshold configurable, default
|
|
695
|
+
0.98) against `goldens/<name>.png`. Exit 0 pass / 1 fail / 4 no golden.
|
|
696
|
+
Goldens are created **only** by an explicit `sloptimize gate --record`,
|
|
697
|
+
which the doctrine reserves for the human (or a human-approved step); the
|
|
698
|
+
skill forbids the agent from re-recording a golden to make a failure pass,
|
|
699
|
+
and a re-recorded golden is a loud artifact in any review of `.sloptimize/`
|
|
700
|
+
timestamps. The gate complements — never replaces — the project's own tests.
|
|
701
|
+
|
|
702
|
+
### 8.5 The timeline and the fix ledger — showing the work
|
|
703
|
+
|
|
704
|
+
The ledger already holds the whole history of a deployment (every line
|
|
705
|
+
self-sufficient: `build`, `phase`, timestamp), so "has sloptimize actually
|
|
706
|
+
made the game faster" must be answerable by FOLDING it, never by a number
|
|
707
|
+
someone typed. Three surfaces, one fold (`src/history.js`, pure, shared by
|
|
708
|
+
node and the page):
|
|
709
|
+
|
|
710
|
+
- **Timeline** — `perf.jsonl` in equal time buckets: frame p95 and median,
|
|
711
|
+
draw calls / triangles / programs (medians of the heartbeats, which carry
|
|
712
|
+
the counters for this reason), hitch count and worst frame per bucket,
|
|
713
|
+
and the build that ran. Plus one measured window per build. CLI:
|
|
714
|
+
`sloptimize history [--json] [--buckets N]`; MCP: `get_history`.
|
|
715
|
+
- **Fix ledger** — `.sloptimize/fixes.jsonl`, append-only, one record per
|
|
716
|
+
verified fix: `title`, `issue`, `solution`, `commit`, `files`, `at`, and
|
|
717
|
+
`before`/`after` — each a **measured window** of the ledger (the previous
|
|
718
|
+
build vs the latest build with evidence by default; or any build name or
|
|
719
|
+
`<ISO>..<ISO>` range) carrying the window summary and a p95 series. CLI:
|
|
720
|
+
`sloptimize fix --title … [--issue … --solution … --commit … --before …
|
|
721
|
+
--after …]`; MCP: `record_fix`. The doctrine's last step.
|
|
722
|
+
- **The in-game debugger** (`src/panel.js`, `createPanel`) — the human's
|
|
723
|
+
Ctrl+F12 modal, four tabs: **Current Session** (this tab's incidents, each
|
|
724
|
+
with its footprint id, + the note box), **Issues** (§3.7: every incident
|
|
725
|
+
type grouped by footprint, `×N`, last seen, the situation as chips; a row
|
|
726
|
+
opens its history and the fixes applied), **Optimizations** (the strips
|
|
727
|
+
above on one time axis, build boundaries marked, a shared crosshair, and
|
|
728
|
+
one card per fix: date, commit, was → now, before/after sparklines and
|
|
729
|
+
deltas, merge/reject for proposals), **Settings** (the fix loop's
|
|
730
|
+
automation level). The host serves the ledger back over a dev-gated GET
|
|
731
|
+
(the reference: `/api/sloptimize/ledger`, the 2MB tail of `perf.jsonl` +
|
|
732
|
+
`fixes.jsonl`); the page folds it, so the Issues tab covers that tail and
|
|
733
|
+
the CLI covers the whole file.
|
|
734
|
+
|
|
735
|
+
Honesty rules, inherited: a bucket with no evidence draws a gap, never a
|
|
736
|
+
zero; a spike past a strip's ceiling (1.5 × p90) is drawn at the ceiling
|
|
737
|
+
with a caret and read out at its real value; one series per strip, never a
|
|
738
|
+
dual axis; every metric shown is lower-is-better and the delta is colored
|
|
739
|
+
by direction only.
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## 9. Anti-gaming posture
|
|
744
|
+
|
|
745
|
+
A local agent with write access can edit budgets, goldens, this package, or
|
|
746
|
+
the ledger file itself. That cannot be prevented and this spec does not
|
|
747
|
+
pretend otherwise. It can be priced and exposed:
|
|
748
|
+
|
|
749
|
+
- Budgets in reviewed source (§7): moving a goalpost is a visible diff.
|
|
750
|
+
- The ledger is append-only by contract and machine-written; the CLI offers
|
|
751
|
+
no edit verb, and each entry carries the `configHash` — a comparison
|
|
752
|
+
against a mutated config self-identifies.
|
|
753
|
+
- Goldens regenerate only via the explicit human-reserved command (§8.4).
|
|
754
|
+
- Bench numbers are produced by this package's harness, not by
|
|
755
|
+
agent-authored probe code; the skill forbids hand-rolled measurement
|
|
756
|
+
scripts when the harness exists.
|
|
757
|
+
- The final backstop is unchanged from all agent work: a human reads the PR,
|
|
758
|
+
where the ledger lines in commit messages make the claimed trajectory
|
|
759
|
+
checkable in minutes.
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## 10. Stated limits (what sloptimize will say it cannot do)
|
|
764
|
+
|
|
765
|
+
Printed by `sloptimize doctor`, documented in the README, never silently
|
|
766
|
+
degraded:
|
|
767
|
+
|
|
768
|
+
- **No per-draw GPU timing.** WebGL's timer extension allows one non-nested
|
|
769
|
+
query scope, results arrive frames late, and pipelined draws do not sum to
|
|
770
|
+
frame time. GPU ms is frame-level, Chromium-only, via stats-gl, or absent.
|
|
771
|
+
- **Fill-rate/overdraw attribution is heuristic only** (pixel-ratio
|
|
772
|
+
experiment: fps recovers when resolution drops → fill-bound), and the
|
|
773
|
+
verdict says "heuristic".
|
|
774
|
+
- **Costs from bisection do not sum** (§4.2) — ranking, not accounting.
|
|
775
|
+
- **Timing from software renderers is flagged and never compared** (§6.4).
|
|
776
|
+
- **Workload repro, not trajectory repro** (§5).
|
|
777
|
+
- **Simulation-CPU spikes attribute poorly** — the classification will say
|
|
778
|
+
`long-script` and the escalation seam points at Chrome DevTools MCP.
|
|
779
|
+
- **EffectComposer/custom-pipeline hosts** inherit the inspector's
|
|
780
|
+
interception gap; the recorder degrades to rAF-delta-only sampling there
|
|
781
|
+
and `doctor` names it.
|
|
782
|
+
- **Multiplayer** is out of scope for the same reason it is out of scope for
|
|
783
|
+
the inspector's pause: you cannot pause or restore the server.
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## 11. Dependency policy
|
|
788
|
+
|
|
789
|
+
Hard runtime dependencies: **none** (the slopjs posture). Peers:
|
|
790
|
+
`@slopjs/inspector` (required), `three` (required),
|
|
791
|
+
`playwright-core` (optional — managed browser for `--launch`),
|
|
792
|
+
`stats-gl` (optional — GPU ms). Dev-only tooling may use whatever it needs.
|
|
793
|
+
The vite plugin activates only under `serve`; a production build contains
|
|
794
|
+
nothing of this package.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## 12. Milestones
|
|
799
|
+
|
|
800
|
+
- **M0 — recorder.** In-page runtime + vite plugin + `profile.json` +
|
|
801
|
+
`perf.jsonl` + hitch classification + `sloptimize doctor`. The platform
|
|
802
|
+
ask: `onFrame`. *Exit criterion: a hitch that happened before the panel was
|
|
803
|
+
ever opened is explained from the file alone.*
|
|
804
|
+
- **M1 — census + hints.** `census.json`, hint vocabulary, `sloptimize
|
|
805
|
+
attribute --static`, id→site resolution via `locate`. *Exit: the
|
|
806
|
+
instancing-candidate hint names a real site in a real vibe-coded game.*
|
|
807
|
+
- **M2 — budgets + check + hook.** `tune('perf.budget.*')` convention,
|
|
808
|
+
`sloptimize check` exit codes, `hook-status`. *Exit: CI fails a PR that
|
|
809
|
+
doubles draw calls, on a GPU-less runner, counters-only.*
|
|
810
|
+
- **M3 — bench + ledger + gate.** Snapshot-pinned bench, `--launch`,
|
|
811
|
+
compare verdicts, noise floors, `bench-history.jsonl`, the SSIM gate.
|
|
812
|
+
Platform asks: snapshot camera, `captureFrame`. *Exit: the same fix
|
|
813
|
+
benched twice yields the same verdict; a no-op change yields
|
|
814
|
+
no-detectable-change.*
|
|
815
|
+
- **M4 — bisection + MCP + doctrine.** `attribute_cost`, the MCP server, the
|
|
816
|
+
skill, the `/loop` contract end to end. *Exit: a `/loop` session takes a
|
|
817
|
+
seeded 2,000-draw-call scene inside budgets unattended, with a clean
|
|
818
|
+
ledger and zero gate failures.*
|
|
819
|
+
|
|
820
|
+
M0 is independently shippable and useful; each milestone is a publishable
|
|
821
|
+
release.
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## 13. Open questions (tracked, not blocking M0)
|
|
826
|
+
|
|
827
|
+
1. Should `hook-status` and the inspector's selection hook merge into one
|
|
828
|
+
block to save prompt tokens, or stay separable products? (Leaning:
|
|
829
|
+
separable, one line each.)
|
|
830
|
+
2. Census texture-byte estimation for compressed formats (KTX2/basis) —
|
|
831
|
+
estimate decoded or on-disk size? (Leaning: decoded, labeled.)
|
|
832
|
+
3. Does the bench restore path need `loadLayout` awareness for scenes built
|
|
833
|
+
from layout files, or does snapshot restore subsume it? (Needs a real
|
|
834
|
+
test scene.)
|
|
835
|
+
4. WebGPU (`WebGPURenderer`): `renderer.info` parity is partial and the
|
|
836
|
+
interception point differs. Deferred until the inspector takes a position
|
|
837
|
+
on WebGPU; tracked here so it is not forgotten.
|
|
838
|
+
5. Whether `attribute_cost` should also bisect **lights** (visibility
|
|
839
|
+
toggling a light changes compiled program count — measurable, but slow).
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
*Derived from: the slopjs performance strategy (`docs/performance-strategy.md`
|
|
844
|
+
in the slopjs repo — audit of the inspector's current perf surface, two
|
|
845
|
+
research sweeps with citations, and the spin-out decision). August 2026.*
|