Skip to content

Performance: add opt-in profiler and reproducible benchmarks with an explicit data collection contract #4

Description

@Eric-Song-Nop

Goal

Establish an opt-in profiler and reproducible benchmark baseline before optimizing quickjs-oxide. The collected data should tell us where memory is retained, where allocations occur, how much initialization costs, and which operations are slow relative to the pinned QuickJS 2026-06-04 reference and previous Oxide revisions.

This issue tracks the observability foundation and its data contract. The interfaces below are proposed, not implemented functionality.

Data we will collect

Area Planned data Measurement boundary
Runtime memory snapshots (-d / --dump) Counts for objects, shapes/properties, atoms, strings, arrays/elements, functions/bytecode and backing stores; owned bytes and buffer capacity where measurable; runtime allocator live allocation count/bytes where supported. Record the snapshot phase, runtime identity, resource coverage and accounting basis. Deduplicate shared storage. Keep logical counts, requested bytes, capacity and usable bytes separate.
Allocation events (-T / --trace) Allocation / reallocation / free events, event sequence, allocation identity, requested size, result and failures where observable; previous/new identity and size for reallocations where available. Trace from runtime creation through teardown. State exactly which allocation paths are covered. Mark partial coverage and dropped events/output failures; object creation counts are not allocator events.
Runtime / Context lifecycle timing (-q -d) Separate durations for Runtime creation, Context creation, Context destruction and Runtime destruction, with iteration count and raw repeated samples. Identify timer and units explicitly (CPU time vs monotonic wall time). Report summaries with their aggregation rule; keep process startup separate from API lifecycle cost.
Operation benchmarks Per-case elapsed time, iteration/operation count, ns/op, repeated independent run results, minimum and median with variation, and per-case ratios against a recorded baseline. Start with loop, property access, array, call and numeric workloads from the pinned tests/microbench.js. Preserve benchmark bodies; adapt only host facilities as needed. Measure cold start, parse/compile and steady-state execution separately where the harness supports that separation. Record successful execution/result checks, failures and unsupported cases.
Reproduction metadata Report schema version, engine version/commit, reference release, workload/harness hashes, CPU/architecture, OS, pointer width, Rust/C compiler versions, build profile/optimization flags, enabled features, profiler mode, timer, warm-up and repetition policy. Save alongside every result so comparisons use matching workloads, environments and measurement definitions.

Accounting rules

  • Start with reliable existing HeapCounts and GcStats capabilities. GC node statistics describe a collection; they do not establish complete byte accounting.
  • Represent unavailable metrics as null / unavailable, with a reason. Never substitute zero or present node_count * size_of as total memory usage.
  • Label estimates and resource ownership rules. Current live memory, peak memory, cumulative allocation and process RSS are separate quantities; peak/RSS collection can be added later with an explicit source.
  • Preserve the project's unsafe_code = "forbid" policy. Assess safe allocation instrumentation coverage before committing to full allocator interception; use coverage=partial when appropriate.
  • Snapshots must not execute getters/Proxy traps, advance jobs, add persistent roots or implicitly force GC. Explicit post-GC experiments must be identified separately.
  • This first scope covers memory/allocation observation and timing benchmarks. Integrated CPU/call-stack sampling and JavaScript function execution tracing are future extensions; allocation trace must not be described as either.

Collection and benchmark workflow

  • Add an optional profiling feature, forwarded by the CLI, with collection disabled by default. A build without support should explain unsupported profiling flags.
  • Keep report formatting and I/O outside core resource accounting. Send diagnostics to stderr or a dedicated file, keeping script stdout usable.
  • Produce a human-readable summary and versioned machine-readable results; retain raw benchmark samples and reference output.
  • Use the same pinned workload and equivalent harness for both engines. Save any host-adaptation patch and its hash; minimal harness support does not imply complete QuickJS std/os compatibility.
  • Run timing benchmarks with dump/trace disabled, on a fixed machine, serially with alternating engine order. Do not mix CPU-time and wall-time results into speedup ratios.
  • Record unsupported/failed workloads explicitly and exclude them from performance aggregates. Keep Test262 correctness results separate from performance scores.
  • Measure instrumentation overhead separately for four modes: feature absent, feature compiled but inactive, snapshot enabled, and trace enabled. Publish the measurements rather than assuming zero overhead.

Deliverables / acceptance

  • Define the report schema, units, snapshot/exit phases, coverage labels and unavailable-field behavior.
  • Implement opt-in memory snapshots and four-stage lifecycle timing using reliable counts first, then documented byte accounting.
  • Document allocation-path coverage and implement trace within that boundary, including realloc failure and runtime teardown handling.
  • Add a reproducible dual-engine benchmark runner with fixed inputs, raw samples, metadata and per-case comparison reports.
  • Capture an initial baseline and a short report identifying observed bottlenecks and measurement limitations, without assuming a target speedup.
  • Verify shared-resource deduplication and relevant GC/jobs/Context lifecycle boundaries; reporting failures must preserve JavaScript results and original errors.
  • Verify inactive collection produces no diagnostic files/output or event-buffer growth, and report overhead for each mode.
  • Run relevant Rust and QuickJS comparison tests plus the full Test262 correctness gate with no new result differences.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions