Skip to content

perf(match_order): bound MatchResult pre-alloc by fill count (0.8.3)#107

Merged
joaquinbejar merged 2 commits into
mainfrom
issue-106-bound-matchresult-prealloc
Jun 25, 2026
Merged

perf(match_order): bound MatchResult pre-alloc by fill count (0.8.3)#107
joaquinbejar merged 2 commits into
mainfrom
issue-106-bound-matchresult-prealloc

Conversation

@joaquinbejar

Copy link
Copy Markdown
Owner

Closes #106.

Problem

PriceLevel::match_order pre-sized the MatchResult buffers (trades, filled_order_ids) to the whole level depth (self.order_count()), then freed the unused capacity at the end of the match. On a deep level a qty-1 taker still reserved ~176 B × depth — a multi-MB transient allocation per match (17.6 MB for a qty-1 match against a 100k-deep level). It is not a leak (allocs ≈ deallocs, allocs/op unchanged), which is why the downstream OrderBook-rs alloc-budget guard (asserts on allocs/op, not bytes) didn't catch it — its bytes_alloc/op ballooned to ~790 KB.

Fix (src/price_level/level.rs)

-let capacity = self.order_count();                              // whole depth
+let capacity = (incoming_quantity as usize).min(self.order_count());

Tight upper bound on the number of fills: every emitted trade consumes ≥1 unit of the taker (a consumed == 0 maker is set aside without a trade), so #trades ≤ incoming_quantity, and trivially ≤ order_count. Capacity stays advisory — both Vecs still grow if a concurrent add_order lands mid-sweep. Computed once before the loop, so no new hot-loop branch; strictly fewer/smaller allocations.

  • qty-1 market order against a deep level → min(1, depth) = 1 (KB, not MB).
  • large sweep → grows to the real fill bound, unaffected.

Also updates the adjoining comment and the now-stale sentence in MatchResult::with_capacity's doc.

No behavior change

Capacity is a hint only. Trade contents/order, FIFO/price-time priority, counters, and MatchResult field agreement are all unchanged. The full matching / FOK / iceberg-reserve correctness suite stays green (regression guard).

Tests (src/price_level/tests/level.rs)

  • test_match_order_capacity_bounded_by_incoming_quantity — qty-1 match against a 200-deep level: result.trades().as_vec().capacity() <= 1 (pre-fix: 200), exactly one fill.
  • test_match_order_capacity_bounded_by_order_count — taker of qty 10 000 against a 3-maker level: capacity <= 3 (the min picks depth), all three filled.

Verification

  • make pre-push clean: 417 lib tests (415 + 2 new), 9 proptests, 1 integration, 9 doctests; clippy + fmt + rustdoc -D warnings all green.
  • Examples all run (make run-examples); benches compile and run (cargo bench).
  • No README.md diff; no public API / wire-format change → no lib.rs / prelude.rs / migration-guide change.

Ships as 0.8.3 (CHANGELOG entry added) so OrderBook-rs can bump and its bytes_alloc/op returns to the low-KB range.

…el depth

match_order pre-sized the MatchResult trades / filled_order_ids buffers to
the whole level depth (order_count), so a qty-1 taker against a deep level
reserved ~176 B x depth (multi-MB on a 100k-deep level) and freed it at the
end of the match -- pure transient allocator pressure, not a leak.

Bound the pre-size by min(incoming_quantity, order_count): each emitted
trade consumes >= 1 unit of the taker, so the number of fills is at most
incoming_quantity, and at most order_count. Capacity stays advisory (both
Vecs still grow if a concurrent add_order lands mid-sweep). Matching
semantics, FIFO/price-time priority, and trade output are unchanged.

Tests assert the trade-buffer capacity is bounded by incoming quantity (qty-1
vs a 200-deep level) and by order count (huge taker vs a 3-maker level).
Bump 0.8.2 -> 0.8.3 and document the match_order pre-allocation fix so
OrderBook-rs can bump the dependency (its alloc_count bytes_alloc/op drops
back to the low-KB range).
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/execution/match_result.rs 79.81% <ø> (ø)
src/price_level/level.rs 93.12% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joaquinbejar joaquinbejar merged commit e88d65d into main Jun 25, 2026
13 checks passed
@joaquinbejar joaquinbejar deleted the issue-106-bound-matchresult-prealloc branch June 25, 2026 06:03
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.

perf(match_order): MatchResult pre-allocates to the whole level depth, not the fill count — multi-MB transient buffer per match on deep levels

2 participants