perf: cut allocations in firehose, archive and Spaces decoding #205
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
5 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!205
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-195-streaming-perf"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #195.
What changed
DagCborJson,DagCborDecoder,EventStreamFrame). OneArrayBufferWriterper thread, taken and returned synchronously around each decode and dropped if it grows past 1 MB. It replaces a fresh buffer of 2× the frame per frame: a 2 MB commit used to allocate 4 MB every time. Reads are bounded byWrittenSpan, and a test covers decoding after a failed decode.#commit/#sync/… types.RecordProofbuilds the node CID string only for an error message.DagCborEncodergets one$link/$byteshelper.AtProtoCryptodrops its padded big-endian comparer in favour ofSequenceCompareTo: every caller checks the 64-byte P1363 length first, and the order constants are fixed 32-byte arrays, so the comparison is numeric.LtHashgets one lane combiner.Behaviour note
The transcoder now rejects indefinite-length text and byte strings in frames. DAG-CBOR forbids them, and the stricter readers (
ReadSeq,TryScan,MstNodeData) already did, so such a frame is dropped as malformed instead of accepted in lax mode.Measurements (BenchmarkDotNet, in-process, baseline
origin/main)Verification time is dominated by native ECDSA (~206 of ~240 µs per commit).
Net
src: −25 lines. Build has 0 warnings; 4966 tests pass.🤖 Generated with Claude Code
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
🤖 Resolving the merge conflicts — the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the fix runs.
Independent adversarial review (read-only Sonnet reviewer): no defects found. A differential probe of
DagCborDecoder.Decodeagainstmaingave byte-identical output on 9 cases: invalid UTF-8 variants, indefinite lengths, escapes, duplicate keys and key order. The reused buffer is synchronous-only, nested-safe and returned infinally, and nothing references it after return. EverySequenceCompareTocaller is gated on the 64-byte length, and the order constants are 32 bytes.LtHash.Combinematches the old add/subtract lanes. Unit suite: 4966 passed.32fb60b1d233ae74e1a6⚠️ The conflict run failed: git -c http.extraheader=Authorization: Basic [REDACTED] push origin HEAD:refs/heads/issue-195-streaming-perf failed (1): To http://forgejo:3000/Grandiras/ATProto.NET.git
! [rejected] HEAD -> issue-195-streaming-perf (fetch first)
error: failed to push some refs to 'http://forgejo:3000/Grandiras/ATProto.NET.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. T
@claude /review
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
I found no blocking problems. I read the diff and the surrounding code; I did not build the PR or run the tests.
finallyruns. TheUtf8JsonWriteris disposed before the buffer is reset. A failed decode leaves nothing behind becauseResetWrittenCountruns infinally, and there is a test for that. A nested call gets a fresh buffer because the field is nulled on rent. The 1 MB cap bounds memory held per thread.ReadDefiniteLengthTextStringBytesandReadDefiniteLengthByteStringraiseInvalidOperationExceptionorCborContentExceptionon indefinite-length strings. Every call site (DagCborDecoder,TryReadHeader,Deserialize) already catches those throughIsMalformed. The stricter handling is documented in the PR description.ReadUtf8: The span stays valid for the writer call. Invalid UTF-8 goes through a string round trip, which reproduces the old replacement behaviour, and a test covers it.SequenceCompareTois numerically correct for equal-length big-endian inputs, andNormalizeSnow returns early on the same conditions as before.LtHash.Combine: Behaviour is equivalent to the two removed helpers.Minor: a short comment in
AtProtoCryptostating that theSequenceCompareTocalls rely on the callers' 64-byte length check would stop a future caller from quietly breaking the numeric ordering.🧠 claude-sonnet-5-5 · ⏱ 0m 16s · 3 turns · tokens: 27.2k in / 1.8k out (+49.4k cache-read)
📜 Full session transcript
33ae74e1a69533157f8c🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
I found no blocking problems. I read the diff and did not build the PR or run the tests.
My earlier pass was an approval with one minor, non-blocking note: a comment in
AtProtoCryptosaying that theSequenceCompareTocalls depend on the callers' 64-byte P1363 length check. The diff doesn't add it. It would be a cheap guard against a future caller breaking the numeric ordering, but it doesn't block merging.Re-checked and still fine:
finally. The writer is disposed before the reset, and a nested call gets a fresh buffer. The test for decoding after a failed decode covers the reset.ReadDefinite*reads reject indefinite-length strings, and every call site already catches that through its malformed-input handling. The PR description documents the stricter behaviour.LtHash.CombineandNormalizeS: both are equivalent to the code they replace.🧠 claude-sonnet-5-5 · ⏱ 0m 6s · 2 turns · tokens: 26.0k in / 759 out (+12.2k cache-read)
📜 Full session transcript
💬 Verdict: comment — the reviewer did not submit a structured review; the raw output follows.
Let me check the crypto context for the
SequenceCompareTochange, since length mismatches matter there.⏱ 6m 42s · 3 steps · $0.0057 · tokens: 21.0k in / 126 out (+9.0k cache-read) · Go: 5h 0% · wk 0% · mo 91%
📜 Full session transcript
@opencode-bot /review
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
✅ Verdict: approve — no blocking issues found.
No blocking problems found. This is a fresh pass (no prior opencode-reviewer findings to resolve); the earlier approvals from claude-reviewer stand under my own checks.
Verified points:
DagCborJson.RentBuffer/ReturnBuffer): rent/return is synchronous around each decode, theUtf8JsonWriteris disposed beforeResetWrittenCountruns infinally, and the output (WrittenSpan→JsonElement.ParseValue/JsonSerializer.Deserialize) is consumed inside thetry, so nothing references the reused buffer afterwards. A nested call rents a fresh buffer because the field is nulled on rent. The 1 MB cap bounds what a thread retains, and the newDecode_AfterAFailedDecode_ReturnsOnlyItsOwnValuetest covers a failed decode leaving no residue.SequenceCompareToinAtProtoCrypto: every reachable caller checks the 64-byte P1363 signature length first —AtProtoKey.Verify(viaHasSignatureLength),DidKeyCache.VerifyJws, andAtProtoKey.Signvia the fixed-field signature format — andOrderBytes/HalfOrderare fixed 32-byte big-endian arrays, so byte-wise lexicographic order equals numeric order.NormalizeLowSSignature's restructure preserves the old behaviour for the reachable inputs. The minor suggestion to add a comment stating the 64-byte length precondition (raised in previous passes) is still worth a line, but it remains non-blocking.ReadDefiniteLengthTextStringBytes/ReadDefiniteLengthByteString): indefinite-length strings now throw, and every call site (DagCborDecoder.Decode,EventStreamFrame.TryReadHeader/Deserialize,SpaceRecordCbor) already filters malformed input throughIsMalformedor its equivalent, so an indefinite-length frame is dropped as malformed rather than crashing. The behaviour change is documented in the PR description.ReadUtf8invalid-UTF-8 fallback: goes through a string round trip so replacement matches what the laxCborReaderproduced before, and the returned span is written immediately. The randomized comparison plus the new test give good coverage.LtHash.Combine,RecordProof.Name(error-path-only string),SpaceRepoCarbinary CID comparison, and theDagCborEncoder.TryGetOnlyStringhelper: all behaviour-equivalent to what they replace.⏱ 0m 53s · 10 steps · $0.0129 · tokens: 30.6k in / 1.1k out (+140.2k cache-read) · Go: 5h 0% · wk 0% · mo 91%
📜 Full session transcript