Skip to content

perf(loki.process): Run stages that implement the Processor interface together to reduce channel usage - #6882

Draft
csmarchbanks wants to merge 2 commits into
loki-process-benchmarksfrom
loki-process-narrow-processor-fusion
Draft

perf(loki.process): Run stages that implement the Processor interface together to reduce channel usage#6882
csmarchbanks wants to merge 2 commits into
loki-process-benchmarksfrom
loki-process-narrow-processor-fusion

Conversation

@csmarchbanks

Copy link
Copy Markdown

Brief description of Pull Request

Alternative, smaller-diff approach to the same problem as #6873 and #6875: loki.process gives every pipeline stage its own goroutine and unbuffered channel, even though most stages are pure synchronous single-entry transforms.

This PR doesn't introduce a new stage interface or touch any stage implementation. It recognizes stages already wrapped via the existing Processor interface (docker, label_drop, label_keep, logfmt, luhn, output, pattern, regex, replace, static_labels, template, tenant, timestamp) as safe to call directly as plain functions instead of through a channel, since Processor.Process has no return value to signal drop or fan-out. Pipeline.Run fuses maximal contiguous runs of these into one goroutine; matcherStage caches the same fusion for its nested pipeline at construction, recursing through further nested match blocks. Every other stage (json, labels, drop, sampling, limit, metric, pack, truncate, windowsevent, decolorize, eventlogmessage, geoip, structured_metadata[_drop], split_json, cri, multiline) keeps its existing Run()-based channel behavior unchanged. Diff is two non-test files (match.go, pipeline.go).

This PR is chained on top of #6879 (shared benchmark commit). git checkout loki-process-benchmarks vs git checkout loki-process-narrow-processor-fusion reproduces the numbers below.

benchstat, GOMAXPROCS=2, -count=10:

                                                             │       base (#6879)        │   this branch (56a4a8c81/46664433c)  │
                                                             │           sec/op          │    sec/op     vs base                │
PipelineManyRules/rules=1-2                                              708.3n ±  2%   568.9n ± 1%  -19.69% (p=0.000 n=10)
PipelineManyRules/rules=10-2                                            2299.5n ±  2%   868.6n ± 1%  -62.23% (p=0.000 n=10)
PipelineManyRules/rules=50-2                                             9.973µ ±  1%   2.332µ ± 1%  -76.62% (p=0.000 n=10)
PipelineManyRules/rules=100-2                                           19.051µ ±  1%   3.892µ ± 2%  -79.57% (p=0.000 n=10)
PipelineManyRules/rules=500-2                                            84.05µ ±  1%   17.14µ ± 1%  -79.61% (p=0.000 n=10)
PipelineManyRules/rules=1000-2                                          152.10µ ±  1%   37.02µ ± 0%  -75.66% (p=0.000 n=10)
PipelineOneMultilineAmongManyRules/all_sync-2                           149.30µ ±  1%   36.00µ ± 1%  -75.88% (p=0.000 n=10)
PipelineOneMultilineAmongManyRules/one_multiline_at_start-2             151.46µ ±  2%   35.92µ ± 1%  -76.28% (p=0.000 n=10)
PipelineOneMultilineAmongManyRules/one_multiline_in_middle-2            151.25µ ±  2%   18.99µ ± 1%  -87.45% (p=0.000 n=10)
PipelineOneMultilineAmongManyRules/one_multiline_at_end-2               148.66µ ±  2%   35.44µ ± 1%  -76.16% (p=0.000 n=10)
PipelineManyStreamsSaturated-2                                          173.68m ±  2%   26.17m ± 2%  -84.93% (p=0.000 n=10)
geomean                                                                  73.72µ         18.01µ       -75.56%

                               │       base (#6879)        │   this branch (56a4a8c81/46664433c)   │
                               │        entries/sec        │  entries/sec    vs base                │
PipelineManyStreamsSaturated-2              9.212k ±  2%   61.132k ± 2%  +563.61% (p=0.000 n=10)

                               │       base (#6879)        │   this branch (56a4a8c81/46664433c)   │
                               │        B/op / allocs/op   │      B/op / allocs/op   vs base        │
PipelineManyStreamsSaturated-2       6.612Mi / 88.37k      1.309Mi / 16.15k    -80.20% / -81.72% (p=0.000 n=10)

PipelineManyRules is one stream, mostly idle. PipelineOneMultilineAmongManyRules checks that a single stage needing its own channel (nesting stage.multiline) doesn't drag the rest of a 1000-rule pipeline back to the old per-stage-channel cost. PipelineManyStreamsSaturated models many concurrent streams competing for the CPU, which is the throughput number that matters for a loaded instance: +564% entries/sec, with -80% memory and -82% allocs.

Directly against #6873 (same benchmarks, GOMAXPROCS=2, both branches diffed against the same #6879 base): latency and throughput are within noise of each other (geomean -0.11%) on this benchmark suite, because every rule here is stage.static_labels, which both PRs already fuse identically. This suite can't show the actual coverage difference: #6873 additionally fuses stage.labels, drop, sampling, limit, json, metric, pack, truncate, windowsevent, decolorize, eventlogmessage, geoip, and structured_metadata[_drop] by retrofitting those 13 stage implementations to a new SyncStage interface (a 25-file diff); this PR fuses only the pre-existing Processor-wrapped set and leaves every other stage's implementation untouched. On PipelineManyStreamsSaturated, this PR also shows ~24% more memory and ~2x the allocs/op of #6873 (16.15k vs 8.06k) — traced to Pipeline.Run recomputing which stages fuse on every call instead of caching that decision once at construction the way #6873's syncFn does; in production Run is only called once per component (re)configuration, so this doesn't scale with traffic, but it's a real, fixable gap relative to #6873's approach.

Verified with go test -race across internal/component/loki/process/... and golangci-lint (no new findings).

Pull Request Details

Issue(s) fixed by this Pull Request

Notes to the Reviewer

PR Checklist

  • Documentation added
  • Tests updated
  • Config converters updated
  • This pull request was substantially generated with AI assistance (see the GenAI policy)

@csmarchbanks csmarchbanks changed the title perf(loki.process): run all sync stages together to reduce channel usage perf(loki.process): run stages that implement the Processor interface together to reduce channel usage Aug 13, 2026
@csmarchbanks csmarchbanks changed the title perf(loki.process): run stages that implement the Processor interface together to reduce channel usage perf(loki.process): Run stages that implement the Processor interface together to reduce channel usage Aug 13, 2026
Recognizes stages wrapped via the Processor interface (docker, label_drop,
label_keep, logfmt, luhn, output, pattern, replace, static_labels, template,
tenant, timestamp) as safe to run as plain function calls instead of their
own channel and goroutine, since Processor.Process can never drop or fan out
an entry. Pipeline.Run fuses maximal contiguous runs of these into one
goroutine; matcherStage caches the same fusion for its nested pipeline at
construction, recursing through further nested match blocks.

Every other stage (json, drop, sampling, limit, split_json, multiline, etc.)
keeps its existing Run()-based channel behavior unchanged.

Assisted-by: Claude Sonnet 5
Self-review turned up two real coverage gaps: a top-level pipeline with
qualifying stages on both sides of a non-qualifying one (exercises the
flush/restart boundary in Pipeline.Run), and two independent match blocks
where a drop precedes an unrelated keep (exercises composeNarrow's skip
short-circuit across match boundaries, not just within one). Also fixes
the trySyncNarrow comment, which omitted "regex" from the list of
toStage-wrapped stages that silently qualify for fusion.

Assisted-by: Claude Sonnet 5
@csmarchbanks
csmarchbanks force-pushed the loki-process-narrow-processor-fusion branch from 4666443 to 55c7007 Compare August 13, 2026 22:41
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.

1 participant