perf: leaner XRPC request path (HTTP/2, single JSON serialization, DPoP) #201
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!201
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-194-request-path-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 #194.
The request path was already lean, so these are small, measured wins plus dead-code removal. There is no public API change (
PublicAPI.*.txtuntouched).XrpcClient.CreateMessage):Version = 2.0withRequestVersionOrLower. Concurrent calls to one PDS or AppView multiplex over a single connection. Plain-http and handlers without h2 fall back to 1.1. Against plc.directory the same change cut a concurrent burst from ~550 ms to ~335 ms p50 (#197).ByteArrayContentreused across retries. Requests carry aContent-Lengthinstead of going out chunked.Uridirectly, skipping a render and re-parse per request (about −240 B per DPoP request). Thehtunormalization is unchanged; the public string overloads share the same code.IsEnabled.AuthorizationServerDiscovery.ResolveFromIdentifierAsync, the twoFetch*MetadataAsyncwrappers,XrpcClient.HasAdminCredentialsandClearAdminCredentials. The tests that drove the discovery wrappers go through a small test-side extension.Net
src: −34 lines. Build has 0 warnings; 4962 unit 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.
✅ Verdict: approve — no blocking issues found.
Small, well-scoped performance/dead-code pass with no correctness issues found.
Version = 2.0+RequestVersionOrLoweris the right pattern — ALPN negotiation on TLS, automatic 1.1 fallback for plain http, handlers without h2 or proxy paths. Nothing in the client or readers depends on the response version.ByteArrayContentfactory is safe across the retry loop (each attempt gets a fresh content over the same serialized bytes; serialization is lazy so a null body still sends none). Content-Length comes for free, and the "bytes held for retry" trade-off is acknowledged in the PR table.Urioverloads computehtuvia the sameGetComponentsnormalization as before; userinfo/query/fragment handling is unchanged, and both public overloads keep their argument contracts (null → ArgumentNullException; non-absolute/no-host → ArgumentException), matching the existing tests.OAuthClientperforms the identity → PDS → authorization-server steps inline with identical semantics, and the tests cover themselves throughDiscoveryTestExtensions.Minor observation (not blocking): the tested
adjustedScopeString-style cleanups and theXrpcParams.AddAllrewrite are pure refactors with equivalent semantics; the log call behindIsEnabledavoids the arguments allocation only when a logger that filters Debug is installed — withNullLoggerit's a skip either way.⏱ 18m 31s · 13 steps · $0.0303 · tokens: 117.7k in / 1.5k out (+96.4k cache-read) · Go: 5h 0% · wk 0% · mo 91%
📜 Full session transcript
f68d55d3820e0dd80e14🤖 Reviewing this PR — the review will link the full session transcript.
0e0dd80e141abe9dfd6d🤖 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.
Re-review of the current head: unchanged from the state I approved earlier — no blockers, then or now.
Version = 2.0withRequestVersionOrLoweris correct — ALPN on TLS, automatic 1.1 fallback for plain http, handlers without h2, and proxies. Nothing downstream depends on the response version.ByteArrayContentfactory is safe across retries (a fresh content over the same serialized bytes each attempt;ByteArrayContentwraps without copying) and givesContent-Lengthfor free; the "bytes held for retry" trade-off is acknowledged in the PR table. Serialization stays deferred to first use, so failure timing matches the oldJsonContentbehaviour.Urioverloads go through the sameGetComponents(SchemeAndServer | Path)normalization as before (query/fragment/userinfo handling unchanged), and both public string overloads keep their argument contracts — null →ArgumentNullException, non-absolute/no-host →ArgumentExceptionnamingurl— as the existingDPoPProofGeneratorTestsexpect. The relaively relative/parsed-URI split is internal-only, withInternalsVisibleTowhere the tests need it.ResolveFromIdentifierAsync, the twoFetch*MetadataAsyncwrappers,HasAdminCredentials/ClearAdminCredentials); no stale references remain insrc, and the tests cover the same paths throughDiscoveryTestExtensions.Minor non-blocking notes:
DPoPProofGenerator.GenerateProof, the越过htu is nullbranch now raisesArgumentException("The URL is not an absolute URL naming a host.", "url")with a literal param name rather than anameof— same value, so no behavioural difference.IsEnabled(LogLevel.Debug)guard avoids the format-argument allocation only when a filtering logger is installed; withNullLoggerit was already a skip. Pure hygiene either way.Net effect is a small, measured improvement with no public API or behavioural change, per the PR's scenario table. Good to merge.
⏱ 0m 37s · 11 steps · $0.0122 · tokens: 36.9k in / 1.3k out (+132.6k cache-read) · Go: 5h 1% · wk 0% · mo 91%
📜 Full session transcript
@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 read the diff only and did not build or run the tests. It looks correct and matches the description.
Version = 2.0withRequestVersionOrLoweris the right policy. Plain-http and handlers without h2 fall back to 1.1.JsonBodyserializes lazily once and builds a freshByteArrayContentper attempt, so retries are safe and the request gets aContent-Length. The content type is still the sharedJsonMediaType, as before.NormalizeHtu(Uri)produces the samehtuas the string path, and theIsAbsoluteUricheck covers relative URIs. The public string overloads still throwArgumentNullExceptionfor null andArgumentExceptionfor non-absolute URLs.ResolveFromIdentifierAsync, the twoFetch*MetadataAsyncwrappers,HasAdminCredentialsandClearAdminCredentialsis consistent. The test-side extension keeps the discovery tests compiling.Minor notes, non-blocking:
ArgumentExceptionfor a badhtuno longer includes the offending URL.ObjectDisposedExceptioncheck inGenerateProofnow runs afterNormalizeHtu, so a disposed generator given a bad URL throwsArgumentExceptionrather thanObjectDisposedException.ClearAdminCredentials_RemovesAuthorizationHeadertest was deleted along with the method, which is fine since nothing calls it.🧠 claude-sonnet-5-5 · ⏱ 0m 11s · 2 turns · tokens: 32.4k in / 1.4k out (+0 cache-read)
📜 Full session transcript
1abe9dfd6d3de051cb98🤖 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 re-read the diff only and did not build or run the tests. My earlier approval had no blocking issues, so there was nothing to resolve. Nothing new blocks merging.
Version = 2.0withRequestVersionOrLowerfalls back to 1.1 for plain-http and handlers without h2.ByteArrayContent. Retries are therefore safe, and the request carries aContent-Length.Urioverload and the string overloads share the samehtunormalization.Minor, non-blocking notes (the first two were in my earlier review):
ArgumentExceptionfor a badhtuno longer names the offending URL.GenerateProofnow checksObjectDisposedExceptionafter normalizing the URL. A disposed generator given a bad URL therefore throwsArgumentExceptioninstead.🧠 claude-sonnet-5-5 · ⏱ 0m 5s · 2 turns · tokens: 21.1k in / 596 out (+12.2k cache-read)
📜 Full session transcript
Approval carry-over: this head (
3de051c) is a CHANGELOG-only rebase of the approved1abe9dfd. The PR's patch against its merge base, excludingCHANGELOG.md, has the samegit patch-id(d3b6dbe1). CI is green on the new head, and claude-reviewer approved it.