Skip to content

perf(equivalence): avoid LINQ allocation in FindBodyOwner - #97

Merged
samtrion merged 1 commit into
mainfrom
perf/equivalence-find-body-owner
Aug 1, 2026
Merged

perf(equivalence): avoid LINQ allocation in FindBodyOwner#97
samtrion merged 1 commit into
mainfrom
perf/equivalence-find-body-owner

Conversation

@samtrion

@samtrion samtrion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

FindBodyOwner in EquivalenceClassifier used
node.AncestorsAndSelf().FirstOrDefault(predicate) to find the innermost
declaration owning a mutated node's body. This method is reached from
HasThrowOnlyBody, which ClassifyUnreachableCode calls ahead of every
check except the cheap no-op, constant-folding and regex-shorthand checks
in Classify's dispatch chain — so it runs for essentially every real
mutation candidate processed. Each call allocated Roslyn's
AncestorsAndSelf() iterator state machine plus a delegate for the
predicate, on a hot path where neither allocation buys anything.

This change replaces the LINQ chain with a manual
for (var current = node; current is not null; current = current.Parent)
loop using the same pattern-matching checks and an early return. The
traversal order, stopping conditions, and return value are identical for
every input — this is a pure performance fix with no behavior change.

The sibling LINQ chain in ClassifyConstantOnlyContext (around line 1276)
is intentionally left untouched; it is being addressed by a separate,
independent change.

Test coverage added

The existing throw-only-body tests already covered most owner/body-shape
combinations (block method, block accessor, expression-bodied property,
expression-bodied indexer, expression-bodied local function). Two
combinations were still missing and are added now:

  • a local function reached through its block body (as opposed to its
    expression body)
  • an accessor reached through its expression body (as opposed to its
    block body)

Both exercise FindBodyOwner's traversal with the mutation sitting
several ancestors below the eventual owner, confirming the manual loop
stops on the exact same node the old LINQ chain did.

Test plan

  • dotnet test ./Frameshift.slnx — all 29376 tests pass
  • dotnet csharpier format . — working tree clean afterward
  • New tests fail against the old implementation's contract if the
    traversal or stopping condition were wrong (verified by reasoning
    through the owner-kind switch, since the fix is behavior-preserving)

FindBodyOwner used AncestorsAndSelf().FirstOrDefault(predicate) to walk
up to the declaration owning a mutated node's body. Because
ClassifyUnreachableCode calls HasThrowOnlyBody, which calls
FindBodyOwner, ahead of every check except the cheap no-op,
constant-folding and regex shorthand checks, this LINQ chain ran for
essentially every real mutation candidate. Each call allocated the
enumerator state machine produced by AncestorsAndSelf plus a delegate
for the predicate, on a hot path that offers nothing back for the
allocation.

Replace the chain with a manual loop that walks node.Parent and
pattern-matches the same set of declaration kinds, returning as soon
as one matches. The traversal order, stopping conditions and return
value are unchanged for every input; only the allocations are gone.

Add tests pinning FindBodyOwner's behavior through HasThrowOnlyBody
for the two owner/body-shape combinations that were not yet exercised
by the existing throw-only-body tests: a local function reached
through its block body instead of an expression body, and an accessor
reached through its expression body instead of its block body.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • state:ready for merge

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 316b7e7e-4ae2-4364-ae86-6774c6b58460

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.26%. Comparing base (fd0e4ae) to head (48dd9d1).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #97   +/-   ##
=======================================
  Coverage   95.26%   95.26%           
=======================================
  Files          88       88           
  Lines        5850     5852    +2     
  Branches     1273     1273           
=======================================
+ Hits         5573     5575    +2     
  Misses        122      122           
  Partials      155      155           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@samtrion
samtrion merged commit e7cfff2 into main Aug 1, 2026
10 checks passed
@samtrion
samtrion deleted the perf/equivalence-find-body-owner branch August 1, 2026 22:30
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