Skip to content

[perf-improver] perf: fast-path IndexItems when no validator configured#479

Merged
Shazwazza merged 1 commit into
support/3.xfrom
perf-assist/indexitems-fast-path-no-validator-447178102ccbc9e7
Jun 17, 2026
Merged

[perf-improver] perf: fast-path IndexItems when no validator configured#479
Shazwazza merged 1 commit into
support/3.xfrom
perf-assist/indexitems-fast-path-no-validator-447178102ccbc9e7

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🤖 This is an automated PR from Perf Improver, an AI assistant focused on performance.

Goal

Eliminate unnecessary LINQ overhead on the indexing hot path — every bulk IndexItems call currently wraps items in a 3-stage LINQ pipeline even when no validator is configured.

Change

File: src/Examine.Core/BaseIndexProvider.cs

When ValueSetValidator == null (the common, default case — validators are an opt-in feature), the existing code runs every ValueSet through:

values
  .Select(ValidateItem)                         // creates ValueSetValidationResult per item
  .Where(x => x.Status != Failed)               // always passes (status == Valid)
  .Select(x => x.ValueSet)                      // unwraps the struct

ValidateItem short-circuits via ValueSetValidator?.Validate(item) ?? new ValueSetValidationResult(Valid, item) — when the validator is null, it always returns Valid, so the Where always passes and Select always unwraps.

After: when no validator is set, IndexItems passes the IEnumerable<ValueSet> straight through to PerformIndexItems with no wrapping:

if (ValueSetValidator == null)
{
    PerformIndexItems(values, OnIndexOperationComplete);
    return;
}

Performance Evidence

Methodology: Static code-path analysis + allocation counting.

Cost per IndexItems call Before After
LINQ iterator allocations 3 (Select + Where + Select state machines) 0
Per-item struct operations ValidateItem → null-conditional → struct ctor none
Per-item delegate overhead 2 lambdas invoked per item 0

For a batch of N documents:

  • Saved: 3 iterator allocations per batch (fixed cost)
  • Saved: N null-conditional evaluations + N ValueSetValidationResult struct constructions + 2N lambda invocations

This matters most in bulk indexing scenarios (e.g. rebuilding an index), where IndexItems is called with large sequences of ValueSet objects.

Trade-offs

  • Adds a one-time null check per IndexItems call (negligible)
  • Behaviour is identical: with no validator all items have always been indexed as-is

Reproducibility

dotnet build src/Examine.sln --configuration Release
dotnet test src/Examine.Test/Examine.Test.csproj --configuration Release --filter "TestCategory!=Benchmarks" -f net8.0

Test Status

✅ Build clean (0 errors, 0 new warnings). All 147 tests passed (2 skipped as expected).

Generated by Perf Improver · sonnet46 5.5M ·
Comment /perf-assist to run again

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/perf-improver.md@dcdf09723d42ef9b6c75335e4612fd145d4ccdaa

When ValueSetValidator is null (the common case - validator is an optional
opt-in), the current code runs every ValueSet through a 3-stage LINQ
chain: Select(ValidateItem) + Where(status != Failed) + Select(ValueSet).

For each item, ValidateItem short-circuits to
  new ValueSetValidationResult(ValueSetValidationStatus.Valid, item)
because the null-conditional evaluates to null. The Where clause then
always passes (status == Valid), and Select unwraps the struct.

With no validator, this is 100% wasteful overhead:
- 3 iterator state-machine objects allocated per IndexItems call
- 1 ValueSetValidationResult struct created per item (stack, but still
  traverses null-conditional path)

This change adds an early-return when ValueSetValidator == null, passing
the IEnumerable<ValueSet> straight through to PerformIndexItems.
At indexing time — the primary hot path — this saves the LINQ wrapping
overhead for every batch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Shazwazza Shazwazza marked this pull request as ready for review June 17, 2026 22:03
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a fast-path null check in IndexItems so that when no ValueSetValidator is configured — the common default case — the call is forwarded directly to PerformIndexItems, skipping the three-stage LINQ pipeline entirely.

  • Behavioral equivalence is preserved: when the validator is null, ValidateItem always returns ValueSetValidationStatus.Valid, the Where filter always passes, and the final Select just unwraps the same ValueSet — so every item would have reached PerformIndexItems unchanged anyway.
  • Allocation savings are genuine: the three LINQ state-machine allocations (two Select iterators and one Where iterator) and the per-item ValidateItem struct construction are eliminated on every IndexItems call when no validator is set, which compounds in bulk-indexing scenarios.

Confidence Score: 5/5

Safe to merge — the fast path is a strict subset of the existing behaviour and cannot reach PerformIndexItems with items that would have been filtered out.

The optimization is narrowly scoped: it only activates when ValueSetValidator is null, a state in which the original three-stage LINQ chain was always a no-op filter. IndexOptions.Validator is a mutable property, but dynamic mutation of a validator while indexing was never safe in the original code either, so no new exposure is introduced. The validator-configured path is entirely unchanged.

No files require special attention.

Important Files Changed

Filename Overview
src/Examine.Core/BaseIndexProvider.cs Adds a null guard on ValueSetValidator at the top of IndexItems to short-circuit the LINQ pipeline; logic is correct and the validator path is unaffected.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["IndexItems(IEnumerable<ValueSet> values)"] --> B{ValueSetValidator == null?}
    B -- "Yes (fast path)" --> C["PerformIndexItems(values, OnIndexOperationComplete)"]
    B -- "No (validator configured)" --> D["values.Select(ValidateItem)"]
    D --> E[".Where(x => x.Status != Failed)"]
    E --> F[".Select(x => x.ValueSet)"]
    F --> G["PerformIndexItems(filtered, OnIndexOperationComplete)"]
    C --> H[["Abstract PerformIndexItems impl"]]
    G --> H
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["IndexItems(IEnumerable<ValueSet> values)"] --> B{ValueSetValidator == null?}
    B -- "Yes (fast path)" --> C["PerformIndexItems(values, OnIndexOperationComplete)"]
    B -- "No (validator configured)" --> D["values.Select(ValidateItem)"]
    D --> E[".Where(x => x.Status != Failed)"]
    E --> F[".Select(x => x.ValueSet)"]
    F --> G["PerformIndexItems(filtered, OnIndexOperationComplete)"]
    C --> H[["Abstract PerformIndexItems impl"]]
    G --> H
Loading

Reviews (1): Last reviewed commit: "perf: fast-path IndexItems when no valid..." | Re-trigger Greptile

@Shazwazza Shazwazza merged commit 874170a into support/3.x Jun 17, 2026
8 checks passed
@Shazwazza Shazwazza deleted the perf-assist/indexitems-fast-path-no-validator-447178102ccbc9e7 branch June 17, 2026 22:09
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.

1 participant