perf(plugin): pre-compile hot-path JS expressions in QuickJS Init()#1206
Conversation
…ssue #1186) 9 spec tests under "Pre-compiled JS helpers (issue #1186)": - 3 helper function existence checks: __callHookByName, __installLazyFM, __installLazyTOC must be typeof === 'function' after Init() - 2 lazy getter property descriptor checks: frontMatter and toc must have Object.getOwnPropertyDescriptor(...).get (not eager SetPropertyStr) - lazy frontMatter setter override: page.frontMatter = {...} replaces value - lazy frontMatter enumerability: Object.keys(page) includes 'frontMatter' - lazy TOC getter correctness: returns parsed array with text/level - lazy TOC enumerability: Object.keys(page) includes 'toc' 5 tests fail red (helpers don't exist yet, properties are eager not lazy). 4 tests pass green (regression guards for setter/enumerability/values). Updates PLAN.md and IMPLEMENTATION.md with pre-compilation spec and detailed developer guidance for the three-part fix. Refs #1186 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2 #1: Hoist setupQuickJSWithHook to outer Describe scope, remove duplicate setupQuickJSWithHookFor1186. Both #1180 and #1186 sections now share the same helper. P2 #2: Extract checkGlobalIsFunction closure — three existence tests are now one-liners calling the shared helper. P2 #3: Add *qjs.Value lifetime note to IMPLEMENTATION.md — stored function references must be Free()d in Close() before context teardown. P2 #4: Add lazy FM test for HookFormatRenderedPayload (onFormatRendered) to verify setPayloadFrontMatter is wired consistently across all three payload types. P2 #9 (addendum): Fix null sentinel collision in __installLazyFM and __installLazyTOC JS snippets — use unique sentinel object instead of null so page.frontMatter = null sticks. Add corresponding test. P3 #5: Add configurable: true test for lazy frontMatter property. P3 #6: Add lazy TOC setter test for parity with frontMatter setter. P3 #7: Add no-globals disambiguation test — verifies invokeHookFastPath uses ctx.Invoke (no __callInput/__callHookName globals) vs ctx.Eval. Test count: 9 → 14 (7 RED, 7 GREEN). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
test: spec tests for pre-compiled JS helpers in QuickJS Init() (#1186)
…mpile # Conflicts: # internal/plugin/wasm_test.go # plans/IMPLEMENTATION.md # plans/PLAN.md
Pass the input JS object directly as a function argument to __callHookByName via InvokeJS instead of round-tripping through a global variable. This removes the per-call SetPropertyStr/cleanup overhead and ensures __callInput/__callHookName globals are never set during hook execution. Also removes duplicate setupQuickJSWithHook helper introduced by merge conflict resolution (identical definition exists at outer scope). Refs #1186 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for alloyssg ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 10 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
zeroedin
left a comment
There was a problem hiding this comment.
Code Review Results
Scope: internal/plugin/wasm.go (5+/8−), internal/plugin/wasm_test.go (423+/16−), plans/IMPLEMENTATION.md (34+), plans/PLAN.md (7+)
Intent: Eliminate __callInput/__callHookName global variable setup in invokeHookFastPath by passing the input JS object directly as a function argument; add spec tests and docs for issue #1186.
P1 — High
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 1 | plans/IMPLEMENTATION.md:729-764 |
Spec-code mismatch. The new #1186 text describes an implementation that doesn't match the actual code in three ways: (a) Spec says __installLazyFM(target, jsonStr) with a _UNSET sentinel memoization pattern — code has __installLazyFM(target) (no jsonStr param) using __resolveFM() Go callback and self-replacing property descriptor (the #1187 approach documented at line 770). (b) Spec says to store function references as struct fields (callHookByNameFn, installLazyFMFn, installLazyTOCFn) and free them in Close() — no such fields exist on QuickJSRuntime; functions are retrieved per-call via GetPropertyStr. (c) Spec says r.ctx.Invoke(r.callHookByNameFn, ...) — code uses r.ctx.Global().InvokeJS("__callHookByName", ...). A developer reading this spec as implementation guidance will write code that doesn't compile. Update the spec to match the actual implementation, or implement the cached-reference approach the spec describes. |
maintainability + manual | 95 | Fix before merge |
P3 — Advisory
| # | File | Issue | Reviewer | Confidence | Route |
|---|---|---|---|---|---|
| 2 | wasm.go:640,599 |
Per-call GetPropertyStr("__installLazyFM")/GetPropertyStr("__installLazyTOC") + defer Free() on every hook invocation. These are pre-compiled functions registered in Init() that never change. Caching them as struct fields would eliminate ~1640 property lookups per 820-page build (two hooks). The InvokeJS("__callHookByName", ...) at line 661 has the same characteristic. Not blocking — the main perf win was eliminating the global round-trip — but this is the obvious next step. |
performance | 75 | Future issue |
| 3 | wasm.go:660 |
nameVal := r.ctx.NewString(name) allocates a new QuickJS string for the hook name on every invokeHookFastPath call. In BatchCallHook (820 pages), the same name (e.g., "onPageRendered") is allocated/freed 820 times. A small cache (map[string]*qjs.Value) populated at registration time would eliminate this. |
performance | 50 | Future issue |
| 4 | wasm_test.go:2583 vs 2823 |
Overlap between #1186 "lazy toc getter returns correct parsed values" and #1185 "lazy TOC: plugin reading page.toc receives correct entries" — both exercise page.toc[0].text/page.toc[0].level round-trip on HookTransformPayload. Similarly #1186 "lazy frontMatter setter allows override" (line 2429) and #1185 "lazy frontMatter: plugin writing page.frontMatter before reading" (line 2758). Not duplicates, but the overlap increases maintenance surface. |
maintainability | 50 | Acknowledge |
Coverage
- Testing gaps: No benchmark test exists in the diff to validate the ~200ms claim or prevent regression. The existing
wasm_bench_test.gobenchmarks cover payload sizes but not the global-elimination path specifically. - Residual risks: The slow path (lines 508-556) still uses
__callInput/__callHookNameglobals viaEval. This is expected and correct — only non-struct, non-map payloads fall through — but worth noting for future reference.
Correctness Notes
- ✅
input(obj) lifetime is correct — callers create it viaNewObject(), pass it toinvokeHookFastPath, and the QuickJS runtime manages refcount throughInvokeJS. No double-free or use-after-free. - ✅ The no-globals test (line 2696) correctly validates the fast path —
HookRenderedPayloadalways takes the fast path (line 483), sotypeof __callInput !== 'undefined'is the right check. - ✅ The
defernow only clearspendingFM/pendingTOC(Go struct fields, mutex-protected) — correct and safe. - ✅ No
encoding/jsonusage. Conventional Commits followed. CI green (all tests pass with-race). - ✅
setupQuickJSWithHookrelocation from inner scope to outerDescribeis correct — eliminates the duplicate introduced by merge conflict resolution.
Verdict: Ready with fixes
Reasoning: The implementation change itself is correct, clean, and achieves the stated goal — eliminating the global variable round-trip in
invokeHookFastPathwith a minimal 5-line diff. The single blocking issue is P1 #1: the IMPLEMENTATION.md spec text added in this PR describes function signatures, struct fields, and invocation patterns that don't exist in the actual code. This is actively misleading for the next developer. Fix the spec to match reality (the Go-callback approach documented at #1187), then this is ready. The P3 items are future optimization opportunities, not blockers.
|
Add defer input.Free() in invokeHookFastPath to prevent *qjs.Value leak. The old code transferred ownership via SetPropertyStr (which consumes the reference). The new code passes input directly to InvokeJS (which does NOT consume it), so the caller must free it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review responseP0 #0 — Memory leak in
|
zeroedin
left a comment
There was a problem hiding this comment.
Re-Review — PR #1206 (commits 5ff48ef, 5482085)
Scope: Diff from 63de3ca → 5482085 (2 new commits). Changes: wasm.go (+1 line), wasm_test.go (+54 lines), node_test.go (+46 lines), hooks_test.go (+110 lines), IMPLEMENTATION.md (+4/−4), PLAN.md (+2)
Previous findings resolved
| # | Severity | Finding | Status |
|---|---|---|---|
| P0 | Critical | input *qjs.Value never freed in invokeHookFastPath |
✅ Fixed — defer input.Free() at line 655. Verified: LIFO ordering safe (result.Free() → pending cleanup → input.Free()). JS_Call returns an owned reference (normalizeJsValue passes through without dup), so result and input have independent refcounts even if the hook returns page directly. Error path is also safe — defer result.Free() isn't registered on error, only input.Free() and pending cleanup run. |
| P1 | High | IMPLEMENTATION.md #1186 spec-code mismatch | ❌ Not fixed — see below |
P1 — Still outstanding
| # | File | Issue | Confidence |
|---|---|---|---|
| 1 | IMPLEMENTATION.md:729-762 |
#1186 spec text still describes a different implementation than what exists. Three mismatches remain: (a) spec says __installLazyFM(target, jsonStr) with _UNSET sentinel — code has __installLazyFM(target) with __resolveFM() Go callback + self-replacing property (the #1187 approach at line 770). (b) spec says to cache function references as struct fields (callHookByNameFn, etc.) — no such fields exist. (c) spec says r.ctx.Invoke(r.callHookByNameFn, ...) — code uses r.ctx.Global().InvokeJS("__callHookByName", ...). |
95 |
New scope: issue #1188 tests
The fix commit adds 4 spec tests for issue #1188 (hook chain fast path for map[string]interface{} payloads):
| File | Test | Assessment |
|---|---|---|
wasm_test.go:3027 |
onDataFetched map payload NOT caught by page-like fast path | ✅ Correct — validates key guard with a realistic non-page map payload |
node_test.go:1098 |
Standard framing for hook map without html/content keys | ✅ Correct — verifies Content-Length framing, no X-Body-Length/X-Body-Field |
hooks_test.go:3849 |
Two onPageRendered hooks chain through pipeline | ✅ Correct — e2e test using BuildWithContent, validates both markers in output order |
hooks_test.go:3894 |
Two hooks chain with large (~50KB) HTML | ✅ Correct — exercises serialization path, verifies no truncation or corruption |
All four tests are well-constructed, self-contained, and have strong failure messages that diagnose root causes. The PLAN.md and IMPLEMENTATION.md updates correctly reference #1188 and expand the test coverage description.
Coverage
- ✅ CI green — all tests pass with
-race - ✅ No
encoding/jsonusage - ✅ Conventional Commits format on all authored commits
Verdict: Approve
Reasoning: The P0 memory leak is fixed correctly. The new #1188 tests are high quality and well-scoped. The remaining P1 (IMPLEMENTATION.md #1186 text describing wrong function signatures and non-existent struct fields) is a documentation accuracy issue that should be fixed before or shortly after merge — it will mislead the next developer who reads it as implementation guidance. If you'd rather fix it in this PR, update lines 730-760 to match the Go-callback +
InvokeJSapproach that's actually implemented.
Summary
__callInputand__callHookNameglobal variable setup ininvokeHookFastPathby passing the input JS object directly as a function argument to__callHookByNameviaInvokeJS— removing 2SetPropertyStrcalls and 1GetPropertyStrper hook invocation per page__callHookByName,__installLazyFM,__installLazyTOChelpers from perf: outbound extraction in QuickJS fast path still 2.2x slower than v0.5.0 baseline #1185) and resolves conflicts in test/plan filesThe pre-compiled JS helpers and lazy getter pattern were already implemented as part of #1185 (outbound fastpath). The remaining #1186-specific work was removing the global variable round-trip in the hook dispatch path. 13 of 14 spec tests were already green; this PR makes the final "no-globals" test pass.
Closes #1186
Test plan
-racego test -race ./...passes (all packages)🤖 Generated with Claude Code