Skip to content

perf(ws): _apply_snapshot_inplace dict() copy nullifies #263 identity-adoption speedup #296

Closed

Description

@TexasCoding

Summary

_apply_snapshot_inplace in kalshi/ws/orderbook.py:128-129 calls dict(msg.msg.yes) and dict(msg.msg.no), which copies the already-validated price/quantity dicts. This silently nullifies the ~5× snapshot-apply speedup CHANGELOG v2.5.0 #263 explicitly promises:

WS snapshot apply collapses to a single dict walk (#263).
OrderbookSnapshotPayload.yes / .no validate directly into dict[Decimal, Decimal] via a BeforeValidator; _apply_snapshot_inplace adopts the dict in identity (no rebuild). ~5× faster on a 200-level book.

The validator _levels_to_dict in kalshi/ws/models/orderbook_delta.py is set up correctly — it returns the same dict instance when keys/values are already Decimal. The orderbook caller then immediately copies it. For a 200-level book that's 400 needless re-hashes + reallocs per snapshot, on the recv hot path.

Evidence

kalshi/ws/orderbook.py:128-129:

yes_levels = dict(msg.msg.yes)
no_levels = dict(msg.msg.no)

The validator at kalshi/ws/models/orderbook_delta.py has an explicit fast-path that returns the value by identity if the dict is already Decimal-keyed/-valued, with a comment:

# Field type is ``dict[Decimal, Decimal]`` (not ``dict[DollarDecimal, FixedPointCount]``)
# so pydantic-core does not re-run the per-item ``BeforeValidator(_coerce_decimal)``
# walk on values our ``BeforeValidator`` has already coerced.
PriceCountMap = Annotated[dict[Decimal, Decimal], BeforeValidator(_levels_to_dict)]

Then orderbook.py copies the dict anyway.

Suggested fix

Drop the dict() wrappers — the validated dicts are safe to adopt by identity because:

  1. Each frame is a fresh JSON parse → msg.msg.yes / .no have no external references.
  2. The Pydantic model msg is discarded immediately after _apply_snapshot_inplace returns.
  3. _BookState then owns the dicts and mutates them via _apply_delta_inplace.
# kalshi/ws/orderbook.py
- yes_levels = dict(msg.msg.yes)
- no_levels = dict(msg.msg.no)
+ yes_levels = msg.msg.yes
+ no_levels = msg.msg.no

Update / add a bench in scripts/bench_orderbook_iterator.py that snapshots a 200-level book in a loop and report the wallclock delta. This is the metric the v2.5 release notes claim.

Discovered by

Post-v2.5 multi-reviewer audit (Performance surface). The CHANGELOG claim is correct in intent; only the code-side adoption regressed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance / hot-path concernwsWebSocket-related

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions