Skip to content

feat(subtree): NodeAllocator + DeserializeFromReaderWithAllocator + ReleaseNodes#124

Merged
mrz1836 merged 4 commits into
masterfrom
feat/node-allocator
May 18, 2026
Merged

feat(subtree): NodeAllocator + DeserializeFromReaderWithAllocator + ReleaseNodes#124
mrz1836 merged 4 commits into
masterfrom
feat/node-allocator

Conversation

@icellan

@icellan icellan commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an opt-in caller-supplied allocator for the per-subtree `Node` array so downstream callers can pool the (often multi-GB) backing across uses. The existing `DeserializeFromReader` / `deserializeNodes` / `deserializeFromReaderMmap` paths are byte-for-byte unchanged; the new surface lives alongside them.

Motivation

For multi-hundred-million-tx blocks each subtree allocates ~1M `Node` entries (48 B each) inside `deserializeNodes`. At 600+ subtrees per block that is ~29 GB allocated then freed per block. Pooling that backing across blocks is the goal of the downstream teranode change; this PR adds the minimum surface needed.

New surface

```go
type NodeAllocator func(numLeaves int) []Node

func (st *Subtree) DeserializeFromReaderWithAllocator(reader io.Reader, alloc NodeAllocator) error
func (st *Subtree) ReleaseNodes() []Node
```

  • When `alloc` is non-nil the deserializer slices into the caller-supplied backing rather than `make`-ing a fresh slice.
  • If `alloc` returns nil or too-small a slice the deserializer falls back to `make` so callers cannot break existing behaviour.
  • `ReleaseNodes` returns the full-cap slice and clears `st.Nodes` so the caller can return the backing to a pool exactly once.

Risk

Zero existing call sites change. New paths are purely additive.

Test plan

  • New `TestDeserializeFromReaderWithAllocator_UsesProvidedSlice` — sentinel value pre-written into the caller's slice survives deserialization, confirming the slice was used.
  • New `TestDeserializeFromReaderWithAllocator_NilFallback` — `nil` allocator still works.
  • New `TestDeserializeFromReaderWithAllocator_TooSmallFallsBackToMake` — undersized allocation falls back rather than panicking.
  • New `TestReleaseNodes_ReturnsFullCap` — cap of returned slice = original cap, `st.Nodes` becomes nil.
  • `go test -race ./...` passes locally.
  • CI race.

Downstream

Teranode block validation depends on this: https://github.com/bsv-blockchain/teranode (perf(blockvalidation) PR — to be opened after this merges).

…eleaseNodes

For multi-hundred-million-tx blocks each subtree allocates ~1M *Node*
entries (48 B each) inside deserializeNodes. At 600+ subtrees per
block that is ~29 GB allocated then freed per block. The existing
DeserializeFromReader / deserializeNodes / deserializeFromReaderMmap
paths are untouched.

New surface:

  type NodeAllocator func(numLeaves int) []Node
  func (st *Subtree) DeserializeFromReaderWithAllocator(reader io.Reader,
      alloc NodeAllocator) error
  func (st *Subtree) ReleaseNodes() []Node

When alloc is non-nil the deserializer slices into the caller-supplied
backing rather than make()ing a fresh slice. If alloc returns nil or
too-small a slice the deserializer falls back to make() so callers
cannot break existing behaviour. ReleaseNodes returns the full-cap
slice and clears st.Nodes so the caller can return the backing to a
pool exactly once.

Tests cover the happy path, nil-allocator fallback, too-small fallback,
and ReleaseNodes cap preservation.
@icellan icellan requested a review from mrz1836 as a code owner May 18, 2026 13:26
@github-actions github-actions Bot added the size/L Large change (201–500 lines) label May 18, 2026
@github-actions github-actions Bot added the feature Any new significant addition label May 18, 2026

@mrz1836 mrz1836 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - CI is failing - fix the failing checks please.

icellan added 3 commits May 18, 2026 16:22
… wrappers

DeserializeFromReader and deserializeNodes were near-copies of their
allocator-aware twins introduced in this PR, differing only in how
st.Nodes is allocated. SonarCloud flagged the duplication
(18.7% new-code duplication, threshold 3%).

Have the legacy entry points delegate to the *WithAllocator variants
with a nil allocator — the nil-allocator branch falls back to make(),
exactly matching the old behavior. Zero functional change; all tests
remain green under -race.

Recovery error string now reports "recovered in
DeserializeFromReaderWithAllocator" when the panic originated under
the legacy entry point. No tests pin the old string.
@sonarqubecloud

Copy link
Copy Markdown

@mrz1836 mrz1836 self-requested a review May 18, 2026 14:45

@mrz1836 mrz1836 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mrz1836 mrz1836 merged commit bd3d65e into master May 18, 2026
45 checks passed
@github-actions github-actions Bot deleted the feat/node-allocator branch May 18, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Any new significant addition size/L Large change (201–500 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants