perf(loki.process): Reduce pipeline latency with buffered inter-stage channels - #6875
Draft
csmarchbanks wants to merge 1 commit into
Draft
perf(loki.process): Reduce pipeline latency with buffered inter-stage channels#6875csmarchbanks wants to merge 1 commit into
csmarchbanks wants to merge 1 commit into
Conversation
csmarchbanks
commented
Aug 12, 2026
| // (instead of requiring a synchronous unbuffered rendezvous) reduce the | ||
| // goroutine park/wake cost that dominates a pipeline with many stages? 0 | ||
| // keeps the original unbuffered behavior. | ||
| const entryChanBufferSize = 16 |
Author
There was a problem hiding this comment.
I tested the benchmarks across a variety of buffer sizes and with trying to set to ~GOMAXPROCS. 16 seemed like a reasonable balance of improving performance without too much complexity or memory across all core counts.
Every stage-to-stage handoff used an unbuffered channel, which forces a synchronous rendezvous (sender blocks until a receiver is ready) for every single entry at every single stage boundary. That rendezvous is where the goroutine park/wake cost identified in BenchmarkPipelineManyRules and BenchmarkPipelineManyStreamsSaturated actually comes from. Giving each channel created via the new newEntryChan a small buffer lets a send complete without waiting for the receiving goroutine to actually be scheduled, decoupling adjacent stages' scheduling instead of forcing them to synchronize on every entry. Swept buffer sizes 0/1/4/16/64 and GOMAXPROCS 2/4/8/18 (see conversation): the improvement is consistent across every GOMAXPROCS level tried, unlike the larger fuse-into-one-goroutine redesign, which trades away single-idle-stream latency for a multi-stream throughput win. 16 is the sweet spot found in this sweep; benefit drops off by 64. This is deliberately a minimal, surgical change: only channel construction sites change (all now go through newEntryChan), nothing about the stage interfaces, goroutine topology, or match/pipeline structure is touched. Assisted-by: Claude Sonnet 5
4 tasks
csmarchbanks
force-pushed
the
loki-process-buffered-channels
branch
from
August 13, 2026 14:58
b4ce31c to
b79220f
Compare
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brief description of Pull Request
Alternative, smaller-diff approach to the same problem as #6873:
loki.processgives every pipeline stage its own unbuffered channel, forcing a synchronous rendezvous (sender blocks until the receiver is scheduled) for every entry at every stage boundary. This PR keeps the existing goroutine-per-stage architecture entirely as-is and only changes channel construction: every inter-stage channel now goes through one helper,newEntryChan()(pipeline.go), with a buffer of 16 instead of 0. No stage logic, interfaces, or goroutine topology change.This PR is now chained on top of #6879, which holds the shared benchmark commit as a common base (that commit also picked up a benchmark-harness fix: the drain goroutine wasn't awaited, so a sub-benchmark's leftover work could bleed CPU into the next one's timing). This branch itself contains only the one-constant change (
b79220f83).git checkout loki-process-benchmarksvsgit checkout loki-process-buffered-channelsreproduces the numbers below.benchstat,GOMAXPROCS=2,-count=10, re-run against the fixed benchmark harness (this supersedes the numbers previously posted here, which predated that fix):The single-stream latency win here is larger across the board than #6873's full redesign (e.g. -83.9% vs -77.1% at
rules=1000), consistent with the original sweep. The concurrent-throughput win is real but smaller than the full redesign (+142% vs +577%entries/sec) — and this re-run also surfaces a real cost the earlier numbers didn't show:PipelineManyStreamsSaturatedmemory jumps +957% (6.6MiB → 69.9MiB per op) and allocations +44%, versus #6873's redesign which cuts both (-84% / -91%). Buffering trades memory for latency by letting more entries sit in flight at once; the full redesign avoids that trade-off entirely by removing the per-stage goroutines and channels rather than padding them.Buffer size was swept (0/1/2/4/8/16/32/64) at several
GOMAXPROCSlevels before settling on 16: single-stream latency keeps improving monotonically at least to buffer=32 with no core-count-dependent downside found, but the concurrent-streams throughput benchmark has a real peak (varies by ~4-16 depending onGOMAXPROCS) and loses most of its gain by buffer=64. 16 balances both. (This sweep itself predates the harness fix and hasn't been re-run; the memory trade-off above means it's worth re-sweeping withB/opas a second axis, not just throughput.)Verified with
go test -raceacrossinternal/component/loki/process/...andgolangci-lint(no new findings).Pull Request Details
Issue(s) fixed by this Pull Request
Notes to the Reviewer
PR Checklist