Skip to content

Improve MSBuild expansion performance - #14697

Open
DustinCampbell wants to merge 20 commits into
dotnet:mainfrom
DustinCampbell:expansion-perf
Open

Improve MSBuild expansion performance#14697
DustinCampbell wants to merge 20 commits into
dotnet:mainfrom
DustinCampbell:expansion-perf

Conversation

@DustinCampbell

Copy link
Copy Markdown
Member

Important

This depends on #14660. Until that PR merges, this PR includes its benchmark commits; the expansion changes begin after bd61887.

Tip

Each commit is self-contained and has a good description. Consider reviewing commit-by-commit starting at 71d8b82.

Context

Expansion repeatedly scans strings for property, item, and metadata markers and performs parsing, dispatch, and context resolution for every match. These operations are pervasive during project evaluation, so even small per-expression or per-item costs accumulate across large solutions.

This change optimizes those hot paths while preserving existing expansion behavior.

Note

This is the first in a series of expansion performance PRs. It focuses on targeted, behavior-preserving reductions in CPU overhead; allocation-focused work and more fundamental architectural changes are intentionally left for follow-up PRs. There is still substantial low-hanging fruit in these paths.

Changes Made

  • Add specialized marker detection and indexing APIs for $(, @(, and %( expressions, replacing duplicated and broader searches throughout evaluation.
  • Replace the stateful item-expression enumerator with indexed capture parsing and avoid wrapping large item captures in Nullable<T>.
  • Specialize common quoted-transform shapes so literal, exact, embedded-single, and multiple-match expressions avoid general-purpose scanning and builders.
  • Separate property-specific parenthesis scanning from general function argument parsing and classify ordinary properties while scanning.
  • Encapsulate property expansion state in a readonly ref struct and cache invariant expansion policy.
  • Join explicitly separated item vectors directly with ValueStringBuilder on .NET and StringBuilderCache on .NET Framework, avoiding intermediate collections and LINQ.
  • Cache item-spec modifier classification while parsing quoted-transform metadata.
  • Dispatch implicit quoted transforms directly instead of synthesizing an intrinsic function call and argument array.
  • Resolve project-directory and defining-project context only for modifiers that consume those values.

Benchmark Results

The baseline is bd61887f13; the optimized implementation is 34c6229708. Negative timing percentages indicate faster execution. The four benchmark classes were unchanged between these commits.

🚀 Performance Highlights

Area .NET 11 LegacyJIT x86 Allocation impact
🔍 Marker detection (NoExpansion) 30.9–51.1% faster 40.6–56.0% faster ➖ Unchanged
✨ Quoted transform, 100 items 48.3% faster 23.6% faster 📉 -32 B / -17 B
🔗 Explicit separator, 100 items 12.1% faster 28.9% faster 📉 -1,664 B / -844 B
🏷️ Qualified metadata 32.9% faster 19.1% faster ➖ Unchanged
⚙️ Multiple distinct properties 15.8% faster 14.7% faster ➖ Unchanged
🧩 Mixed property, metadata, and 100-item transform 59.3% faster 27.0% faster 📉 -32 B / -16 B
.NET 11.0 — RyuJIT x64

ItemExpansionBenchmark

Method Items Mean: baseline → latest Time Allocated: baseline → latest
NoExpansion 10 26.22 → 13.92 ns -46.9% 0 → 0 B
Simple 10 573.49 → 557.89 ns -2.7% 280 → 280 B
WithQuotedTransform 10 3,029.15 → 1,712.49 ns -43.5% 1,024 → 992 B (-32 B)
WithSeparator 10 600.19 → 530.68 ns -11.6% 768 → 544 B (-29.2%)
WithQuotedTransformAndUnescape 10 3,016.00 → 1,725.81 ns -42.8% 1,024 → 992 B (-32 B)
NoExpansion 100 24.80 → 13.62 ns -45.1% 0 → 0 B
Simple 100 3,923.71 → 3,847.24 ns -1.9% 1,720 → 1,720 B
WithQuotedTransform 100 25,621.73 → 13,239.86 ns -48.3% 7,505 → 7,473 B (-32 B)
WithSeparator 100 3,552.17 → 3,123.38 ns -12.1% 5,624 → 3,960 B (-29.6%)
WithQuotedTransformAndUnescape 100 25,539.33 → 13,135.71 ns -48.6% 7,505 → 7,473 B (-32 B)

MetadataExpansionBenchmark

Method Mean: baseline → latest Time Allocated
NoExpansion 15.81 → 10.92 ns -30.9% 0 → 0 B
Unqualified 167.10 → 116.72 ns -30.1% 0 → 0 B
Qualified 227.22 → 152.43 ns -32.9% 48 → 48 B
Multiple 290.44 → 224.46 ns -22.7% 0 → 0 B
MultipleAndUnescape 307.20 → 224.48 ns -26.9% 0 → 0 B

MixedExpansionBenchmark

Method Mean: baseline → latest Time Allocated: baseline → latest
NoExpansion 38.72 → 18.94 ns -51.1% 0 → 0 B
PropertyAndItem 17,827.80 → 7,470.08 ns -58.1% 7,497 → 7,465 B (-32 B)
PropertyAndMetadata 525.56 → 405.57 ns -22.8% 0 → 0 B
All 19,144.79 → 7,785.14 ns -59.3% 7,665 → 7,633 B (-32 B)
AllAndUnescape 18,446.47 → 7,810.63 ns -57.7% 7,665 → 7,633 B (-32 B)

PropertyExpansionBenchmark

Method Mean: baseline → latest Time Allocated
NoExpansion 16.95 → 11.52 ns -32.0% 0 → 0 B
Single 103.20 → 80.27 ns -22.2% 0 → 0 B
Embedded 209.95 → 205.71 ns -2.0% 40 → 40 B
MultipleDistinct 481.61 → 405.37 ns -15.8% 0 → 0 B
MultipleRepeated 472.70 → 402.12 ns -14.9% 0 → 0 B
Undefined 183.21 → 183.53 ns +0.2% 40 → 40 B
LongValue 88.46 → 73.90 ns -16.5% 0 → 0 B
MultipleAndUnescape 413.11 → 416.51 ns +0.8% 0 → 0 B
.NET Framework 4.8.1 — LegacyJIT x86

ItemExpansionBenchmark

Method Items Mean: baseline → latest Time Allocated: baseline → latest
NoExpansion 10 191.4 → 112.6 ns -41.2% 0 → 0 B
Simple 10 2,793.1 → 2,667.0 ns -4.5% 152 → 152 B
WithQuotedTransform 10 13,727.2 → 10,449.7 ns -23.9% 962 → 946 B (-16 B)
WithSeparator 10 2,246.3 → 1,674.0 ns -25.5% 593 → 469 B (-20.9%)
WithQuotedTransformAndUnescape 10 13,778.2 → 10,467.6 ns -24.0% 962 → 946 B (-16 B)
NoExpansion 100 190.9 → 112.4 ns -41.1% 0 → 0 B
Simple 100 19,269.2 → 18,822.0 ns -2.3% 874 → 874 B
WithQuotedTransform 100 120,620.9 → 92,212.8 ns -23.6% 7,816 → 7,799 B (-17 B)
WithSeparator 100 10,977.8 → 7,810.4 ns -28.9% 9,162 → 8,318 B (-9.2%)
WithQuotedTransformAndUnescape 100 121,049.3 → 91,529.3 ns -24.4% 7,816 → 7,799 B (-17 B)

MetadataExpansionBenchmark

Method Mean: baseline → latest Time Allocated
NoExpansion 172.0 → 102.1 ns -40.6% 0 → 0 B
Unqualified 946.8 → 712.2 ns -24.8% 0 → 0 B
Qualified 1,120.2 → 905.8 ns -19.1% 40 → 40 B
Multiple 1,454.9 → 1,139.5 ns -21.7% 0 → 0 B
MultipleAndUnescape 1,523.0 → 1,170.3 ns -23.2% 0 → 0 B

MixedExpansionBenchmark

Method Mean: baseline → latest Time Allocated: baseline → latest
NoExpansion 343.4 → 151.2 ns -56.0% 0 → 0 B
PropertyAndItem 79,038.1 → 57,716.0 ns -27.0% 7,806 → 7,790 B (-16 B)
PropertyAndMetadata 2,541.3 → 2,103.6 ns -17.2% 0 → 0 B
All 80,833.6 → 58,976.5 ns -27.0% 7,914 → 7,898 B (-16 B)
AllAndUnescape 81,588.6 → 59,358.2 ns -27.2% 7,914 → 7,898 B (-16 B)

PropertyExpansionBenchmark

Method Mean: baseline → latest Time Allocated
NoExpansion 166.0 → 88.49 ns -46.7% 0 → 0 B
Single 459.8 → 333.3 ns -27.5% 0 → 0 B
Embedded 1,037.3 → 879.82 ns -15.2% 28 → 28 B
MultipleDistinct 2,269.6 → 1,935.24 ns -14.7% 0 → 0 B
MultipleRepeated 2,194.5 → 1,921.38 ns -12.4% 0 → 0 B
Undefined 959.3 → 827.33 ns -13.8% 28 → 28 B
LongValue 441.7 → 300.15 ns -32.0% 0 → 0 B
MultipleAndUnescape 2,301.5 → 1,908.8 ns -17.1% 0 → 0 B

Analysis

  • Quoted item transforms improve by 42.8–48.6% on .NET 11 and 23.6–24.4% on LegacyJIT. Savings scale with item count, confirming that per-item modifier classification and context lookup work was removed.
  • Mixed expressions containing a 100-item quoted transform improve by 57.7–59.3% on .NET 11 and approximately 27% on LegacyJIT.
  • Metadata expansion improves by 19.1–32.9% across qualified, unqualified, multiple, and unescaping cases without allocation changes.
  • Property expansion improves by 12.4–32.0% on LegacyJIT. Common .NET 11 property shapes improve by 14.9–22.2%; the small changes to embedded, undefined, and unescaping cases are within benchmark noise.
  • Explicit separator expansion reduces allocations by approximately 29% on .NET 11 and saves 124–844 B on LegacyJIT while improving execution by 11.6–28.9%.
  • The fixed 32 B x64 and approximately 16 B x86 reduction in quoted and mixed item benchmarks matches removal of the synthetic transform argument array.
  • No benchmark case shows a meaningful timing regression or allocation increase.

Testing

  • Full test suite passes.
  • Captured BenchmarkDotNet baselines and optimized results for ItemExpansionBenchmark, MetadataExpansionBenchmark, MixedExpansionBenchmark, and PropertyExpansionBenchmark on .NET 11.0 x64 and .NET Framework 4.8.1 LegacyJIT x86.

Notes

This change intentionally preserves existing expansion semantics and public API behavior. The focused benchmark infrastructure is reviewed separately in #14660.

Replace the monolithic Expander benchmark with targeted property, item,
metadata, mixed-expression, condition, shredder, and lazy-item suites.
Add reusable setup helpers and make cache-state expectations explicit.

Add a cross-TFM PowerShell runner with broad and granular benchmark sets,
storing results under the repository artifacts directory.
Create fresh item instances for each iteration so cold and repeated
modifier lookups measure the intended cache state. Clear the shared
defining-project cache after setup to prevent state leaking between
iterations.

Batch item accesses and normalize results with OperationsPerInvoke.
Move item discovery and materialization out of the measured code.
Separate cached escaping into explicit cache-hit and cache-miss cases.
Prime hit inputs before measurement and generate unique miss inputs for
each iteration so warmup cannot change the measured cache state.

Batch operations to reduce harness overhead while preserving input
length and escaping characteristics.
Expose launch count through the benchmark runner for higher-confidence
comparisons across independent processes. Keep power-plan enforcement
opt-in so dedicated machines retain their configured plan.

Document smoke, exploratory, and confirmation runs, including execution
cost and cross-framework comparison constraints.
Measure fixed property-expansion shapes with 10 and 100 unrelated
properties in the backing bag. Include a no-expansion baseline to
separate lookup effects from fixed benchmark overhead.

Add a dedicated category and runner set while preserving the broader
property-expansion scaling groups.
Resolve explicit relative artifact paths against PowerShell's current
location instead of the process working directory, which may not track
Set-Location. This ensures paths such as .\artifacts are created under
the directory from which Run-Benchmarks.ps1 was invoked.

Document the relative path behavior for the ArtifactsPath parameter.
Document the intentional cold-path and cache-isolation behavior of the
focused evaluation benchmarks.

Use the shared temporary-directory helper for modifier benchmarks to
centralize setup and cleanup.
MSBuild repeatedly searches evaluation strings for the `$(`, `@(`, and
`%(` syntax markers. Existing call sites used culture-aware searches,
ordinal substring searches, broad single-character checks, and
character-by-character loops. This duplicated work and made the common
marker-absent path pay for slower two-character searches.

Add named marker constants and specialized `Contains*Marker` and
`IndexOf*Marker` APIs to `ExpressionShredder`. The index helpers search
for a single-character prefix and check the following `(` directly.
They support both unbounded and count-bounded ranges while leaving full
expression validation to the existing parsers.

Use the helpers throughout property, metadata, and item-vector
expansion; SDK-reference property detection; item provenance; and
intrinsic item metadata handling. This removes the unnecessary
invariant-culture SDK-reference search, replaces broad `Contains('@')`
checks with exact marker detection, and lets expansion loops jump
directly between candidate references.

Refactor `ReferencedItemExpressionsEnumerator` to jump between bounded
`@(` markers while preserving resumable state. Refactor
`GetReferencedItemNamesAndMetadata` to perform one bounded `IndexOfAny`
scan for `@` and `%` instead of scanning the remaining input twice. Its
explicit scan position preserves marker ordering, subrange boundaries,
and malformed-expression recovery without manipulating a `for` loop
counter.

Add focused coverage for marker constants, all marker types, missing
and trailing prefixes, rejected prefix characters, repeated markers,
start indexes, bounded ranges, range boundaries, and containment
checks.
Replace the stateful item expression enumerator with an indexed
TryGetNextItemVectorExpression API.

Update expansion callers, tests, and benchmarks to consume captures
directly and avoid redundant marker scans.
Use the Try pattern with an out parameter when expanding a single item
vector expression. This avoids wrapping the relatively large capture
struct in Nullable<T> and copying it through Nullable<T>.Value.
Specialize literal, exact, embedded-single, and multiple-match transforms so common cases avoid general-purpose scanning and builders.

Jump directly between metadata markers and hoist invariant boundaries while preserving existing expansion and allocation behavior.
Separate property-specific parenthesis scanning from the general function-argument scanner. Classify property and registry function candidates while scanning so ordinary properties go directly to lookup.

Move uncommon compatibility and function handling into ExpandProperty, use string-based marker and quote scans, and replace magic comparison lengths with named constants. This reduces work in common property expansion paths and keeps specialized behavior isolated.
Encapsulate property expansion context in a readonly ref struct so helper methods no longer pass the same state through each call. Cache the truncation policy for reuse during expansion while preserving the existing static entry points.
Avoid materializing intermediate entries and LINQ iterators when joining
item vectors with explicit separators. Use ValueStringBuilder on .NET
and StringBuilderCache on .NET Framework.

Preserve transform classification for empty item vectors while still
evaluating Count and AnyHaveMetadataValue on empty lists.
Resolve item-spec modifier kinds when parsing quoted transform metadata
instead of classifying the same name for every item.

Add a non-caching enum overload of GetItemSpecModifier so quoted
transforms can call the modifier implementation directly.
Dispatch unnamed quoted transforms directly instead of synthesizing an
intrinsic function name and one-element argument array. This avoids
modifier classification and intrinsic dictionary lookup.

Keep explicitly named ExpandQuotedExpressionFunction invocations on the
existing validated argument path.
Resolve project directory and defining-project metadata only for
item-spec modifiers that consume those values.

Common quoted transforms such as Filename now skip both per-item
lookups.

@github-actions github-actions Bot left a comment

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.

No blocking issues found in the snippet reviewed. GetItemSpecModifier already tolerates a null currentDirectory for Filename, Extension, RelativeDir, and Identity, and RecursiveDir is not derivable (TryGetDerivableModifierKind excludes it), so the new switch does not appear to introduce a functional regression. The change is likely performance-positive on this hot path because it avoids eagerly computing project context and DefiningProjectFullPath for modifiers that do not need them.

One concern: I could not find targeted regression coverage for this new dispatch in GetMetadataValueFromMatch, especially for quoted transform expansion of %(Filename), %(RelativeDir), and the %(DefiningProject*) modifiers. Existing tests cover ItemSpecModifiers.GetItemSpecModifier directly and some transform behavior, but not this exact expander-layer routing. A focused table-driven test here would better lock in the intended mapping and guard future additions to derivable modifiers.

Generated by Expert Code Review (on open) for #14697 · sonnet46 · 63.4 AIC · ⌖ 7.62 AIC · ⊞ 5.3K

Copilot AI left a comment

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.

Pull request overview

This pull request optimizes MSBuild’s evaluation-time expansion hot paths (properties, item vectors, metadata, and quoted transforms) to reduce per-expression CPU overhead, and updates/expands the BenchmarkDotNet benchmark suite + runner script to measure these scenarios more directly.

Changes:

  • Refactors expansion marker detection and item-vector parsing (new indexing APIs in ExpressionShredder, new item-vector capture flow in Expander).
  • Optimizes item transform execution paths (quoted transforms dispatch, separator joins, metadata-match classification, and reduced context resolution for item-spec modifiers).
  • Reworks benchmark infrastructure and adds focused benchmark suites plus a cross-TFM runner script and updated documentation.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/MSBuild.Benchmarks/Run-Benchmarks.ps1 Adds a cross-TFM benchmark runner with filter/set selection and common options.
src/MSBuild.Benchmarks/readme.md Documents the new runner, named sets, and run modes.
src/MSBuild.Benchmarks/Program.cs Adds --enforce-power-plan plumbing into the BenchmarkDotNet config overrides.
src/MSBuild.Benchmarks/ItemSpecModifiersCachingBenchmark.cs Updates modifier-caching benchmark setup + per-iteration item creation.
src/MSBuild.Benchmarks/DefiningProjectModifiersBenchmark.cs Updates defining-project modifier benchmarks to use per-iteration setup and new temp-dir infra.
src/MSBuild.Benchmarks/EscapingUtilitiesBenchmark.cs Removes the caching cases (now covered by the dedicated caching benchmark).
src/MSBuild.Benchmarks/EscapingUtilitiesCachingBenchmark.cs Adds cache-hit vs cache-miss escaping benchmarks with per-iteration input generation.
src/MSBuild.Benchmarks/Infrastructure/TemporaryDirectory.cs Adds reusable temp-directory lifetime management for benchmarks.
src/MSBuild.Benchmarks/Infrastructure/BenchmarkProject.cs Adds helper for creating project instances/items for expander benchmarks.
src/MSBuild.Benchmarks/Infrastructure/ExpanderBuilder.cs Adds a builder for constructing expanders with explicit properties/items/metadata.
src/MSBuild.Benchmarks/Infrastructure/ExpanderBenchmarkFixture.cs Adds a fixture to keep expander + project state alive for benchmark lifetime.
src/MSBuild.Benchmarks/ExpanderBenchmark.cs Removes the previous monolithic expander benchmark (replaced by focused suites).
src/MSBuild.Benchmarks/Evaluation/Expansion/PropertyExpansionBenchmark.cs Adds focused property-shape expansion benchmarks.
src/MSBuild.Benchmarks/Evaluation/Expansion/PropertyFunctionExpansionBenchmark.cs Adds focused property-function benchmarks.
src/MSBuild.Benchmarks/Evaluation/Expansion/PropertyExpansionScalingBenchmark.cs Adds scaling benchmarks for property reference counts.
src/MSBuild.Benchmarks/Evaluation/Expansion/PropertyBagCardinalityBenchmark.cs Adds scaling benchmarks for unrelated property-bag cardinality.
src/MSBuild.Benchmarks/Evaluation/Expansion/ItemExpansionBenchmark.cs Adds focused item-vector benchmarks (incl. quoted transforms and separators).
src/MSBuild.Benchmarks/Evaluation/Expansion/ItemFunctionExpansionBenchmark.cs Adds focused item-function/transform benchmarks.
src/MSBuild.Benchmarks/Evaluation/Expansion/MetadataExpansionBenchmark.cs Adds focused metadata expansion benchmarks.
src/MSBuild.Benchmarks/Evaluation/Expansion/MetadataExpansionScalingBenchmark.cs Adds scaling benchmarks for metadata reference counts.
src/MSBuild.Benchmarks/Evaluation/Expansion/MixedExpansionBenchmark.cs Adds focused mixed property/item/metadata expansion benchmarks.
src/MSBuild.Benchmarks/Evaluation/ExpressionShredder/ExpressionShredderBenchmark.cs Adds focused ExpressionShredder throughput benchmarks.
src/MSBuild.Benchmarks/Evaluation/ExpressionShredder/ExpressionShredderAllocationBenchmark.cs Adds opt-in cold-cache allocation diagnostics for ExpressionShredder.
src/MSBuild.Benchmarks/Evaluation/Conditions/ConditionStrings.cs Adds shared condition strings for parsing/evaluation benchmarks.
src/MSBuild.Benchmarks/Evaluation/Conditions/ConditionParsingBenchmark.cs Adds focused condition parsing benchmarks.
src/MSBuild.Benchmarks/Evaluation/Conditions/ConditionEvaluationBenchmark.cs Adds focused end-to-end condition evaluation benchmarks with warmed production cache.
src/MSBuild.Benchmarks/Evaluation/Items/LazyItemEvaluationBenchmark.cs Adds end-to-end evaluation benchmarks for lazy item operations.
src/Framework/ItemSpecModifiers.cs Adds an overload using ItemSpecModifierKind to avoid repeated name classification and enable more direct dispatch.
src/Build/Evaluation/ExpressionShredder.cs Adds specialized marker indexing APIs and a new indexed item-vector parser.
src/Build/Evaluation/Expander.cs Updates item-vector detection API usage and simplifies parenthesis scanning signature usage in function parsing.
src/Build/Evaluation/Expander.Function.cs Updates closing-paren scanning call site after signature change.
src/Build/Evaluation/Expander.PropertyExpander.cs Refactors property expansion into a readonly ref struct and uses new marker indexing + specialized paren scan.
src/Build/Evaluation/Expander.MetadataExpander.cs Uses new marker detection + indexed item-vector scanning to expand metadata in gaps.
src/Build/Evaluation/Expander.ItemExpander.cs Introduces TryExpandSingleItemVectorExpression, refactors item-vector expansion/joining, and optimizes quoted-transform dispatch.
src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs Caches derivable modifier classification/kind per metadata match.
src/Build/Evaluation/Expander.ItemExpander.Transforms.cs Special-cases quoted transform shapes, reduces allocation, and avoids unnecessary context resolution for modifiers.
src/Build/Evaluation/LazyItemEvaluator.cs Switches to the new TryExpandSingleItemVectorExpression API for item reference detection.
src/Build/Evaluation/ItemSpec.cs Switches item-spec parsing to the new TryExpandSingleItemVectorExpression API.
src/Build/Evaluation/Evaluator.cs Uses new property-marker detection API for SDK reference property expansion gating.
src/Build/Definition/Project.cs Uses new property-marker detection API for provenance checks.
src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs Uses new item-vector marker detection API for metadata self-reference logging behavior.
src/Build.UnitTests/Evaluation/Expander_Tests.cs Adds coverage to ensure empty item vectors still report whether a transform was present.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/MSBuild.Benchmarks/EscapingUtilitiesCachingBenchmark.cs
Cover every derivable item-spec modifier through quoted transform
expansion. Verify each modifier reads only the project and defining
project context it requires.
@DustinCampbell

DustinCampbell commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

One concern: I could not find targeted regression coverage for this new dispatch in GetMetadataValueFromMatch, especially for quoted transform expansion of %(Filename), %(RelativeDir), and the %(DefiningProject*) modifiers. Existing tests cover ItemSpecModifiers.GetItemSpecModifier directly and some transform behavior, but not this exact expander-layer routing. A focused table-driven test here would better lock in the intended mapping and guard future additions to derivable modifiers.

Added coverage with d7eaa2f.

@rainersigwald

Copy link
Copy Markdown
Member

Nice! lgtm

@OvesN OvesN self-assigned this Aug 14, 2026
@dotnet dotnet deleted a comment from azure-pipelines Bot Aug 14, 2026
@OvesN

OvesN commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/770759
experimental insertion seems fine

@DustinCampbell

Copy link
Copy Markdown
Member Author

https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/770759 experimental insertion seems fine

It looks like there are wins related to this PR. Speedometer shows four highlights in CPlusPlus scenarios and it looks like all of them are attributable to vcxprojreader.

/// <returns>
/// The zero-based index of the marker, or <c>-1</c> if it is not found.
/// </returns>
public static int IndexOfPropertyMarker(string expression)

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.

nit: Since we already have named constants for the full markers, should we also introduce  PropertyMarkerPrefix ,  ItemVectorMarkerPrefix , and  MetadataMarkerPrefix  for the repeated  $ ,  @ , and  % ?


// PERF NOTE: pre-scanning the string for "%(" is cheaper than a full scan.
if (expression.IndexOf("%(", StringComparison.Ordinal) < 0)
if (!ExpressionShredder.ContainsMetadataMarker(expression))

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.

Could we avoid scanning for the first metadata marker twice?ExpandMetadataLeaveEscaped first calls ContainsMetadataMarker, and ScanAndExpandMetadata later starts again with IndexOfMetadataMarker. Perhaps we could find the first index up front and pass it into the expansion path?

/// The CultureInfo from the invariant culture. Used to avoid allocations for
/// performing IndexOf etc.
/// </summary>
private static readonly CompareInfo s_invariantCompareInfo = CultureInfo.InvariantCulture.CompareInfo;

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 guess we do not need it now at all. Only remaining use of it is
if (s_invariantCompareInfo.IndexOf(_expression, "::", CompareOptions.OrdinalIgnoreCase) > -1)

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.

4 participants