Skip to content

metadata: replace added [][]string with O(1) delta linked list#9129

Open
notandruu wants to merge 1 commit into
grpc:masterfrom
notandruu:metadata-outgoing-context-linked-list
Open

metadata: replace added [][]string with O(1) delta linked list#9129
notandruu wants to merge 1 commit into
grpc:masterfrom
notandruu:metadata-outgoing-context-linked-list

Conversation

@notandruu
Copy link
Copy Markdown

@notandruu notandruu commented May 16, 2026

Problem

AppendToOutgoingContext is O(N) per call because it allocates a new
[][]string of length N+1 and copies all N prior slices on every
invocation. A call chain with N sequential appends costs O(N²) in total
allocation and copy work. This is visible in production as latency spikes
in metadata-heavy RPCs.

Fixes #8860.

Solution

Replace rawMD.added [][]string with a singly-linked list of deltaKV
nodes. Each AppendToOutgoingContext call allocates exactly one node and
one flattened kv slice — O(1) regardless of chain depth.

FromOutgoingContext and fromOutgoingContextRaw traverse the chain
(newest-first), collect into a temporary slice, then iterate in reverse
to preserve FIFO ordering, keeping the read path O(N) and maintaining
backward-compatible return types for internal callers in transport and
stream code.

Benchmarks

Apple M3 Max, go1.26.3:

BenchmarkAppendToOutgoingContext (accumulating context, num=10):
  before: 3,263,813 ns/op  (quadratic growth)
  after:      1,646 ns/op  (1982× faster)

BenchmarkFromOutgoingContext:
  before: 544.3 ns/op
  after:  212.4 ns/op  (2.6× faster)

BenchmarkAppendToOutgoingContextN (fresh context, N sequential appends):
  N= 1:   87 ns/op,  160 B/op,   4 allocs
  N= 5:  446 ns/op,  800 B/op,  20 allocs
  N=10:  872 ns/op, 1600 B/op,  40 allocs
  N=50: 4373 ns/op, 8000 B/op, 200 allocs
  (linear: ~87·N ns/op)

All existing metadata tests pass.

RELEASE NOTES:

  • metadata: AppendToOutgoingContext is now O(1) per call instead of O(N), eliminating quadratic allocation overhead in metadata-heavy RPC call chains.

AppendToOutgoingContext was O(N) per call: it allocated a new [][]string
of length N+1 and copied all N prior slices into it on every append.
A context with N sequential appends therefore cost O(N²) in total
allocation/copy work.

Replace the flat slice with a singly-linked list of deltaKV nodes.
Each AppendToOutgoingContext call allocates exactly one node and one
flattened kv slice — O(1) regardless of chain depth.

FromOutgoingContext and fromOutgoingContextRaw collect the chain
(newest-first) into a temporary slice, then iterate in reverse to
preserve FIFO ordering, keeping the read path O(N) while maintaining
backward-compatible return types for internal callers.

Benchmark results on Apple M3 Max (go1.26.3):

  BenchmarkAppendToOutgoingContext (accumulating context, num=10):
    before: 3,263,813 ns/op  (quadratic growth, chain length ≈ 10·N)
    after:      1,646 ns/op  (1982× faster)

  BenchmarkFromOutgoingContext:
    before: 544.3 ns/op
    after:  212.4 ns/op  (2.6× faster)

  BenchmarkAppendToOutgoingContextN (fresh context, N appends):
    N= 1:   87 ns/op,  160 B/op,   4 allocs
    N= 5:  446 ns/op,  800 B/op,  20 allocs
    N=10:  872 ns/op, 1600 B/op,  40 allocs
    N=50: 4373 ns/op, 8000 B/op, 200 allocs
    (linear: ~87·N ns/op confirmed)

Fixes grpc#8860

Signed-off-by: Andrew Liu <andrewjliu22@gmail.com>
@linux-foundation-easycla
Copy link
Copy Markdown

CLA Not Signed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.25%. Comparing base (6602080) to head (7f6f7d3).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9129      +/-   ##
==========================================
+ Coverage   83.20%   83.25%   +0.04%     
==========================================
  Files         414      414              
  Lines       33489    33496       +7     
==========================================
+ Hits        27865    27887      +22     
+ Misses       4214     4201      -13     
+ Partials     1410     1408       -2     
Files with missing lines Coverage Δ
metadata/metadata.go 94.78% <100.00%> (+2.19%) ⬆️

... and 17 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@notandruu
Copy link
Copy Markdown
Author

Could a maintainer add a Type: label to unblock PR Validation? Happy to make any changes needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata.FromOutgoingContext is too slow

1 participant