Skip to content

Parallelize node/edge encoding in serializeGraphStore - #291

Merged
mbastian merged 2 commits into
masterfrom
feature/parallel-serialize
Aug 25, 2026
Merged

Parallelize node/edge encoding in serializeGraphStore#291
mbastian merged 2 commits into
masterfrom
feature/parallel-serialize

Conversation

@mbastian

Copy link
Copy Markdown
Member

Summary

  • serializeNode/serializeEdge and the main write loop now call type-specific writers directly instead of routing every scalar field through the generic ~110-branch serialize(DataOutput, Object) dispatcher. Byte-identical by construction; reduces per-element overhead that would otherwise be multiplied across worker threads.
  • serializeGraphStore now fans node/edge encoding out across a per-call ExecutorService whenever a store spans more than one internal storage block, reusing NodeStore/EdgeStore's existing lock-free, block-boundary-aware Spliterator (already backing parallelStream()) to split work without touching any locking. Results are drained back to the output stream through a bounded in-flight window (not a single blocking invokeAll) so peak memory stays bounded instead of materializing the whole payload at once. Stores with a single block (small graphs, all existing golden fixtures) take the exact same sequential path as before.
  • No public API or on-disk format changes: GraphModel.Serialization.write(DataOutput, GraphModel) keeps its signature and produces byte-identical output for a given graph.

Test plan

  • testParallelAndSequentialNodeEdgeEncodingProduceIdenticalBytes: byte-equality between the sequential and parallel encode paths on a multi-block graph with scattered garbage (removed) slots.
  • testSerializeAndDeserializeMultiBlockGraphRoundTrips: end-to-end round-trip through the public serializeGraphStore/deserializeGraphStore on a multi-block graph, confirming the parallel path is actually exercised.
  • testSerializeChunksInParallelPropagatesExceptionAndLeavesNoThreads: a failure mid-chunk surfaces as the expected exception and leaves no leaked worker threads.
  • Existing golden-fixture / byte-pin / determinism tests pass unchanged (single-block graphs take the sequential path).
  • Full suite green (1643 tests), run 3x to check for concurrency flakiness.

Note: while writing the round-trip test I found a pre-existing, unrelated bug — a graph that's both multi-block and has scattered node/edge removals fails to round-trip with "The edge source or target can't be found," reproducible on the pre-existing sequential code too. Tracking that separately; it's not introduced by this change and this PR's tests route around it.

🤖 Generated with Claude Code

serializeNode/serializeEdge and the main write loop routed every
scalar field (storeId, edge type, weight, directed flag, properties)
through the generic ~110-branch serialize(DataOutput, Object)
dispatcher. Call the type-specific writers directly instead -
byte-identical by construction, and removes a per-element dispatch
cost that would otherwise be multiplied across worker threads once
serialization is parallelized.
NodeStore/EdgeStore already have a lock-free, block-boundary-aware
Spliterator (backing parallelStream()) that splits at storage block
boundaries and skips garbage slots. Use it to fan node/edge encoding
out across a per-call thread pool whenever a store spans more than one
block, and drain results back to the output stream through a bounded
in-flight window (not invokeAll) so peak memory stays bounded instead
of materializing the whole payload at once.

Below the single-block threshold - which trySplit() reports on its
own - encoding stays on the calling thread with the exact same code
path, so small graphs are unaffected. Output bytes are unchanged
either way: no shared mutable state exists on the write path, so
concatenating independently-encoded chunks in original block order
reproduces today's exact serialization format.
@mbastian

Copy link
Copy Markdown
Member Author

Ran an benchmark on the graphstore-benchmark repo:

Ran both versions with default JMH settings (3 forks × 5×1s iterations). Deserialize is essentially unchanged (parallel writing doesn't touch reads), but serialize shows a large, size-dependent speedup:

┌─────────────────┬──────────────┬────────────┬─────────────────┬─────────────────────┐
│    workload     │   chunks     │   0.8.6    │ 0.8.7-SNAPSHOT  │       speedup       │
│  (nodes:edges)  │ (node/edge)  │ serialize  │    serialize    │                     │
├─────────────────┼──────────────┼────────────┼─────────────────┼─────────────────────┤
│                 │              │            │                 │ ~1.0× (no benefit — │
│ 4000:8000       │ 1/1          │ 2.439 ms   │ 2.413 ms        │  single chunk,      │
│                 │              │            │                 │ nothing to          │
│                 │              │            │                 │ parallelize)        │
├─────────────────┼──────────────┼────────────┼─────────────────┼─────────────────────┤
│ 20000:100000    │ 3/4          │ 28.412 ms  │ 4.488 ms        │ 6.3×                │
├─────────────────┼──────────────┼────────────┼─────────────────┼─────────────────────┤
│ 100000:500000   │ 13/16        │ 185.497 ms │ 15.591 ms       │ 11.9×               │
├─────────────────┼──────────────┼────────────┼─────────────────┼─────────────────────┤
│ 500000:2500000  │ 62/77        │ 1199.710   │ 88.823 ms       │ 13.5×               │
│                 │              │ ms         │                 │                     │
└─────────────────┴──────────────┴────────────┴─────────────────┴─────────────────────┘
```

The speedup tracks chunk count almost exactly — flat at 1 chunk, then climbing toward ~13-14× as the graph spans dozens of NodeBlocks/EdgeBlocks, consistent with parallel writing scaling with the number of independently-writable blocks (this machine likely has ≥16 cores available to soak up that parallelism).

@mbastian mbastian added this to the 0.8.7 milestone Aug 25, 2026
@mbastian
mbastian merged commit a0c56f2 into master Aug 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant