Skip to content

Add Frozen object backend for reduced memory footprint - #66

Open
anakrish wants to merge 8 commits into
mainfrom
object-mem-boxed-slice
Open

Add Frozen object backend for reduced memory footprint#66
anakrish wants to merge 8 commits into
mainfrom
object-mem-boxed-slice

Conversation

@anakrish

@anakrish anakrish commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a Frozen representation for Object storage that uses a compact Box<[(K, V)]> instead of BTreeMap, reducing memory footprint substantially for fully-materialized objects.

Memory impact

Live memory measurements across canonical JSON benchmark fixtures:

Fixture BTree (baseline) Frozen Reduction
SARIF×50 94.3 MiB 24.3 MiB 3.9×
citm 7.6 MiB 3.1 MiB 2.5×
synthea 11.4 MiB 5.2 MiB 2.2×
twitter 2.2 MiB 1.4 MiB 1.6×
canada 9.1 MiB 9.1 MiB 1.0× (number-heavy, unaffected)

Design

Object::Repr becomes an enum with four variants:

  • Empty — zero-cost no allocation for empty objects
  • Inline(Box<SmallVec<[(K, V); 2]>>) — for tiny objects (≤2 entries)
  • Frozen(Box<[(K, V)]>) — compact, sorted, deduplicated; used for fully-materialized immutable-shape objects
  • BTree(BTreeMap<K, V>) — fallback for actively-mutated objects

Frozen is the default for objects constructed via parsing/serialization. Mutation (insert/remove) thaws to BTree on demand. Value-only mutation on existing keys preserves Frozen.

Where Frozen kicks in

freeze_recursive runs at value-construction boundaries (JSON parse, value builder paths in src/value/mod.rs) to convert fully-materialized subtrees into the compact representation.

Behavior

  • All existing tests pass (238 default).
  • New tests added covering: Frozen storage construction, sort/dedup invariants, mutation thawing, freeze recursion, cursor traversal.
  • New integration tests tests/corpus_memory.rs and tests/sarif_memory.rs measure live memory across fixtures.
  • No public API changes; Object remains a black box.
  • One new public diagnostic helper: object_storage_variant_for_memory_diagnostics(&Object) -> &'static str for testing/diagnostics.

Commits

  1. `tests: add SARIF memory residency integration test` — baseline measurement
  2. `value/object: two-tier SmallMap (inline+BTreeMap) representation` — foundation
  3. `Freeze objects into boxed slices` — Frozen variant
  4. `tests: corpus_memory integration test across canon JSON benchmark fixtures` — full corpus measurement
  5. `Harden object frozen storage core` — invariant enforcement
  6. `Freeze objects at value boundaries` — wire up freeze_recursive
  7. `Add object frozen storage tests` — additional coverage
  8. `Record memory measurement rerun` — refreshed numbers

Gates

  • `cargo fmt --all -- --check` ✓
  • `cargo clippy --all-targets --all-features -- -Dwarnings` ✓
  • `cargo test --lib` ✓ (238 passed, 2 ignored)

Follow-ups (separate PRs)

  • Lazy JSON parsing backend (in progress on parallel branch object-mem-lazy) — adds ~3-4× further reduction by deferring child materialization.
  • materialize_lazy() at eval boundary — drops source bytes when results are returned to caller.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

anakrish and others added 8 commits June 6, 2026 19:09
Adds tests/sarif_memory.rs, a single-binary integration test that
installs a custom global allocator to measure live, peak, total_alloc,
and nalloc per phase of a realistic SARIF policy evaluation:

  - load policy
  - Value::from_json_str
  - drop the source JSON
  - Engine::set_input
  - Engine::eval_query

Fixtures (tests/data/sarif_memory/):
  - input.json: SARIF v2.1.0 sample, ~378 KB
  - policy.rego: 85-line compliance check used by membench

The dataset can be amplified via MULT env var (default 1 for CI speed;
MULT=50 reproduces the customer workload at 6.6 MiB JSON input).

On upstream/main (BTreeMap-backed Object) at MULT=50:
  Value::from_json_str cost: live +102.3 MiB (15.4x JSON size)
  peak during run:           102.3 MiB

The test is gated off when the regorus 'mimalloc' feature is enabled
(its custom global allocator would conflict).

Provides a permanent regression guardrail for the storage abstraction
and a baseline against which alternative Object representations
(SmallMap, boxed-slice, etc.) can be measured apples-to-apples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the single `BTreeMap<Value, Value>` backing storage with a
two-variant enum:

  - `Inline`: sorted, deduplicated `SmallVec<[(Value, Value); 4]>`
  - `BTree`: `BTreeMap<Value, Value>` (used after the inline buffer
    is full and a new key needs to be inserted)

Inline capacity is 4. SARIF workloads (and Rego objects generally)
average ~1.65 fields per object, so the inline tier covers the vast
majority of allocations without a heap node.

All Object methods route through the repr enum; PartialEq/Eq/Ord/Debug
are defined in terms of sorted iteration so they are oblivious to the
backend variant.

Measured on tests/sarif_memory.rs (MULT=50, 6.6 MiB SARIF input):
                       BTreeMap     SmallMap   reduction
  live after parse     94.3 MiB     39.2 MiB   2.4x
  peak                102.3 MiB     47.2 MiB   2.2x

A separate A/B with hashbrown::HashMap as the spill backend produced
byte-identical numbers on this workload, confirming the win comes
entirely from the inline tier.

All 228 lib tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a Frozen boxed-slice Object representation and thaw mutation paths back to Inline or BTree. Box the mutable Inline builder so frozen objects do not retain the SmallVec inline-buffer size in the Object enum.

SARIF MULT=50: live after parse 32.313 MiB, live after drop JSON 24.313 MiB, peak 32.315 MiB. Object variants after parse: Inline=0 Frozen=160559 BTree=0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tures

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Measurements rerun for SARIF MULT=50, corpus MULT=1, and twitter/synthea MULT=10; no source changes required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant