Skip to content

[PERF] Inhibitor: Add inverted index for O(k) rule lookup instead of O(N) linear scan.#5021

Open
Mwea wants to merge 7 commits intoprometheus:mainfrom
Mwea:perf/invertex-index-inhibit
Open

[PERF] Inhibitor: Add inverted index for O(k) rule lookup instead of O(N) linear scan.#5021
Mwea wants to merge 7 commits intoprometheus:mainfrom
Mwea:perf/invertex-index-inhibit

Conversation

@Mwea
Copy link
Copy Markdown
Contributor

@Mwea Mwea commented Feb 20, 2026

Pull Request Checklist

Please check all the applicable boxes.

  • Please list all open issue(s) discussed with maintainers related to this change
    • N/A
  • Is this a new Receiver integration?
    • N/A
  • Is this a bugfix?
    • N/A
  • Is this a new feature?
    • I have added tests that test the new feature's functionality
  • Does this change affect performance?
    • I have provided benchmarks comparison that shows performance is improved or is not degraded
    • I have added new benchmarks if required or requested by maintainers
  • Is this a breaking change?
    • My changes do not break the existing cluster messages
    • My changes do not break the existing api
  • I have added/updated the required documentation
  • I have signed-off my commits
  • I will follow best practices for contributing to this
    project

[PERF] Inhibitor: Add inverted index for O(k) rule lookup instead of O(N) linear scan.

This PR aAdd an inverted index for inhibit rule target matcher lookup to achieve O(k) rule selection instead of O(N) linear scan, where k = number of labels on the alert and N = number of inhibit rules.

  • Add benchmarks to measure scaling behavior with different rule distributions
  • Refactor Mutes() to extract core checking logic into checkInhibit()
  • Implement ruleIndex with configurable thresholds for index construction

Motivation

When alertmanager has many inhibit rules (e.g., hundreds or thousands), the current implementation checks every rule for every alert, resulting in O(N) complexity. In environments with rules targeting different label values (e.g., per-cluster or per-service rules), most of this work is wasted.

Benchmark Results

Summary

Scenario Before After Improvement Complexity (before) Complexity (after)
different_targets 28.2µs 1.6µs 18x faster O(N) O(k)
no_match 11.5µs 0.8µs 15x faster O(N) O(k)
same_target 143µs 136µs ~same O(N) O(N) fallback

N = number of rules, k = number of labels on alert

Details

Benchmark Baseline (ns/op) HEAD (ns/op) Delta
BenchmarkMutesScaling/different_targets/rules=10 1708 1524 -10.8%
BenchmarkMutesScaling/different_targets/rules=100 4023 1523 -62.1%
BenchmarkMutesScaling/different_targets/rules=1000 28231 1565 -94.5%
BenchmarkMutesScaling/same_target/rules=10 2439 2396 -1.8%
BenchmarkMutesScaling/same_target/rules=100 14970 14380 -3.9%
BenchmarkMutesScaling/same_target/rules=1000 143224 135880 -5.1%
BenchmarkMutesScaling/no_match/rules=10 760 744 -2.1%
BenchmarkMutesScaling/no_match/rules=100 1756 739 -57.9%
BenchmarkMutesScaling/no_match/rules=1000 11492 778 -93.2%

When the index is effective (O(1) lookup):

  • Rules have different equality target matchers (e.g., cluster=X)
  • Alert labels allow direct lookup into the index

When the index falls back to O(N) scan:

  • All rules share the same target matcher (high overlap)
  • Rules use regex matchers only

Summary by CodeRabbit

  • Performance

    • Faster and more scalable inhibition checks via a new indexing approach that reduces candidate scanning and short-circuits when matches are found, improving throughput for large rule sets.
  • Tests

    • Added extensive benchmarks covering scaling, overlap ratios, and index thresholds.
    • Added unit tests for indexed candidate selection, deduplication, and mixed-index/linear scenarios.

@Mwea Mwea marked this pull request as ready for review February 20, 2026 16:25
Copy link
Copy Markdown
Contributor

@ultrotter ultrotter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, with some minor comments/nits

ih := NewInhibitor(s, rules, m, promslog.NewNopLogger())
defer ih.Stop()
go ih.Run()
<-time.After(time.Second)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do ih.WaitForLoading() here instead of effectively a sleep?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, replaced all occurrences with WaitForLoading().

}

// RuleIndexOptions configures the rule index behavior.
type RuleIndexOptions struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this and the one below DefaultRuleIndexOptions, should we consider private as well? Or do we have a reason to export?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, made both unexported since they're only used internally.

}

func TestForEachCandidate_EarlyTermination(t *testing.T) {
rules := []*InhibitRule{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this test have more than 2 rules to also test the early termination via index? Or is it not necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more rules (4 total) to ensure we're testing index-based iteration rather than linear scan at the threshold boundary.

@Mwea
Copy link
Copy Markdown
Contributor Author

Mwea commented Feb 26, 2026

Approved, with some minor comments/nits

Thanks ! I will tackle this tomorrow.
I had some thoughts on proposing a similar solution for the Silencer , but I don't really know it would bring that much benefits as for this one. Any thoughts ? @ultrotter

@ultrotter
Copy link
Copy Markdown
Contributor

Approved, with some minor comments/nits

Thanks ! I will tackle this tomorrow. I had some thoughts on proposing a similar solution for the Silencer , but I don't really know it would bring that much benefits as for this one. Any thoughts ? @ultrotter

We already have some caches in the silencer to improve performace, plus you have the situation that the rules would not be static based on the config, right? This would mean extra locking to make it work which may or may not create an issue? Let's focus on the current patches, then we'll see what we can do if we see that you have scalability issues there! :)

@Mwea Mwea force-pushed the perf/invertex-index-inhibit branch from 2cac831 to 785c02a Compare March 10, 2026 11:09
Mwea and others added 5 commits March 10, 2026 12:16
Add BenchmarkMutesScaling with three cases to measure how Mutes()
performance scales with rule count:

- different_targets: Each rule has unique target matcher, only one
  matches the alert (best case for selective lookup)
- same_target: All rules have same target matcher, all must be checked
- no_match: Alert matches no rule's target, all must be checked

These benchmarks establish baseline performance for potential
optimizations to the inhibition rule matching logic.

Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
Extract the core inhibition checking logic into a separate checkInhibit
method. This separates concerns:

- Mutes(): handles tracing span lifecycle and marker updates
- checkInhibit(): contains the rule iteration logic

This refactoring prepares for future optimizations to the rule matching
logic without changing the public API or tracing behavior.

No functional changes.

Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
Add ruleIndex to Inhibitor for O(k) rule lookup instead of O(N) linear
scan, where k = number of labels and N = number of inhibit rules.

When the index IS effective (O(1) lookup):
- Rules have different equality target matchers (e.g., cluster=X)
- Alert labels allow direct lookup into the index
- Example: 1000 rules each targeting different clusters, checking
  an alert for cluster=999 → only examines 1 rule instead of 1000

When the index is NOT effective (falls back to O(N) scan):
- All rules share the same target matchers (e.g., all target dst=0)
- Rules use regex or not-equal matchers (cannot be indexed)
- High-overlap matchers excluded from indexing (>50% of rules)

Implementation details:
- Index rules by exact match target matchers at construction time
- Use callback pattern (forEachCandidate) to avoid slice allocation
- Pool visited map to reduce GC pressure
- Skip deduplication for single-matcher rules

Benchmark results (BenchmarkMutesScaling, 1000 rules):

  different_targets: 32µs → 2.1µs  (15x faster, index effective)
  no_match:          15µs → 1.0µs  (15x faster, index effective)
  same_target:      218µs → 209µs  (no change, index not effective)

Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
Replace hardcoded constants with RuleIndexOptions struct to allow
testing different threshold values.

Benchmark results for MinRulesForIndex (ns/op):

  rules | linear | indexed
     1  |     17 |      17
     2  |     29 |      85
     5  |     68 |      84
    10  |    135 |      94

Crossover at ~7 rules. Default of 2 enables indexing early since
high-overlap detection handles pathological cases.

Benchmark results for MaxMatcherOverlapRatio (ns/op):

  ratio | time
   0.10 |  183
   0.50 |  186
   0.60 |  552
   1.00 |  571

Clear cliff between 0.5 and 0.6 with 3x degradation. Default of 0.5
is optimal - highest value before performance degrades.

Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
- Replace time.After sleeps with WaitForLoading() in benchmarks
- Make ruleIndexOptions and defaultRuleIndexOptions unexported
- Expand early termination test to use more rules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
@Mwea Mwea force-pushed the perf/invertex-index-inhibit branch from 785c02a to 113e756 Compare March 10, 2026 11:17
Copy link
Copy Markdown
Contributor

@TheMeier TheMeier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This possibly causes one change in behaviour, which I consider ok.
Previously Mutes walked ih.rules in config order, so first-match selection was deterministic. Now it is possible that one sees a different inhibition in Alert.InhibitedBy. (Even though it is a slice only one seems to get added)

while InhibitedBy may contain only a subset of the inhibiting alerts
// – in practice exactly one ID. (This somewhat confusing semantics might change
// in the future.)

@Mwea
Copy link
Copy Markdown
Contributor Author

Mwea commented Mar 12, 2026

This possibly causes one change in behaviour, which I consider ok. Previously Mutes walked ih.rules in config order, so first-match selection was deterministic. Now it is possible that one sees a different inhibition in Alert.InhibitedBy. (Even though it is a slice only one seems to get added)

while InhibitedBy may contain only a subset of the inhibiting alerts
// – in practice exactly one ID. (This somewhat confusing semantics might change
// in the future.)

@TheMeier Would like me to tackle it by documenting the change or do you feel it is good enough to get it merged ?

@TheMeier
Copy link
Copy Markdown
Contributor

@Mwea I personally do not care. But maybe someone else does, who knows?

Comment on lines +176 to +185
for labelName, labelValue := range lset {
valueMap, ok := idx.exactIndex[string(labelName)]
if !ok {
continue
}

rules, ok := valueMap[string(labelValue)]
if !ok {
continue
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran an analysis and it seems if someone uses a marcher with an empty value this logic changes behaviour: https://app.devin.ai/review/prometheus/alertmanager/pull/5021

I don't know how common is such a configuration though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! You're right , this was indeed a correctness regression.

Fixed by excluding empty-value matchers from indexing, they now fall back to linearRules where they're always checked. Added a regression test as well

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Adds a ruleIndex-based optimization for inhibition checks, a new checkInhibit helper used by Mutes, extensive benchmarks for scaling and overlap behavior, and comprehensive tests covering index construction, candidate iteration, and deduplication.

Changes

Cohort / File(s) Summary
Inhibitor core
inhibit/inhibit.go
Adds ruleIdx *ruleIndex field, introduces checkInhibit(lset, now, span) (model.Fingerprint, bool), and updates Mutes to use the index-based candidate lookup, adjust trace events, markers, and early-return on inhibition.
Rule index implementation
inhibit/rule_index.go
New unexported ruleIndex type with exactIndex, singleMatcherRules, linearRules, visited-rule pooling, constructor/options, two-pass index build (overlap detection), and forEachCandidate iteration with deduplication and linear fallback.
Benchmarks
inhibit/inhibit_bench_test.go
Replaces sleep with ih.WaitForLoading(), adds BenchmarkMutesScaling and helper benchmarks for different target/same target/no match, plus benchmarks for index thresholds and matcher-overlap ratios.
Index tests
inhibit/rule_index_test.go
New comprehensive tests for index construction, overlap thresholds, regex and mixed matchers, deduplication, linear vs indexed paths, early termination behavior, and option variants.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Alert as AlertProducer
  participant Inh as Inhibitor
  participant Index as ruleIndex
  participant Trace as Tracer

  Alert->>Inh: Mutes(lset, now, span)
  Inh->>Index: forEachCandidate(lset, fn)
  Index-->>Inh: candidate rules (deduplicated / linear fallback)
  Inh->>Inh: evaluate TargetMatchers & hasEqual(source, candidate)
  Inh->>Trace: emit per-rule trace event (source fingerprint / rule name)
  alt inhibited
    Inh-->>Alert: return inhibiting fingerprint, set marker & trace
  else not inhibited
    Inh-->>Alert: mark not inhibited, emit not-inhibited trace
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through indexes, quick and sly,
Sniffed duplicates as they scampered by.
Rules lined up, then one took the crown,
I traced the path and settled it down.
Hooray — a nimble hop, and no slowdown! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[PERF] Inhibitor: Add inverted index for O(k) rule lookup instead of O(N) linear scan.' directly describes the main performance optimization introduced in the changeset.
Description check ✅ Passed The PR description addresses the template requirements: documents the new feature with tests, provides benchmark comparisons showing performance improvements, confirms no API/cluster breaking changes, and indicates commits are signed off.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Rules with MatchEqual(label, "") should match alerts that don't have
the label at all, since Go's map lookup returns "" for absent keys.
However, forEachCandidate only iterates over labels present in the
alert's label set, causing such rules to be missed when indexed.

Fix by treating empty-value matchers as non-indexable, so rules with
only such matchers fall back to linearRules where they're always checked.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
@Mwea Mwea force-pushed the perf/invertex-index-inhibit branch from fd42ae2 to ebda3df Compare March 17, 2026 14:48
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
inhibit/rule_index_test.go (1)

223-262: ⚠️ Potential issue | 🟡 Minor

This test still doesn't exercise indexed early termination.

At the default overlap threshold, cluster=prod appears in 3 of 4 rules, so those matching rules are classified as high-overlap and moved to linearRules. The assertion still passes, but it only proves early exit in the fallback path, not through the index.

Suggested fix
-	idx := newRuleIndex(rules)
+	idx := newRuleIndexWithOptions(rules, ruleIndexOptions{
+		minRulesForIndex:       2,
+		maxMatcherOverlapRatio: 1.0,
+	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inhibit/rule_index_test.go` around lines 223 - 262, The test currently puts
`cluster=prod` in 3 of 4 `InhibitRule`s so the index classifies that value as
high-overlap and uses the linear fallback; update the test data in
`TestForEachCandidate_EarlyTermination` so `cluster=prod` appears in fewer rules
(e.g., only 2 rules) to keep overlap low and force the index path. Edit the
`rules` slice (the `TargetMatchers` on the `InhibitRule` entries used to build
`idx := newRuleIndex(rules)`) so only two rules have `cluster: "prod"` and the
others use different values (e.g., "staging" or "qa"); then keep the call to
`idx.forEachCandidate(model.LabelSet{"cluster": "prod"}, ...)` and the same
assertions to validate early termination through the index.
🧹 Nitpick comments (1)
inhibit/rule_index.go (1)

101-158: Deduplicate identical exact matchers per rule before indexing.

A single rule can carry the same name=value matcher more than once, which is easy to hit when deprecated and new target matcher syntax are combined. This code then inflates matcherCount, can classify a matcher as “high overlap” too early, and can append the same rule repeatedly to one bucket. The fallback is safe, but it weakens the fast path for duplicate-heavy configs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inhibit/rule_index.go` around lines 101 - 158, The issue is duplicate
identical exact matchers in a single InhibitRule inflating matcherCount and
causing duplicate inserts; fix by deduplicating per-rule exact matchers: inside
the first pass over rules (where matcherCount and matcherKey are updated) track
a per-rule set of seen matcherKey and only increment matcherCount once per
unique name=value per rule; similarly, in the second pass when computing
indexableCount, deciding singleMatcherRules, and when appending to
idx.exactIndex, iterate only over the unique per-rule matcherKey set so a rule
is counted/inserted once per distinct exact matcher (use the existing
TargetMatchers to build the per-rule seen set of matcherKey before using
matcherCount, indexableCount, singleMatcherRules, and exactIndex).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inhibit/inhibit_bench_test.go`:
- Around line 444-446: The "indexed" benchmark arm isn't forcing index
construction because with numRules == 1 the configured maxMatcherOverlapRatio
(0.5) excludes the only matcher; update the options passed to
newRuleIndexWithOptions so the index actually builds (e.g., increase
maxMatcherOverlapRatio to 1.0 or otherwise relax overlap filtering, or increase
minRulesForIndex/numRules so rules meet indexing thresholds). Target the
ruleIndexOptions struct (fields minRulesForIndex and maxMatcherOverlapRatio)
used when creating idx via newRuleIndexWithOptions and ensure idx.exactIndex is
non-empty in the "indexed" branch so the benchmark compares linear vs. indexed
paths.

In `@inhibit/rule_index.go`:
- Around line 171-206: The current indexed-first walker can return a later rule
before an earlier rule in idx.allRules, breaking config-order determinism;
change the logic in the walker (the block using idx.exactIndex,
idx.singleMatcherRules, getVisitedRules/putVisitedRules, and final
slices.ContainsFunc on idx.linearRules) to collect the first matching rule
according to idx.allRules instead of returning on first match: iterate all
indexed buckets and linearRules as now but when fn(rule) is true, record the
matched rule (and its index/position in idx.allRules) rather than returning;
after scanning everything, choose the matched rule with the smallest position in
idx.allRules (ensuring deduplication still uses visited) and return true only if
such a rule exists, preserving the original order used by checkInhibit().

---

Duplicate comments:
In `@inhibit/rule_index_test.go`:
- Around line 223-262: The test currently puts `cluster=prod` in 3 of 4
`InhibitRule`s so the index classifies that value as high-overlap and uses the
linear fallback; update the test data in `TestForEachCandidate_EarlyTermination`
so `cluster=prod` appears in fewer rules (e.g., only 2 rules) to keep overlap
low and force the index path. Edit the `rules` slice (the `TargetMatchers` on
the `InhibitRule` entries used to build `idx := newRuleIndex(rules)`) so only
two rules have `cluster: "prod"` and the others use different values (e.g.,
"staging" or "qa"); then keep the call to
`idx.forEachCandidate(model.LabelSet{"cluster": "prod"}, ...)` and the same
assertions to validate early termination through the index.

---

Nitpick comments:
In `@inhibit/rule_index.go`:
- Around line 101-158: The issue is duplicate identical exact matchers in a
single InhibitRule inflating matcherCount and causing duplicate inserts; fix by
deduplicating per-rule exact matchers: inside the first pass over rules (where
matcherCount and matcherKey are updated) track a per-rule set of seen matcherKey
and only increment matcherCount once per unique name=value per rule; similarly,
in the second pass when computing indexableCount, deciding singleMatcherRules,
and when appending to idx.exactIndex, iterate only over the unique per-rule
matcherKey set so a rule is counted/inserted once per distinct exact matcher
(use the existing TargetMatchers to build the per-rule seen set of matcherKey
before using matcherCount, indexableCount, singleMatcherRules, and exactIndex).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eeb9ea0f-b267-4b73-8eaa-5ceffb9732fe

📥 Commits

Reviewing files that changed from the base of the PR and between 35cbf8d and ebda3df.

📒 Files selected for processing (4)
  • inhibit/inhibit.go
  • inhibit/inhibit_bench_test.go
  • inhibit/rule_index.go
  • inhibit/rule_index_test.go

Comment on lines +171 to +206
// Fast path: if rule count is small or no index was built, iterate all rules
if idx.useLinearScan || len(idx.exactIndex) == 0 {
return slices.ContainsFunc(idx.allRules, fn)
}

visited := getVisitedRules()
defer putVisitedRules(visited)

for labelName, labelValue := range lset {
valueMap, ok := idx.exactIndex[string(labelName)]
if !ok {
continue
}

rules, ok := valueMap[string(labelValue)]
if !ok {
continue
}

for _, rule := range rules {
// Rules with multiple indexed matchers need deduplication since they
// appear in multiple index entries. Single-matcher rules can skip this.
if _, isSingleMatcher := idx.singleMatcherRules[rule]; !isSingleMatcher {
if _, seen := visited[rule]; seen {
continue
}
visited[rule] = struct{}{}
}

if fn(rule) {
return true
}
}
}

return slices.ContainsFunc(idx.linearRules, fn)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The candidate walk drops config-order priority.

checkInhibit() returns on the first inhibiting rule, but this walker evaluates indexed buckets first and only then falls back to linearRules. That means a later indexed rule can now beat an earlier linear rule, and two indexed rules from different buckets are no longer ordered by ih.rules. The mute verdict stays correct, but the stored inhibiting fingerprint/rule can change. If config-order determinism still matters, this needs a stable rule priority or explicit documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inhibit/rule_index.go` around lines 171 - 206, The current indexed-first
walker can return a later rule before an earlier rule in idx.allRules, breaking
config-order determinism; change the logic in the walker (the block using
idx.exactIndex, idx.singleMatcherRules, getVisitedRules/putVisitedRules, and
final slices.ContainsFunc on idx.linearRules) to collect the first matching rule
according to idx.allRules instead of returning on first match: iterate all
indexed buckets and linearRules as now but when fn(rule) is true, record the
matched rule (and its index/position in idx.allRules) rather than returning;
after scanning everything, choose the matched rule with the smallest position in
idx.allRules (ensuring deduplication still uses visited) and return true only if
such a rule exists, preserving the original order used by checkInhibit().

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inhibit/inhibit_bench_test.go`:
- Around line 318-328: The code is using the wrong package name `config` for
types like InhibitRule and Matchers; replace those occurrences with
`amcommoncfg` (e.g., change config.InhibitRule to amcommoncfg.InhibitRule and
config.Matchers to amcommoncfg.Matchers) in the block that builds `rules`, and
ensure uses of `mustNewMatcher` and `labels.MatchEqual` remain unchanged; also
update the file import to reference the amcommoncfg package instead of config so
the types resolve.
- Around line 365-375: Replace the undefined package identifier "config" with
the correct package "amcommoncfg" in this block: change config.InhibitRule to
amcommoncfg.InhibitRule and config.Matchers to amcommoncfg.Matchers so the types
used for rules, SourceMatchers and TargetMatchers (and their references
alongside mustNewMatcher, labels.MatchEqual, strconv.Itoa) resolve correctly.
- Around line 263-275: The failure is due to using the wrong package identifier:
replace references to config.InhibitRule and config.Matchers with the imported
alias amcommoncfg (e.g., use amcommoncfg.InhibitRule and amcommoncfg.Matchers)
in the loop that builds rules (the block creating rules :=
make([]config.InhibitRule, numRules) and assigning entries that call
mustNewMatcher); ensure the slice type and any type assertions/usages referring
to Matchers/InhibitRule are updated to amcommoncfg to match the import alias.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f405713-020f-4287-b613-e900723070d8

📥 Commits

Reviewing files that changed from the base of the PR and between ebda3df and c8aead0.

📒 Files selected for processing (2)
  • inhibit/inhibit.go
  • inhibit/inhibit_bench_test.go

Signed-off-by: Titouan Chary <titouan.chary@aiven.io>
@Mwea Mwea force-pushed the perf/invertex-index-inhibit branch from c8aead0 to 71cf3ab Compare March 17, 2026 15:12
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inhibit/inhibit_bench_test.go`:
- Around line 444-446: The benchmark's "indexed" case builds a rule index with
ruleIndexOptions{minRulesForIndex: 1, maxMatcherOverlapRatio: 0.5}, which yields
maxOverlap=0 for numRules=1 so all matchers are treated as high-overlap and
exactIndex remains empty; change the options in the b.Run("indexed", ...) setup
(where newRuleIndexWithOptions is called) to ensure an index is actually built
for small rule counts—e.g., set minRulesForIndex to 2 or increase
maxMatcherOverlapRatio (or both) so exactIndex is non-empty; this will make
forEachCandidate (rule_index.go) exercise the indexed path instead of falling
back to linear scan.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b33d8bd-1fed-4cf7-9f1f-9bca77f786cd

📥 Commits

Reviewing files that changed from the base of the PR and between c8aead0 and 71cf3ab.

📒 Files selected for processing (4)
  • inhibit/inhibit.go
  • inhibit/inhibit_bench_test.go
  • inhibit/rule_index.go
  • inhibit/rule_index_test.go
✅ Files skipped from review due to trivial changes (1)
  • inhibit/rule_index.go

Comment on lines +444 to +446
b.Run("indexed", func(b *testing.B) {
opts := ruleIndexOptions{minRulesForIndex: 1, maxMatcherOverlapRatio: 0.5}
idx := newRuleIndexWithOptions(rules, opts)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the ruleIndex construction logic to understand how overlap is calculated
# and whether a single rule with a unique matcher would be indexed or fall back to linear.

# Find the newRuleIndexWithOptions function and overlap calculation logic
ast-grep --pattern $'func newRuleIndexWithOptions($_, $_) $_ {
  $$$
}'

# Also search for overlap ratio calculation
rg -n -A10 'maxMatcherOverlapRatio|overlapRatio' inhibit/rule_index.go

Repository: prometheus/alertmanager

Length of output: 5626


🏁 Script executed:

#!/bin/bash
# Find the benchmark setup to see how many rules are created
# Search for the context around lines 444-446
sed -n '430,460p' inhibit/inhibit_bench_test.go

# Also search for numRules variable and setup
rg -n 'numRules' inhibit/inhibit_bench_test.go

Repository: prometheus/alertmanager

Length of output: 2446


🏁 Script executed:

#!/bin/bash
# Find the forEachCandidate implementation
ast-grep --pattern $'func ($_) forEachCandidate($_, $_) {
  $$$
}'

# Also search for it directly
rg -n 'func.*forEachCandidate' inhibit/rule_index.go -A 30

Repository: prometheus/alertmanager

Length of output: 1090


The "indexed" benchmark doesn't construct an index for numRules=1.

When numRules=1 and maxMatcherOverlapRatio=0.5, the overlap calculation results in maxOverlap=0, which causes all matchers to be marked high-overlap and excluded from indexing. This leaves exactIndex empty, causing forEachCandidate() (line 172 of rule_index.go) to fall back to linear scan: if idx.useLinearScan || len(idx.exactIndex) == 0. The benchmark thus compares linear vs. linear rather than linear vs. indexed for the numRules=1 case.

For larger rule counts (2, 3, 5, 10), the index is properly constructed and tested. Consider increasing minRulesForIndex or adjusting maxMatcherOverlapRatio to ensure the indexed variant actually uses the index even for small rule sets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inhibit/inhibit_bench_test.go` around lines 444 - 446, The benchmark's
"indexed" case builds a rule index with ruleIndexOptions{minRulesForIndex: 1,
maxMatcherOverlapRatio: 0.5}, which yields maxOverlap=0 for numRules=1 so all
matchers are treated as high-overlap and exactIndex remains empty; change the
options in the b.Run("indexed", ...) setup (where newRuleIndexWithOptions is
called) to ensure an index is actually built for small rule counts—e.g., set
minRulesForIndex to 2 or increase maxMatcherOverlapRatio (or both) so exactIndex
is non-empty; this will make forEachCandidate (rule_index.go) exercise the
indexed path instead of falling back to linear scan.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants