perf(octane): scope the settings toggle, drop two of three dismissal replays, add branch CI - #228
Draft
doodlewind wants to merge 13 commits into
Draft
perf(octane): scope the settings toggle, drop two of three dismissal replays, add branch CI#228doodlewind wants to merge 13 commits into
doodlewind wants to merge 13 commits into
Conversation
Octane (React's programming model, compiled) joins Solid and Vue Vapor: universal-driver renderer over the native tree, full 15-component surface, use-prefixed frame hooks, app.octane.tsx variants for all 8 demos, playground in-browser compilation, byte-exact PPSSPP goldens, and a three-framework PPSSPP benchmark (docs/bench/) per the PR #6 methodology. The PSP host gains an arena-pressure GC; framework=octane builds stub octane/profiling to dodge a WeakMap pinning pathology in the pinned QuickJS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ly -O0 QuickJS builds Real-hardware testing showed hero-octane under 1 fps. Two root causes: 1. Every PSP QuickJS build was -O0: CRATE_CC_NO_DEFAULTS=1 drops the cc crate's synthesized flags including build.rs opt_level(), and the pin predated quickjs-rs PR #6 anyway. -O2 now lives in TARGET_CFLAGS and libquickjs-sys is repinned to master (ba5bdd0d) registry-wide (psp + vita Cargo, psp/symbian toolchain manifests, pin tests). 2. An Octane state commit replays the whole root regardless of where the state lives (~2-4ms desktop, engine-bound on PSP), so per-frame state is unaffordable there. Continuous motion now rides the native channels: sprite atlases (hero/gallery/library spinners via sprites.json + a new spinner-atlas.svg), baked keyframe timelines (music equalizer, apps/music/pocket.config.ts), animate()/jump() one-shots (stats systems reveal), ref-based phase timers (notifications, library loading), and a new setTextContent() escape hatch - the text-shaped sibling of animate() - for count-ups and percentages (stats tiles, music progress). useSpriteAnimation also stops committing state on ticks that do not change the visible sprite frame. PPSSPP avg frame work, before -> after (budget 16.7ms): hero 387.8 -> 6.5ms, stats 279.9 -> 9.2ms, music 282.7 -> 12.9ms, notifications 271.2 -> 13.8ms, library 50.8 -> 4.9ms, settings 27.4 -> 14.5ms, cards 9.7 -> 6.6ms. All three frameworks' e2e suites pass; only the six goldens whose pixels legitimately changed were re-baselined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ blog to post-fix numbers - tools/gen-demo-covers.ts bakes the pocket app-family cover convention for every showcase demo: ICON0 as a geometry-drawn family tile, PIC1 as a real sim-pumped frame of the app with the XMB legibility dim. apps/<demo>/psp/ fragments apply to every framework build of that demo. - docs/bench: archive the 2026-07-30 post-optimization dataset (geomean avg_work octane 1.66x vs solid, was 15.58x; all 21 cells inside the 16.7ms budget) alongside the pre-fix 07-29 dataset. - blog: add "The hardware said no" chapter (the -O0-since-2021 discovery, replay-scope-independence, the native-channel rule, per-demo before/after), refresh the benchmark section to the new dataset with a linear-scale chart, and re-count byte-identical goldens (14/23). - playground compiler-entry merges the music demo's equalizer keyframes into the playground-wide animation theme. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No repo-internal references, leads with the machine and the three findings; post dated to its publish day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pinner atlas The svg baker scans shapes flat, so the spinner atlas's per-cell <g transform="translate(...)"> was silently dropped - all eight frames baked into cell 0 and cells 1-7 shipped blank. On hardware the spinner sat invisible and "flashed" once per cycle when the frame index wrapped through the one painted cell; the golden re-baseline had baked the bug in. The atlas now carries absolute per-cell circle coordinates (no groups), and bake-svg refuses <g transform> loudly instead of mis-baking. All four affected goldens re-baked and visually verified: the spinner is present at a distinct phase in every capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dent The post-fix builds are verified on the same physical PSP that failed the first run; the sprite-cycling bullet now carries the honest beat about the <g transform> atlas bake and the golden re-baseline that hid it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # package.json
… the root octanejs/octane#578 (shipped in 0.1.26) makes a universal setState replay only the owning component's subtree instead of the whole root. It landed the day after we filed #574 with a repro, and our repro is now their regression benchmark (benchmarks/universal-leaf-update). The upgrade alone changes nothing on device: all 23 psp-octane goldens stay byte-exact, and every bench app's press cost is unmoved. The reason is on our side — octane scopes a replay to the nearest owner holding a committed range, and a root component has none, so state that lives in the root component still replays everything. Every demo keeps its interactive state in the root. Hero now demonstrates the shape the fix rewards: the counter moves into a CounterRow leaf, and the underline it also drives leaves the render path for the native jump() channel. Output is unchanged to the byte (all 23 goldens pass), while on PPSSPP: hero octane avg work 6.53 -> 3.64 ms max work 176.89 -> 37.80 ms hero solid avg work 3.66 ms max work 31.47 ms That is press-latency parity with Solid, from a 4.7x drop in the worst frame. The other six demos are unconverted; several of them hold state at the root legitimately (library's screen, stats' tab switch whole views) and need per-app judgement rather than a blanket refactor. Verified: full suite 8 groups 0 fail, tsc clean, 23/23 psp-octane goldens byte-exact, solid control unchanged (+0%) across the same bench run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A demo's framework twins were indistinguishable on a memory stick: both the Solid and the Octane build of `hero` packed the same icon and the same "PocketJS Hero" title, so the XMB gave you no way to tell which one you were about to launch. gen-demo-covers.ts grows --framework=<fw>, which bakes a variant set into apps/<demo>/psp/<fw>/ instead of the shared directory: the framework's name under the accent rule on the 144x80 tile, an accent-bordered badge in the PIC1 corner, and "(Octane)" appended to the PARAM.SFO title. PIC1 is rendered from that framework's own bundle. tools/psp.ts prefers <app>/psp/<framework>/ when it exists and otherwise keeps using the shared fragment, so default builds are untouched. All eight Octane demos get the variant set. Verified by unpacking the built PBPs: every one carries its Octane icon0/pic1 and an "(Octane)" SFO title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…al replays Two hitches Evan caught on hardware after the 0.1.26 upgrade. Both are the same root cause as hero's — state sitting in a root component, which octane cannot scope a replay to because a root owner holds no committed range — not a new one, and not a gap in the upstream fix. settings: the toggle mirrored its value up to Settings, so every press replayed the whole root (37 component bodies). Nothing up there read `sfx` or `vibration` — the switch already tracked its own `current` — so the root state is gone and the press now scopes to the switch: 5 bodies. `theme` stays at the root, where it belongs; it restyles every row, so that press is still a whole- tree render and is now the app's worst frame rather than the toggles. notifications: one dismissal cost THREE full-root replays of ~35 bodies — press, removal, rise-end — of which only the removal changes anything on screen. `dismissingId` never reached the render tree at all, and the rise offsets only seeded a native tween. Both move to refs; `risingIds` is read during the removal replay to seed the survivors' offset. settings avg work 14.53 -> 9.94 ms (max is now the theme switch) notifications avg work 13.84 -> 7.57 ms 3 replays per dismissal -> 1 A first attempt jumped the survivors' translateY natively instead of rendering it, which silently deleted the rise animation: the list replay recreates the surviving rows, so the jump landed on nodes that were about to be destroyed and the new ones tweened 0 -> 0. The three committed goldens all happen to sit outside the rise, so they stayed green. Caught by diffing every frame hash of the tape against HEAD, which is also how both refactors are verified here: 120/120 frames identical for each, plus 23/23 PPSSPP goldens byte-exact and 11/11 suite stages green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
release.yml already gated a tag on `bun run test` + tsc + the Rust core tests; the same steps now run on every pull request and push to main, which is the branch CI this repo has been missing. Host-independent only: the PSP and Vita golden suites need PPSSPP, Vita3K and the pinned PSP toolchain, so tests/e2e/ stays a local step. hosts/web/pocketjs.wasm is gitignored, so the workflow builds it before the browser stages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two hitches Evan caught on hardware after #227, plus the branch CI this repo has been missing.
The stutters are the same root cause, not a new one
Both are what #227 described and left unconverted: state in a root component, which octane cannot scope a replay to because a root owner holds no committed range. Probing both bundles in the sim showed every scheduling precondition passing and
resolveScopedTarget()returning nothing — identical to hero before its refactor. Nothing is wrong with the upstream fix.settings — the toggle mirrored its value up to
Settings, so each press replayed the whole root: 37 component bodies. Nothing up there readsfx/vibration; the switch already tracked its owncurrent. Dropping the root copies scopes the press to the switch itself: 5 bodies.themestays at the root, where it earns its place — it restyles every row — and it is now the app's worst frame instead of the toggles.notifications — one dismissal cost three full-root replays of ~35 bodies each (press, removal, rise-end), which is why it read as more obvious than a single press. Only the removal changes anything on screen:
dismissingIdnever reached the render tree at all, and the rise offsets only seeded a native tween. Both move to refs, andrisingIdsis read during the removal replay to seed the survivors' offset.max_workis unchanged for both because the worst frame is now a different interaction in each: settings' theme switch and notifications' list removal. Both genuinely rebuild what is on screen, so that cost is real.A bug the goldens could not see
The first notifications attempt jumped the survivors'
translateYnatively instead of rendering it. That silently deleted the rise animation: the list replay recreates the surviving rows, so the jump landed on nodes about to be destroyed and the replacements tweened 0 → 0. All three committed goldens sit outside the rise window, so they stayed green.Caught by diffing every frame hash of the tape against
HEADrather than the three goldened frames. That is now how both refactors are verified: 120/120 frames identical for each.CI
release.ymlalready gated a tag onbun run test+tsc+ the Rust core tests. The same steps now run on every PR and push to main. Host-independent only — the PSP and Vita golden suites need PPSSPP, Vita3K and the pinned toolchain, sotests/e2e/stays local.Verification
HEADfor both demospsp-octanePPSSPP goldens byte-exacttsccleanRelease EBOOTs for both are built and staged; they go on the stick when it is back.
🤖 Generated with Claude Code