fix: show both engines side-by-side in README benchmark table#826
fix: show both engines side-by-side in README benchmark table#826carlos-alm merged 6 commits intomainfrom
Conversation
The benchmark report showed both native and WASM build speeds side-by-side in the README, but since total build time includes many engine-independent JS stages (insert, resolve, structure, roles), the numbers appeared misleadingly similar (~13.3 vs ~13.6 ms/file). This was inconsistent with how rebuild and query metrics already used only the preferred engine. Now build speed, query time, and the 50k estimate all use `pref` (native when available, WASM fallback) — matching the pattern for all other rows. Detailed per-engine breakdown remains in BUILD-BENCHMARKS.md.
|
Claude finished @carlos-alm's task —— View job PR Review: Fix README benchmark table displayReview Progress
Analysis SummaryFile Changed: Phase 1: Root Cause Analysis ✅Problem Being Solved: The README benchmark table showing both native and WASM build speeds side-by-side creates misleading impressions. The total build time includes many engine-independent JavaScript stages (insert, resolve, structure, roles) that dilute the native parsing advantage, making ~13.3 vs ~13.6 ms/file appear artificially similar. Why This Approach: The change follows the existing pattern already used for rebuild and query latency rows, showing only the preferred engine. The detailed per-engine breakdown remains accessible in Risk Assessment: Low risk. This is purely a display change that doesn't affect the underlying data collection or the detailed benchmark files. Phase 2: Configuration Change Detection ✅Result: No configuration changes detected. This is a pure display logic change in a reporting script. Phase 3: Test Integrity ✅Result: No test files modified. No assertions weakened or removed. The change only affects how benchmark data is formatted for display. Phase 4: Code Quality Review ✅Code Quality: Good. The change simplifies the logic by:
Technical Implementation: Sound. The Phase 5: Backlog Compliance Assessment ✅Zero-dep: ✓ — No new dependencies added. Pure refactor of existing display logic. Foundation-aligned: ✅ — Fully aligned. Improves user experience by reducing confusion without violating any principles. Problem-fit: 3/5 — This addresses user confusion about benchmark presentation, which is useful but doesn't directly help AI agents navigate codebases. It's a worthwhile quality-of-life improvement. Breaking: No — This is purely a display change. The underlying data and API remain unchanged. Tier: 1 (zero-dep ✓ + foundation-aligned ✓) Final AssessmentRoot Cause AnalysisProblem Being Solved: Misleading benchmark presentation where native vs WASM speeds appear artificially similar due to engine-independent overhead masking the native parsing advantage. Why This Approach: Consistent with existing patterns in the same codebase; preserves detailed data in specialized benchmark files while simplifying README presentation. Risk Assessment: Very low risk - pure display refactor with no behavioral changes. Backlog Compliance
Critical ConcernsNone. This is a straightforward improvement that addresses a real user confusion issue. Final Recommendation
|
Greptile SummaryThis PR refactors the README benchmark table in Confidence Score: 5/5Safe to merge — all prior P1 concerns resolved, no new issues found The hasBoth flag correctly guards every wasm dereference in the README patch block. All three cases (both engines, native-only, WASM-only) produce consistent column counts between header and rows. No logic errors, security issues, or correctness problems remain. No files require special attention Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Read benchmark JSON] --> B{latest.native != null?}
B -- Yes --> C{latest.wasm != null?}
B -- No --> G[WASM-only: single-column rows]
C -- Yes --> D[hasBoth=true: Native + WASM two-column rows]
C -- No --> E[native-only: single-column rows]
D --> H[tableHeader: Metric | Native | WASM]
E --> I[tableHeader: Metric | Latest]
G --> I
H --> J[Patch README performance section]
I --> J
Reviews (4): Last reviewed commit: "fix: guard against null wasm data in REA..." | Re-trigger Greptile |
Add optional chaining on latest.wasm.perFile (line 203) to prevent TypeError when only native benchmark data is present. Also append prefLabel to Query: fn-deps and Query: path rows for consistent engine labeling in the README table.
|
Addressing Greptile review feedback (ca6a828): P1 — Crash when P2 — Missing engine label on query latency rows (lines 349-350): |
|
Latest commit 97b2cf4 changes the approach: instead of showing only the preferred engine with an engine label suffix, it now uses a proper two-column table layout (Native | WASM) when both engines are available, falling back to a single-column layout for WASM-only environments. This is cleaner and makes the side-by-side comparison more readable. The wasm null guards from ca6a828 are preserved in BUILD-BENCHMARKS.md sections (using optional chaining). The README section uses |
| let rows = ''; | ||
| if (latest.native) { | ||
| rows += `| Build speed (native) | **${latest.native.perFile.buildTimeMs} ms/file** |\n`; | ||
| rows += `| Build speed (WASM) | **${latest.wasm.perFile.buildTimeMs} ms/file** |\n`; | ||
| rows += `| Query time (native) | **${formatMs(latest.native.queryTimeMs)}** |\n`; | ||
| rows += `| Query time (WASM) | **${formatMs(latest.wasm.queryTimeMs)}** |\n`; | ||
| if (hasNative) { | ||
| rows += `| Build speed | **${latest.native.perFile.buildTimeMs} ms/file** | **${latest.wasm.perFile.buildTimeMs} ms/file** |\n`; | ||
| rows += `| Query time | **${formatMs(latest.native.queryTimeMs)}** | **${formatMs(latest.wasm.queryTimeMs)}** |\n`; | ||
| } else { | ||
| rows += `| Build speed | **${latest.wasm.perFile.buildTimeMs} ms/file** |\n`; | ||
| rows += `| Query time | **${formatMs(latest.wasm.queryTimeMs)}** |\n`; | ||
| } | ||
|
|
||
| // Incremental rebuild rows (prefer native, fallback to WASM) | ||
| if (pref.noopRebuildMs != null) { | ||
| rows += `| No-op rebuild${prefLabel} | **${formatMs(pref.noopRebuildMs)}** |\n`; | ||
| } | ||
| if (pref.oneFileRebuildMs != null) { | ||
| rows += `| 1-file rebuild${prefLabel} | **${formatMs(pref.oneFileRebuildMs)}** |\n`; | ||
| // Incremental rebuild rows | ||
| if (hasNative) { | ||
| const nativeNoop = latest.native.noopRebuildMs != null ? `**${formatMs(latest.native.noopRebuildMs)}**` : 'n/a'; | ||
| const wasmNoop = latest.wasm.noopRebuildMs != null ? `**${formatMs(latest.wasm.noopRebuildMs)}**` : 'n/a'; | ||
| if (latest.native.noopRebuildMs != null || latest.wasm.noopRebuildMs != null) { | ||
| rows += `| No-op rebuild | ${nativeNoop} | ${wasmNoop} |\n`; | ||
| } | ||
| const nativeOneFile = latest.native.oneFileRebuildMs != null ? `**${formatMs(latest.native.oneFileRebuildMs)}**` : 'n/a'; | ||
| const wasmOneFile = latest.wasm.oneFileRebuildMs != null ? `**${formatMs(latest.wasm.oneFileRebuildMs)}**` : 'n/a'; | ||
| if (latest.native.oneFileRebuildMs != null || latest.wasm.oneFileRebuildMs != null) { | ||
| rows += `| 1-file rebuild | ${nativeOneFile} | ${wasmOneFile} |\n`; | ||
| } | ||
| } else { | ||
| if (latest.wasm.noopRebuildMs != null) { | ||
| rows += `| No-op rebuild | **${formatMs(latest.wasm.noopRebuildMs)}** |\n`; | ||
| } | ||
| if (latest.wasm.oneFileRebuildMs != null) { | ||
| rows += `| 1-file rebuild | **${formatMs(latest.wasm.oneFileRebuildMs)}** |\n`; | ||
| } | ||
| } | ||
|
|
||
| // Query latency rows (pick two representative queries, skip if null) | ||
| if (pref.queries) { | ||
| if (pref.queries.fnDepsMs != null) rows += `| Query: fn-deps | **${pref.queries.fnDepsMs}ms** |\n`; | ||
| if (pref.queries.pathMs != null) rows += `| Query: path | **${pref.queries.pathMs}ms** |\n`; | ||
| // Query latency rows (pick two representative queries) | ||
| if (hasNative) { | ||
| const nq = latest.native.queries; | ||
| const wq = latest.wasm.queries; | ||
| if (nq?.fnDepsMs != null || wq?.fnDepsMs != null) { | ||
| rows += `| Query: fn-deps | **${nq?.fnDepsMs ?? 'n/a'}ms** | **${wq?.fnDepsMs ?? 'n/a'}ms** |\n`; | ||
| } | ||
| if (nq?.pathMs != null || wq?.pathMs != null) { | ||
| rows += `| Query: path | **${nq?.pathMs ?? 'n/a'}ms** | **${wq?.pathMs ?? 'n/a'}ms** |\n`; | ||
| } | ||
| } else if (latest.wasm.queries) { | ||
| if (latest.wasm.queries.fnDepsMs != null) rows += `| Query: fn-deps | **${latest.wasm.queries.fnDepsMs}ms** |\n`; | ||
| if (latest.wasm.queries.pathMs != null) rows += `| Query: path | **${latest.wasm.queries.pathMs}ms** |\n`; | ||
| } | ||
|
|
||
| // 50k-file estimate | ||
| const estBuild = latest.native | ||
| ? formatMs(latest.native.perFile.buildTimeMs * ESTIMATE_FILES) | ||
| : formatMs(latest.wasm.perFile.buildTimeMs * ESTIMATE_FILES); | ||
| rows += `| ~${(ESTIMATE_FILES).toLocaleString()} files (est.) | **~${estBuild} build** |\n`; | ||
| if (hasNative) { | ||
| const estNativeBuild = formatMs(latest.native.perFile.buildTimeMs * ESTIMATE_FILES); | ||
| const estWasmBuild = formatMs(latest.wasm.perFile.buildTimeMs * ESTIMATE_FILES); | ||
| rows += `| ~${(ESTIMATE_FILES).toLocaleString()} files (est.) | **~${estNativeBuild} build** | **~${estWasmBuild} build** |\n`; | ||
| } else { | ||
| const estBuild = formatMs(latest.wasm.perFile.buildTimeMs * ESTIMATE_FILES); | ||
| rows += `| ~${(ESTIMATE_FILES).toLocaleString()} files (est.) | **~${estBuild} build** |\n`; | ||
| } |
There was a problem hiding this comment.
Null access on
latest.wasm when native is present but WASM is absent
hasNative is only latest.native != null, but every code path inside the if (hasNative) block directly dereferences latest.wasm (lines 332, 333, 342, 346, 362, 378–379) without guarding against latest.wasm being null. If a benchmark run produces native data but WASM data is absent (e.g. the WASM grammar failed to build), this throws TypeError: Cannot read properties of null. The immediately preceding commit (ca6a828) was literally titled "fix: guard wasm null access", so this scenario is known to happen in practice.
Use a hasBoth flag instead:
const hasNative = latest.native != null;
const hasBoth = hasNative && latest.wasm != null;
let rows = '';
if (hasBoth) {
rows += `| Build speed | **${latest.native.perFile.buildTimeMs} ms/file** | **${latest.wasm.perFile.buildTimeMs} ms/file** |\n`;
rows += `| Query time | **${formatMs(latest.native.queryTimeMs)}** | **${formatMs(latest.wasm.queryTimeMs)}** |\n`;
} else if (hasNative) {
rows += `| Build speed | **${latest.native.perFile.buildTimeMs} ms/file** |\n`;
rows += `| Query time | **${formatMs(latest.native.queryTimeMs)}** |\n`;
} else {
rows += `| Build speed | **${latest.wasm.perFile.buildTimeMs} ms/file** |\n`;
rows += `| Query time | **${formatMs(latest.wasm.queryTimeMs)}** |\n`;
}
And replace every downstream if (hasNative) guard (incremental rebuild rows, query latency rows, 50k-file estimate, resolution rows, and tableHeader) with if (hasBoth) so that WASM data is only shown when it is actually present.
There was a problem hiding this comment.
Fixed in 8a83fe5. Introduced a hasBoth flag (hasNative && latest.wasm != null) and replaced every if (hasNative) guard in the README-patch block with if (hasBoth). The two-column layout now only activates when both engines have data. When only native is present (no WASM), falls back to single-column layout using a pref = latest.native || latest.wasm pattern — same approach used for the existing WASM-only fallback. The hasNative variable is retained only for the intermediate native-only branch in the build speed / query time rows.
Use a `hasBoth` flag (native && wasm both present) instead of just `hasNative` for the two-column layout. When only native data exists without WASM, falls back to single-column layout instead of crashing with a TypeError on null wasm access.
Summary
BUILD-BENCHMARKS.mdTest plan
node --import ./scripts/ts-resolve-loader.js scripts/update-benchmark-report.ts benchmark-result.jsonwith bothnativeandwasmdata — verify README table shows| Metric | Native | WASM |header with both values in every rownative: null— verify README table shows single| Metric | Latest |column layoutBUILD-BENCHMARKS.mdstill contains full per-engine breakdown