Files
moli/moli-inspector-json/README.md

7.1 KiB
Raw Permalink Blame History

Inspector JSON

InspectorJson carries V8 Inspector envelopes through CDP without decoding their JavaScript string payloads into Rust UTF-8 strings. It validates JSON syntax with serde_json::RawValue, shares immutable fields across clones, and rewrites the outer command ID and session independently. Payload escapes, including unpaired UTF-16 surrogates, retain their wire representation.

value() provides a cached UTF-8 business view; existing Value API access through Deref requests the same view. Unpaired surrogates become U+FFFD in that view. Reading it does not change serialization. value_mut() explicitly switches the modified copy to Parsed(Value). Other copies retain their original payloads.

Equality compares the emitted wire representation, including payload whitespace and escape spelling. Raw/Raw comparison uses shared-storage and field-byte checks, without creating a business view. Heterogeneous equality with Value is omitted to keep its business semantics separate. Compare message.value() explicitly when lossy UTF-8 business equality is intended.

Raw construction rejects the reserved top-level parameter names used for Moli host metadata, including Unicode-escaped spellings. Host-generated messages use Parsed(Value) and the existing output filter. Other parameter names, including unpaired surrogate escapes, and names inside user objects remain opaque. The reserved-name check uses serde_json's byte decoder instead of requiring UTF-8 field names. Fetch.requestPaused retains its existing Parsed-only interceptor projection policy.

The structured view has the Value parser's depth limit. If a valid raw payload exceeds it, value() returns a synthetic -32603 error view; CDP still serializes the original raw response. SessionSink settlement checks the raw error field, and completion matching reads the raw call ID without requesting a business view. A synthetic view error cannot turn a successful session response into a failed settlement. This compatibility API is not a fallible, lossless JSON accessor. The raw fields and cached Value coexist until their owners drop.

This is validated raw-field transport, not zero-copy or zero-scan forwarding. Remote-object ownership, lifecycle handling, debugger state, and automation adapters may still need structured data. The CDP writer retains its existing message-size and pending-byte limits.

Benchmark

Capture actual Inspector replies using Node.js 22+ with no npm dependencies:

node scripts/benchmark-inspector-json.mjs \
  --capture /path/to/moli --output target/inspector-json-capture
cargo run --release -p moli-inspector-json --example benchmark -- \
  --fixtures target/inspector-json-capture/fixtures --rounds 5

The example compares parsing, two clones, ID/session updates, and serialization. raw-view also materializes a business view. It checks semantic equivalence before timing, warms each group, and rotates mode order. These are carrier measurements, not whole-browser speedups. The example uses the system allocator; Moli on Linux x86_64 uses its V8 PartitionAlloc shim.

Five-round medians in microseconds, Rust 1.96.1, Linux x86_64, Intel Core Ultra 7 270K Plus, pinned to CPU 0:

Actual Inspector reply Bytes Value Raw Raw + view
evaluate 1 KiB string 1,101 1.058 0.399 0.898
evaluate 100 KiB string 102,477 108.336 10.374 72.835
evaluate 1 MiB string 1,048,653 1,517.392 772.093 1,133.940
getProperties, 1,000 properties 159,077 962.984 63.864 448.546

The script also supports paired end-to-end measurements and verifies exact UTF-16 units through Window, Worker, notifications, and non-flat CDP sessions:

node scripts/benchmark-inspector-json.mjs \
  --before /path/to/moli-before --after /path/to/moli-after \
  --output target/inspector-json-e2e --rounds 5

# Check wire semantics without running timings.
node scripts/benchmark-inspector-json.mjs \
  --verify /path/to/moli-after --output target/inspector-json-verification

The output records wall time, server CPU ticks on Linux, request counts, and wire sizes. Compare binaries built with the same profile and flags. A debug-binary run does not establish a production release speedup; allocation counts and RSS need separate measurements.

A local five-round alternating A/B check against 57b2b17658, using matching debug builds and CPU affinity 0–7, measured these per-request wall-time medians:

Request Before, ms After, ms
evaluate 1 KiB 1.078 1.066
evaluate 100 KiB 3.497 2.307
evaluate 1 MiB 30.977 18.869
getProperties 23.003 22.835

Both large-string cases improved in all five pairs. The small reply and getProperties results do not show a stable performance improvement. In particular, remote-object ownership still consumes structured property data; the carrier microbenchmark overstates the benefit for that complete workflow. No release end-to-end speedup or RSS improvement is established by this run.

Retained-memory model

The retained release example holds 16 independently parsed copies of a captured Inspector reply, with one or two queued clones per copy. It samples its own Linux smaps_rollup while the queue is live. All three modes use the system allocator. It does not run the other modes' validation pipelines first, which would contaminate allocation and peak-heap measurements.

Five rotated rounds per configuration, using glibc libmemusage.so for allocation counts (no failed allocations), produced these median PSS values in MiB:

Reply Clones per reply Value Raw Raw + view
1 MiB string 1 18.659 17.715 33.812
1 MiB string 2 34.699 17.716 33.809
1,000 properties 1 29.642 3.238 30.427
1,000 properties 2 56.789 3.238 30.422

For one queued clone per reply, the corresponding complete-process counts of malloc/realloc/calloc calls were 422 / 214 / 422 for the string fixture, and 416,902 / 214 / 208,726 for the property fixture (Value / Raw / Raw + view).

The single-clone string case demonstrates the memory cost of retaining both raw bytes and a parsed copy. Sharing benefits depend on the number of live holders and how often they request a view. These measurements do not settle whole-browser PSS: they use a synthetic retained queue and the system allocator, rather than Moli's allocator and actual message lifetimes. These clones keep identical envelope metadata; copy-on-write routing changes can require independent views. They do not validate or dismiss a Spider memory-regression observation.

The raw CSV contains all 120 runs, including 1 KiB and 100 KiB fixtures, live PSS/RSS, peak requested heap bytes, and allocation counts. Allocation hooks perturb timing; no timing results are derived from these runs. Example command on Linux/glibc:

cargo build --release -p moli-inspector-json --example retained
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libmemusage.so \
  target/release/examples/retained \
  --fixture target/inspector-json-capture/fixtures/evaluate-1m.json \
  --mode raw-view --messages 16 --clones 1