Skip to content

Replace regex-based metadata expansion with manual scanning - #14116

Merged
AR-May merged 13 commits into
dotnet:mainfrom
DustinCampbell:metadata-expansion-perf
Jun 24, 2026
Merged

Replace regex-based metadata expansion with manual scanning#14116
AR-May merged 13 commits into
dotnet:mainfrom
DustinCampbell:metadata-expansion-perf

Conversation

@DustinCampbell

Copy link
Copy Markdown
Member

Summary

Replaces the regex-based metadata expansion in the Expander with a zero-allocation ref struct scanner that performs manual character parsing. This eliminates the Regex.Replace call and associated MatchEvaluator delegate allocations that occurred on every metadata expression expansion.

Changes

  • Removed Expander.MetadataMatchEvaluator.cs and Expander.RegularExpressions.cs — regex infrastructure is no longer needed
  • Rewrote Expander.MetadataExpander.cs using a ref struct scanner that walks the expression character-by-character, identifying %(...) patterns without allocating
  • Simplified MetadataMatch, MetadataMatchType, and OneOrMultipleMetadataMatches structs
  • Added ExpanderBenchmark.cs with 18 benchmarks covering property, item, metadata, and mixed expansion scenarios

Benchmark Results

.NET 10.0 (PropertyCount=10, ItemCount=10)

Benchmark Baseline After Speedup Alloc Before Alloc After
Metadata_Unqualified 413 ns 124 ns 3.3× 624 B 0 B
Metadata_Qualified 496 ns 208 ns 2.4× 736 B 48 B
Metadata_Multiple 765 ns 240 ns 3.2× 1,248 B 0 B
Mixed_PropertyAndMetadata 1,010 ns 489 ns 2.1× 1,248 B 0 B
Mixed_All 3,108 ns 2,580 ns 1.2× 1,880 B 1,352 B
ItemList_WithTransform 2,918 ns 2,823 ns 1.03× 1,376 B 1,024 B

.NET Framework 4.7.2 (PropertyCount=10, ItemCount=10)

Benchmark Baseline After Speedup Alloc Before Alloc After
Metadata_Unqualified 2,051 ns 814 ns 2.5× 369 B 0 B
Metadata_Qualified 1,847 ns 1,035 ns 1.8× 453 B 40 B
Metadata_Multiple 3,662 ns 1,239 ns 3.0× 741 B 0 B
Mixed_PropertyAndMetadata 4,819 ns 2,303 ns 2.1× 741 B 0 B
Mixed_All 16,015 ns 12,282 ns 1.3× 1,494 B 1,170 B
ItemList_WithTransform 13,503 ns 12,834 ns 1.05× 1,186 B 962 B
.NET 10.0 Baseline (before changes)
Method PropertyCount ItemCount Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
Property_Single 10 10 130.98 ns 0.275 ns 0.257 ns 2.73 0.01 - - NA
Property_Multiple 10 10 423.68 ns 0.551 ns 0.488 ns 8.83 0.01 - - NA
Property_Nested 10 10 319.84 ns 0.432 ns 0.360 ns 6.67 0.01 - - NA
Property_Concatenation 10 10 492.58 ns 1.423 ns 1.331 ns 10.27 0.03 0.0019 40 B NA
ItemList_Simple 10 10 997.20 ns 2.848 ns 2.664 ns 20.79 0.06 0.0153 280 B NA
ItemList_WithTransform 10 10 2,917.91 ns 7.731 ns 7.232 ns 60.84 0.16 0.0801 1376 B NA
ItemList_WithSeparator 10 10 738.21 ns 2.974 ns 2.636 ns 15.40 0.06 0.0458 768 B NA
Metadata_Unqualified 10 10 413.46 ns 5.774 ns 5.401 ns 8.63 0.12 0.0372 624 B NA
Metadata_Qualified 10 10 496.08 ns 5.581 ns 5.221 ns 10.35 0.12 0.0439 736 B NA
Metadata_Multiple 10 10 764.50 ns 5.600 ns 4.965 ns 15.94 0.11 0.0744 1248 B NA
Mixed_PropertyAndItem 10 10 1,968.60 ns 4.641 ns 4.114 ns 41.04 0.10 0.0572 1016 B NA
Mixed_PropertyAndMetadata 10 10 1,009.99 ns 6.146 ns 5.749 ns 21.07 0.11 0.0744 1248 B NA
Mixed_All 10 10 3,108.21 ns 14.037 ns 13.130 ns 64.77 0.28 0.1106 1880 B NA
NoExpansion 10 10 47.96 ns 0.038 ns 0.032 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 10 10 423.10 ns 0.605 ns 0.506 ns 8.82 0.01 - - NA
ItemListAndUnescape_WithTransform 10 10 2,856.03 ns 9.655 ns 9.032 ns 59.55 0.21 0.0801 1376 B NA
MetadataAndUnescape_Multiple 10 10 789.42 ns 7.125 ns 6.665 ns 16.46 0.15 0.0744 1248 B NA
MixedAndUnescape_All 10 10 3,048.62 ns 9.876 ns 9.238 ns 63.61 0.20 0.1106 1880 B NA
Property_Single 10 100 139.11 ns 0.147 ns 0.130 ns 3.03 0.01 - - NA
Property_Multiple 10 100 439.69 ns 0.444 ns 0.393 ns 9.57 0.02 - - NA
Property_Nested 10 100 303.68 ns 0.685 ns 0.607 ns 6.61 0.02 - - NA
Property_Concatenation 10 100 615.62 ns 1.369 ns 1.281 ns 13.40 0.03 0.0019 40 B NA
ItemList_Simple 10 100 5,554.81 ns 7.731 ns 6.456 ns 120.86 0.27 0.0992 1720 B NA
ItemList_WithTransform 10 100 20,013.65 ns 63.738 ns 56.502 ns 435.48 1.19 0.4578 7857 B NA
ItemList_WithSeparator 10 100 4,297.85 ns 50.904 ns 47.615 ns 93.52 1.00 0.3357 5624 B NA
Metadata_Unqualified 10 100 397.81 ns 2.257 ns 2.111 ns 8.66 0.05 0.0372 624 B NA
Metadata_Qualified 10 100 510.99 ns 6.103 ns 5.709 ns 11.12 0.13 0.0439 736 B NA
Metadata_Multiple 10 100 791.16 ns 6.240 ns 5.837 ns 17.22 0.12 0.0744 1248 B NA
Mixed_PropertyAndItem 10 100 13,870.06 ns 25.805 ns 24.138 ns 301.82 0.65 0.4425 7497 B NA
Mixed_PropertyAndMetadata 10 100 1,032.05 ns 6.635 ns 5.881 ns 22.46 0.13 0.0744 1248 B NA
Mixed_All 10 100 15,326.58 ns 59.059 ns 55.244 ns 333.51 1.16 0.4883 8361 B NA
NoExpansion 10 100 45.95 ns 0.074 ns 0.070 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 10 100 423.40 ns 1.032 ns 0.915 ns 9.21 0.03 - - NA
ItemListAndUnescape_WithTransform 10 100 19,798.50 ns 51.048 ns 47.750 ns 430.82 0.96 0.4578 7857 B NA
MetadataAndUnescape_Multiple 10 100 804.85 ns 5.537 ns 5.179 ns 17.51 0.11 0.0744 1248 B NA
MixedAndUnescape_All 10 100 15,391.75 ns 30.076 ns 26.661 ns 334.91 0.88 0.4883 8361 B NA
Property_Single 100 10 117.50 ns 0.187 ns 0.166 ns 2.41 0.00 - - NA
Property_Multiple 100 10 409.40 ns 0.376 ns 0.314 ns 8.41 0.02 - - NA
Property_Nested 100 10 307.79 ns 0.414 ns 0.387 ns 6.32 0.01 - - NA
Property_Concatenation 100 10 481.39 ns 0.881 ns 0.736 ns 9.89 0.03 0.0019 40 B NA
ItemList_Simple 100 10 1,011.21 ns 4.075 ns 3.812 ns 20.77 0.09 0.0153 280 B NA
ItemList_WithTransform 100 10 2,902.73 ns 7.269 ns 6.799 ns 59.61 0.17 0.0801 1376 B NA
ItemList_WithSeparator 100 10 752.80 ns 6.447 ns 6.030 ns 15.46 0.12 0.0458 768 B NA
Metadata_Unqualified 100 10 408.72 ns 4.095 ns 3.830 ns 8.39 0.08 0.0372 624 B NA
Metadata_Qualified 100 10 509.34 ns 3.895 ns 3.453 ns 10.46 0.06 0.0439 736 B NA
Metadata_Multiple 100 10 870.89 ns 3.096 ns 2.745 ns 17.88 0.07 0.0744 1248 B NA
Mixed_PropertyAndItem 100 10 1,994.54 ns 17.956 ns 16.796 ns 40.96 0.36 0.0572 1016 B NA
Mixed_PropertyAndMetadata 100 10 1,056.35 ns 7.394 ns 6.916 ns 21.69 0.15 0.0744 1248 B NA
Mixed_All 100 10 3,111.84 ns 12.118 ns 11.335 ns 63.91 0.24 0.1106 1880 B NA
NoExpansion 100 10 48.69 ns 0.078 ns 0.073 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 100 10 413.89 ns 0.628 ns 0.557 ns 8.50 0.02 - - NA
ItemListAndUnescape_WithTransform 100 10 2,844.84 ns 8.383 ns 7.841 ns 58.43 0.21 0.0801 1376 B NA
MetadataAndUnescape_Multiple 100 10 777.41 ns 13.765 ns 12.202 ns 15.96 0.27 0.0744 1248 B NA
MixedAndUnescape_All 100 10 3,107.62 ns 7.542 ns 7.055 ns 63.82 0.15 0.1106 1880 B NA
Property_Single 100 100 124.15 ns 0.141 ns 0.132 ns 2.69 0.00 - - NA
Property_Multiple 100 100 425.65 ns 0.369 ns 0.346 ns 9.23 0.01 - - NA
Property_Nested 100 100 327.94 ns 0.579 ns 0.541 ns 7.11 0.01 - - NA
Property_Concatenation 100 100 506.00 ns 0.876 ns 0.731 ns 10.98 0.02 0.0019 40 B NA
ItemList_Simple 100 100 5,652.04 ns 14.408 ns 13.477 ns 122.62 0.32 0.0992 1720 B NA
ItemList_WithTransform 100 100 20,064.34 ns 73.123 ns 68.399 ns 435.30 1.52 0.4578 7857 B NA
ItemList_WithSeparator 100 100 4,319.37 ns 50.762 ns 47.483 ns 93.71 1.06 0.3357 5624 B NA
Metadata_Unqualified 100 100 429.14 ns 2.539 ns 2.375 ns 9.31 0.05 0.0372 624 B NA
Metadata_Qualified 100 100 518.28 ns 4.242 ns 3.968 ns 11.24 0.08 0.0439 736 B NA
Metadata_Multiple 100 100 761.81 ns 3.667 ns 3.430 ns 16.53 0.08 0.0744 1248 B NA
Mixed_PropertyAndItem 100 100 13,770.44 ns 30.492 ns 28.523 ns 298.75 0.57 0.4425 7497 B NA
Mixed_PropertyAndMetadata 100 100 1,040.45 ns 5.724 ns 5.074 ns 22.57 0.12 0.0744 1248 B NA
Mixed_All 100 100 14,727.39 ns 40.882 ns 36.241 ns 319.50 0.79 0.4883 8361 B NA
NoExpansion 100 100 46.09 ns 0.045 ns 0.042 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 100 100 415.13 ns 0.620 ns 0.550 ns 9.01 0.02 - - NA
ItemListAndUnescape_WithTransform 100 100 19,807.96 ns 46.244 ns 40.994 ns 429.72 0.80 0.4578 7857 B NA
MetadataAndUnescape_Multiple 100 100 764.30 ns 8.365 ns 7.824 ns 16.58 0.17 0.0744 1248 B NA
MixedAndUnescape_All 100 100 15,170.88 ns 145.082 ns 135.710 ns 329.14 2.94 0.4883 8361 B NA
.NET 10.0 (after changes)
Method PropertyCount ItemCount Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
Property_Single 10 10 124.34 ns 0.974 ns 0.813 ns 2.90 0.02 - - NA
Property_Multiple 10 10 414.88 ns 0.534 ns 0.499 ns 9.66 0.02 - - NA
Property_Nested 10 10 313.44 ns 0.313 ns 0.293 ns 7.30 0.02 - - NA
Property_Concatenation 10 10 498.45 ns 0.396 ns 0.331 ns 11.61 0.03 0.0019 40 B NA
ItemList_Simple 10 10 1,071.81 ns 5.265 ns 4.397 ns 24.96 0.12 0.0153 280 B NA
ItemList_WithTransform 10 10 2,822.97 ns 5.025 ns 4.700 ns 65.74 0.15 0.0610 1024 B NA
ItemList_WithSeparator 10 10 754.58 ns 3.282 ns 3.070 ns 17.58 0.07 0.0458 768 B NA
Metadata_Unqualified 10 10 123.65 ns 0.502 ns 0.445 ns 2.88 0.01 - - NA
Metadata_Qualified 10 10 208.18 ns 0.402 ns 0.376 ns 4.85 0.01 0.0029 48 B NA
Metadata_Multiple 10 10 239.93 ns 0.155 ns 0.137 ns 5.59 0.01 - - NA
Mixed_PropertyAndItem 10 10 1,995.66 ns 5.939 ns 5.265 ns 46.48 0.20 0.0572 1016 B NA
Mixed_PropertyAndMetadata 10 10 488.77 ns 1.876 ns 1.663 ns 11.38 0.04 - - NA
Mixed_All 10 10 2,580.41 ns 9.309 ns 8.708 ns 60.11 0.25 0.0801 1352 B NA
NoExpansion 10 10 42.94 ns 0.102 ns 0.090 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 10 10 431.96 ns 1.475 ns 1.380 ns 10.06 0.05 - - NA
ItemListAndUnescape_WithTransform 10 10 2,677.23 ns 6.504 ns 5.766 ns 62.35 0.16 0.0610 1024 B NA
MetadataAndUnescape_Multiple 10 10 259.95 ns 0.466 ns 0.413 ns 6.05 0.02 - - NA
MixedAndUnescape_All 10 10 2,534.57 ns 7.239 ns 6.417 ns 59.03 0.18 0.0801 1352 B NA
Property_Single 10 100 123.49 ns 0.337 ns 0.299 ns 3.02 0.01 - - NA
Property_Multiple 10 100 424.91 ns 0.881 ns 0.824 ns 10.40 0.02 - - NA
Property_Nested 10 100 310.78 ns 0.960 ns 0.898 ns 7.61 0.02 - - NA
Property_Concatenation 10 100 482.49 ns 1.242 ns 1.037 ns 11.81 0.03 0.0019 40 B NA
ItemList_Simple 10 100 5,637.35 ns 12.281 ns 11.488 ns 137.98 0.36 0.0992 1720 B NA
ItemList_WithTransform 10 100 20,394.70 ns 43.186 ns 40.397 ns 499.09 1.10 0.4272 7505 B NA
ItemList_WithSeparator 10 100 4,257.52 ns 26.233 ns 24.539 ns 104.20 0.64 0.3357 5624 B NA
Metadata_Unqualified 10 100 129.49 ns 0.849 ns 0.794 ns 3.17 0.02 - - NA
Metadata_Qualified 10 100 204.93 ns 0.658 ns 0.549 ns 5.02 0.01 0.0029 48 B NA
Metadata_Multiple 10 100 249.55 ns 0.498 ns 0.441 ns 6.11 0.01 - - NA
Mixed_PropertyAndItem 10 100 13,819.14 ns 34.942 ns 32.685 ns 338.29 1.00 0.4425 7497 B NA
Mixed_PropertyAndMetadata 10 100 471.89 ns 0.792 ns 0.702 ns 11.55 0.02 - - NA
Mixed_All 10 100 14,365.34 ns 68.962 ns 61.133 ns 351.62 1.51 0.4578 7833 B NA
NoExpansion 10 100 40.85 ns 0.045 ns 0.040 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 10 100 432.32 ns 0.341 ns 0.302 ns 10.58 0.01 - - NA
ItemListAndUnescape_WithTransform 10 100 19,739.02 ns 101.838 ns 85.039 ns 483.10 2.21 0.4272 7505 B NA
MetadataAndUnescape_Multiple 10 100 257.78 ns 0.392 ns 0.347 ns 6.31 0.01 - - NA
MixedAndUnescape_All 10 100 15,015.87 ns 126.842 ns 112.442 ns 367.54 2.66 0.4578 7833 B NA
Property_Single 100 10 116.61 ns 0.090 ns 0.079 ns 2.68 0.00 - - NA
Property_Multiple 100 10 408.72 ns 0.454 ns 0.379 ns 9.39 0.01 - - NA
Property_Nested 100 10 301.06 ns 0.242 ns 0.214 ns 6.92 0.01 - - NA
Property_Concatenation 100 10 504.98 ns 0.771 ns 0.684 ns 11.60 0.03 0.0019 40 B NA
ItemList_Simple 100 10 1,048.81 ns 3.220 ns 3.012 ns 24.09 0.08 0.0153 280 B NA
ItemList_WithTransform 100 10 2,684.65 ns 4.002 ns 3.547 ns 61.67 0.14 0.0610 1024 B NA
ItemList_WithSeparator 100 10 756.66 ns 3.902 ns 3.459 ns 17.38 0.08 0.0458 768 B NA
Metadata_Unqualified 100 10 123.83 ns 0.359 ns 0.318 ns 2.84 0.01 - - NA
Metadata_Qualified 100 10 209.00 ns 0.193 ns 0.162 ns 4.80 0.01 0.0029 48 B NA
Metadata_Multiple 100 10 242.31 ns 0.328 ns 0.307 ns 5.57 0.01 - - NA
Mixed_PropertyAndItem 100 10 1,993.14 ns 7.872 ns 6.574 ns 45.79 0.17 0.0572 1016 B NA
Mixed_PropertyAndMetadata 100 10 509.55 ns 1.098 ns 1.027 ns 11.70 0.04 - - NA
Mixed_All 100 10 2,635.95 ns 10.728 ns 10.035 ns 60.55 0.25 0.0801 1352 B NA
NoExpansion 100 10 43.53 ns 0.061 ns 0.057 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 100 10 410.32 ns 0.288 ns 0.241 ns 9.43 0.01 - - NA
ItemListAndUnescape_WithTransform 100 10 2,662.43 ns 5.198 ns 4.341 ns 61.16 0.14 0.0610 1024 B NA
MetadataAndUnescape_Multiple 100 10 250.08 ns 0.328 ns 0.307 ns 5.74 0.01 - - NA
MixedAndUnescape_All 100 10 2,606.68 ns 8.508 ns 7.958 ns 59.88 0.21 0.0801 1352 B NA
Property_Single 100 100 122.37 ns 0.151 ns 0.134 ns 2.93 0.01 - - NA
Property_Multiple 100 100 408.56 ns 0.369 ns 0.328 ns 9.79 0.01 - - NA
Property_Nested 100 100 312.80 ns 0.519 ns 0.486 ns 7.50 0.01 - - NA
Property_Concatenation 100 100 466.24 ns 0.454 ns 0.403 ns 11.18 0.01 0.0024 40 B NA
ItemList_Simple 100 100 5,605.19 ns 14.293 ns 13.369 ns 134.38 0.37 0.0992 1720 B NA
ItemList_WithTransform 100 100 20,586.52 ns 43.470 ns 38.535 ns 493.51 1.13 0.4272 7505 B NA
ItemList_WithSeparator 100 100 4,314.38 ns 20.992 ns 18.609 ns 103.43 0.47 0.3357 5624 B NA
Metadata_Unqualified 100 100 120.59 ns 0.106 ns 0.088 ns 2.89 0.00 - - NA
Metadata_Qualified 100 100 210.77 ns 0.279 ns 0.261 ns 5.05 0.01 0.0029 48 B NA
Metadata_Multiple 100 100 238.57 ns 0.204 ns 0.181 ns 5.72 0.01 - - NA
Mixed_PropertyAndItem 100 100 14,010.69 ns 38.157 ns 35.692 ns 335.89 1.05 0.4425 7497 B NA
Mixed_PropertyAndMetadata 100 100 479.25 ns 0.709 ns 0.629 ns 11.49 0.02 - - NA
Mixed_All 100 100 14,438.36 ns 60.947 ns 57.010 ns 346.14 1.32 0.4578 7833 B NA
NoExpansion 100 100 41.71 ns 0.048 ns 0.045 ns 1.00 0.00 - - NA
PropertyAndUnescape_Multiple 100 100 412.33 ns 0.414 ns 0.367 ns 9.88 0.01 - - NA
ItemListAndUnescape_WithTransform 100 100 20,168.04 ns 38.600 ns 32.232 ns 483.54 0.80 0.4272 7505 B NA
MetadataAndUnescape_Multiple 100 100 244.83 ns 0.479 ns 0.424 ns 5.87 0.01 - - NA
MixedAndUnescape_All 100 100 14,591.08 ns 40.316 ns 37.711 ns 349.81 0.97 0.4578 7833 B NA
.NET Framework 4.7.2 Baseline (before changes)
Method PropertyCount ItemCount Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Property_Single 10 10 517.3 ns 1.39 ns 1.30 ns 1.36 0.01 - - - NA
Property_Multiple 10 10 1,830.9 ns 3.31 ns 3.10 ns 4.80 0.02 - - - NA
Property_Nested 10 10 1,297.8 ns 2.50 ns 2.34 ns 3.40 0.01 - - - NA
Property_Concatenation 10 10 1,994.8 ns 2.75 ns 2.44 ns 5.23 0.02 0.0038 - 28 B NA
ItemList_Simple 10 10 2,802.3 ns 5.45 ns 4.83 ns 7.34 0.03 0.0267 - 152 B NA
ItemList_WithTransform 10 10 13,503.0 ns 17.63 ns 16.49 ns 35.39 0.11 0.2136 - 1186 B NA
ItemList_WithSeparator 10 10 2,195.0 ns 7.66 ns 7.17 ns 5.75 0.03 0.1106 - 593 B NA
Metadata_Unqualified 10 10 2,051.3 ns 6.27 ns 5.56 ns 5.38 0.02 0.0687 - 369 B NA
Metadata_Qualified 10 10 1,847.0 ns 5.00 ns 4.43 ns 4.84 0.02 0.0858 - 453 B NA
Metadata_Multiple 10 10 3,661.5 ns 14.93 ns 13.97 ns 9.59 0.04 0.1411 - 741 B NA
Mixed_PropertyAndItem 10 10 9,906.0 ns 24.44 ns 22.86 ns 25.96 0.10 0.1678 - 954 B NA
Mixed_PropertyAndMetadata 10 10 4,819.4 ns 10.91 ns 10.20 ns 12.63 0.04 0.1373 - 741 B NA
Mixed_All 10 10 16,014.6 ns 85.04 ns 79.55 ns 41.96 0.27 0.2747 - 1494 B NA
NoExpansion 10 10 381.6 ns 1.27 ns 1.12 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 10 10 1,822.8 ns 3.55 ns 3.14 ns 4.78 0.02 - - - NA
ItemListAndUnescape_WithTransform 10 10 13,870.9 ns 10.20 ns 9.04 ns 36.35 0.11 0.2136 - 1186 B NA
MetadataAndUnescape_Multiple 10 10 3,688.4 ns 8.75 ns 7.76 ns 9.67 0.04 0.1411 - 741 B NA
MixedAndUnescape_All 10 10 15,915.3 ns 49.95 ns 46.73 ns 41.72 0.19 0.2747 - 1494 B NA
Property_Single 10 100 524.0 ns 0.86 ns 0.76 ns 1.31 0.00 - - - NA
Property_Multiple 10 100 1,764.5 ns 2.78 ns 2.46 ns 4.40 0.01 - - - NA
Property_Nested 10 100 1,313.3 ns 2.01 ns 1.88 ns 3.27 0.01 - - - NA
Property_Concatenation 10 100 2,058.0 ns 1.98 ns 1.76 ns 5.13 0.01 0.0038 - 28 B NA
ItemList_Simple 10 100 19,465.9 ns 27.03 ns 22.57 ns 48.51 0.11 0.1526 - 874 B NA
ItemList_WithTransform 10 100 108,475.2 ns 109.36 ns 96.95 ns 270.37 0.60 1.4648 - 8040 B NA
ItemList_WithSeparator 10 100 10,791.8 ns 44.90 ns 39.80 ns 26.90 0.10 1.7395 0.0305 9162 B NA
Metadata_Unqualified 10 100 2,030.2 ns 7.42 ns 6.57 ns 5.06 0.02 0.0687 - 369 B NA
Metadata_Qualified 10 100 1,803.5 ns 6.41 ns 6.00 ns 4.49 0.02 0.0858 - 453 B NA
Metadata_Multiple 10 100 3,679.5 ns 7.44 ns 6.21 ns 9.17 0.02 0.1411 - 741 B NA
Mixed_PropertyAndItem 10 100 73,272.7 ns 123.33 ns 115.36 ns 182.66 0.41 1.4648 - 7806 B NA
Mixed_PropertyAndMetadata 10 100 4,756.1 ns 13.57 ns 11.33 ns 11.85 0.04 0.1373 - 741 B NA
Mixed_All 10 100 80,697.1 ns 199.66 ns 186.77 ns 201.13 0.69 1.5869 - 8347 B NA
NoExpansion 10 100 401.2 ns 0.87 ns 0.77 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 10 100 1,863.1 ns 2.16 ns 2.02 ns 4.64 0.01 - - - NA
ItemListAndUnescape_WithTransform 10 100 109,868.9 ns 89.80 ns 79.60 ns 273.84 0.64 1.4648 - 8040 B NA
MetadataAndUnescape_Multiple 10 100 3,713.7 ns 11.78 ns 10.45 ns 9.26 0.04 0.1411 - 741 B NA
MixedAndUnescape_All 10 100 79,224.1 ns 171.90 ns 160.79 ns 197.44 0.57 1.5869 - 8347 B NA
Property_Single 100 10 475.3 ns 1.62 ns 1.52 ns 1.14 0.00 - - - NA
Property_Multiple 100 10 1,847.0 ns 12.50 ns 11.69 ns 4.41 0.03 - - - NA
Property_Nested 100 10 1,323.3 ns 5.85 ns 5.47 ns 3.16 0.01 - - - NA
Property_Concatenation 100 10 1,984.2 ns 2.65 ns 2.35 ns 4.74 0.01 0.0038 - 28 B NA
ItemList_Simple 100 10 2,831.1 ns 8.30 ns 7.36 ns 6.76 0.01 0.0267 - 152 B NA
ItemList_WithTransform 100 10 13,579.8 ns 20.64 ns 19.31 ns 32.44 0.05 0.2136 - 1186 B NA
ItemList_WithSeparator 100 10 2,200.6 ns 11.09 ns 10.37 ns 5.26 0.03 0.1106 - 593 B NA
Metadata_Unqualified 100 10 2,045.4 ns 9.43 ns 8.82 ns 4.89 0.02 0.0687 - 369 B NA
Metadata_Qualified 100 10 1,837.9 ns 2.97 ns 2.78 ns 4.39 0.01 0.0858 - 453 B NA
Metadata_Multiple 100 10 3,731.1 ns 12.22 ns 10.83 ns 8.91 0.03 0.1411 - 741 B NA
Mixed_PropertyAndItem 100 10 9,830.4 ns 11.09 ns 10.37 ns 23.49 0.04 0.1678 - 954 B NA
Mixed_PropertyAndMetadata 100 10 4,738.0 ns 15.02 ns 14.05 ns 11.32 0.04 0.1373 - 741 B NA
Mixed_All 100 10 15,860.6 ns 101.16 ns 94.63 ns 37.89 0.23 0.2747 - 1494 B NA
NoExpansion 100 10 418.6 ns 0.58 ns 0.51 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 100 10 1,716.3 ns 2.59 ns 2.42 ns 4.10 0.01 - - - NA
ItemListAndUnescape_WithTransform 100 10 13,757.1 ns 17.09 ns 15.15 ns 32.86 0.05 0.2136 - 1186 B NA
MetadataAndUnescape_Multiple 100 10 3,673.5 ns 16.77 ns 15.68 ns 8.78 0.04 0.1411 - 741 B NA
MixedAndUnescape_All 100 10 16,036.5 ns 91.64 ns 85.72 ns 38.29 0.22 0.2747 - 1494 B NA
Property_Single 100 100 505.9 ns 1.64 ns 1.53 ns 1.29 0.00 - - - NA
Property_Multiple 100 100 1,841.7 ns 3.50 ns 3.28 ns 4.71 0.01 - - - NA
Property_Nested 100 100 1,274.6 ns 2.80 ns 2.34 ns 3.26 0.01 - - - NA
Property_Concatenation 100 100 2,066.2 ns 2.68 ns 2.38 ns 5.28 0.01 0.0038 - 28 B NA
ItemList_Simple 100 100 19,683.2 ns 95.50 ns 89.33 ns 50.31 0.19 0.1526 - 874 B NA
ItemList_WithTransform 100 100 109,411.0 ns 110.15 ns 91.98 ns 279.65 0.46 1.4648 - 8040 B NA
ItemList_WithSeparator 100 100 10,842.9 ns 25.85 ns 22.92 ns 27.71 0.05 1.7395 0.0305 9162 B NA
Metadata_Unqualified 100 100 2,071.2 ns 7.69 ns 7.19 ns 5.29 0.02 0.0687 - 369 B NA
Metadata_Qualified 100 100 1,836.9 ns 3.43 ns 2.86 ns 4.70 0.01 0.0858 - 453 B NA
Metadata_Multiple 100 100 3,709.8 ns 23.46 ns 21.94 ns 9.47 0.05 0.1411 - 741 B NA
Mixed_PropertyAndItem 100 100 72,283.2 ns 121.62 ns 113.77 ns 184.71 0.39 1.4648 - 7806 B NA
Mixed_PropertyAndMetadata 100 100 4,800.7 ns 11.54 ns 10.79 ns 12.28 0.03 0.1373 - 741 B NA
Mixed_All 100 100 78,998.2 ns 149.92 ns 140.23 ns 201.94 0.43 1.5869 - 8347 B NA
NoExpansion 100 100 391.3 ns 0.54 ns 0.42 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 100 100 1,702.7 ns 2.66 ns 2.49 ns 4.35 0.01 - - - NA
ItemListAndUnescape_WithTransform 100 100 110,106.0 ns 167.29 ns 156.48 ns 281.45 0.46 1.4648 - 8040 B NA
MetadataAndUnescape_Multiple 100 100 3,694.7 ns 20.10 ns 16.79 ns 9.44 0.04 0.1411 - 741 B NA
MixedAndUnescape_All 100 100 79,275.3 ns 138.66 ns 129.70 ns 202.56 0.43 1.5869 - 8347 B NA
.NET Framework 4.7.2 (after changes)
Method PropertyCount ItemCount Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Property_Single 10 10 513.0 ns 1.60 ns 1.50 ns 1.36 0.00 - - - NA
Property_Multiple 10 10 1,827.2 ns 5.72 ns 5.35 ns 4.83 0.01 - - - NA
Property_Nested 10 10 1,286.4 ns 6.58 ns 6.15 ns 3.40 0.02 - - - NA
Property_Concatenation 10 10 2,032.8 ns 2.62 ns 2.32 ns 5.37 0.01 0.0038 - 28 B NA
ItemList_Simple 10 10 2,776.7 ns 4.29 ns 3.80 ns 7.34 0.02 0.0267 - 152 B NA
ItemList_WithTransform 10 10 12,833.9 ns 13.45 ns 12.58 ns 33.93 0.06 0.1831 - 962 B NA
ItemList_WithSeparator 10 10 2,195.2 ns 8.23 ns 7.70 ns 5.81 0.02 0.1106 - 593 B NA
Metadata_Unqualified 10 10 814.4 ns 2.89 ns 2.70 ns 2.15 0.01 - - - NA
Metadata_Qualified 10 10 1,034.7 ns 4.00 ns 3.54 ns 2.74 0.01 0.0076 - 40 B NA
Metadata_Multiple 10 10 1,238.8 ns 2.76 ns 2.58 ns 3.28 0.01 - - - NA
Mixed_PropertyAndItem 10 10 9,797.2 ns 26.78 ns 25.05 ns 25.91 0.07 0.1678 - 954 B NA
Mixed_PropertyAndMetadata 10 10 2,302.8 ns 4.33 ns 3.61 ns 6.09 0.01 - - - NA
Mixed_All 10 10 12,282.4 ns 12.91 ns 11.45 ns 32.47 0.07 0.2136 - 1170 B NA
NoExpansion 10 10 378.2 ns 0.68 ns 0.60 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 10 10 1,826.4 ns 3.30 ns 3.09 ns 4.83 0.01 - - - NA
ItemListAndUnescape_WithTransform 10 10 12,832.7 ns 21.78 ns 19.30 ns 33.93 0.07 0.1831 - 962 B NA
MetadataAndUnescape_Multiple 10 10 1,258.7 ns 4.24 ns 3.97 ns 3.33 0.01 - - - NA
MixedAndUnescape_All 10 10 12,285.8 ns 24.43 ns 20.40 ns 32.48 0.07 0.2136 - 1170 B NA
Property_Single 10 100 532.5 ns 1.59 ns 1.41 ns 1.43 0.00 - - - NA
Property_Multiple 10 100 1,766.5 ns 2.20 ns 1.95 ns 4.75 0.01 - - - NA
Property_Nested 10 100 1,233.6 ns 1.78 ns 1.49 ns 3.32 0.01 - - - NA
Property_Concatenation 10 100 1,980.7 ns 5.00 ns 4.68 ns 5.33 0.02 0.0038 - 28 B NA
ItemList_Simple 10 100 19,412.5 ns 34.23 ns 30.34 ns 52.20 0.18 0.1526 - 874 B NA
ItemList_WithTransform 10 100 110,069.7 ns 199.78 ns 186.88 ns 295.99 0.88 1.4648 - 7816 B NA
ItemList_WithSeparator 10 100 10,760.2 ns 25.72 ns 21.48 ns 28.93 0.10 1.7395 0.0305 9162 B NA
Metadata_Unqualified 10 100 807.6 ns 1.96 ns 1.83 ns 2.17 0.01 - - - NA
Metadata_Qualified 10 100 1,022.9 ns 3.49 ns 3.27 ns 2.75 0.01 0.0076 - 40 B NA
Metadata_Multiple 10 100 1,266.5 ns 2.76 ns 2.58 ns 3.41 0.01 - - - NA
Mixed_PropertyAndItem 10 100 72,398.3 ns 75.22 ns 62.81 ns 194.66 0.70 1.4648 - 7806 B NA
Mixed_PropertyAndMetadata 10 100 2,313.9 ns 5.86 ns 5.20 ns 6.22 0.02 - - - NA
Mixed_All 10 100 74,906.8 ns 122.30 ns 114.40 ns 201.43 0.66 1.4648 - 8022 B NA
NoExpansion 10 100 371.9 ns 1.20 ns 1.12 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 10 100 1,807.0 ns 10.32 ns 9.65 ns 4.86 0.02 - - - NA
ItemListAndUnescape_WithTransform 10 100 112,803.3 ns 138.70 ns 115.82 ns 303.30 0.99 1.4648 - 7816 B NA
MetadataAndUnescape_Multiple 10 100 1,269.4 ns 5.50 ns 4.88 ns 3.41 0.02 - - - NA
MixedAndUnescape_All 10 100 75,341.4 ns 132.80 ns 124.22 ns 202.60 0.70 1.4648 - 8022 B NA
Property_Single 100 10 455.8 ns 0.67 ns 0.59 ns 1.16 0.00 - - - NA
Property_Multiple 100 10 1,712.4 ns 2.59 ns 2.29 ns 4.36 0.01 - - - NA
Property_Nested 100 10 1,248.5 ns 1.89 ns 1.67 ns 3.18 0.00 - - - NA
Property_Concatenation 100 10 2,024.9 ns 2.77 ns 2.59 ns 5.16 0.01 0.0038 - 28 B NA
ItemList_Simple 100 10 2,767.7 ns 5.69 ns 5.32 ns 7.05 0.02 0.0267 - 152 B NA
ItemList_WithTransform 100 10 12,855.3 ns 16.95 ns 15.85 ns 32.73 0.07 0.1831 - 962 B NA
ItemList_WithSeparator 100 10 2,159.0 ns 6.96 ns 6.51 ns 5.50 0.02 0.1106 - 593 B NA
Metadata_Unqualified 100 10 839.3 ns 4.88 ns 4.56 ns 2.14 0.01 - - - NA
Metadata_Qualified 100 10 1,024.2 ns 2.99 ns 2.80 ns 2.61 0.01 0.0076 - 40 B NA
Metadata_Multiple 100 10 1,253.2 ns 4.38 ns 4.09 ns 3.19 0.01 - - - NA
Mixed_PropertyAndItem 100 10 9,797.1 ns 23.63 ns 22.11 ns 24.94 0.07 0.1678 - 954 B NA
Mixed_PropertyAndMetadata 100 10 2,299.7 ns 4.41 ns 3.91 ns 5.86 0.02 - - - NA
Mixed_All 100 10 12,151.7 ns 22.81 ns 20.22 ns 30.94 0.07 0.2136 - 1170 B NA
NoExpansion 100 10 392.8 ns 0.81 ns 0.72 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 100 10 1,753.9 ns 4.22 ns 3.95 ns 4.46 0.01 - - - NA
ItemListAndUnescape_WithTransform 100 10 12,762.8 ns 23.90 ns 21.19 ns 32.49 0.09 0.1831 - 962 B NA
MetadataAndUnescape_Multiple 100 10 1,262.9 ns 2.16 ns 2.02 ns 3.22 0.01 - - - NA
MixedAndUnescape_All 100 10 12,250.3 ns 19.17 ns 16.01 ns 31.19 0.08 0.2136 - 1170 B NA
Property_Single 100 100 441.5 ns 3.14 ns 2.78 ns 1.22 0.01 - - - NA
Property_Multiple 100 100 1,723.2 ns 6.39 ns 5.98 ns 4.77 0.02 - - - NA
Property_Nested 100 100 1,235.0 ns 2.10 ns 1.86 ns 3.42 0.02 - - - NA
Property_Concatenation 100 100 2,079.8 ns 3.31 ns 2.93 ns 5.76 0.03 0.0038 - 28 B NA
ItemList_Simple 100 100 19,358.1 ns 18.49 ns 16.39 ns 53.63 0.22 0.1526 - 874 B NA
ItemList_WithTransform 100 100 109,961.7 ns 189.90 ns 158.57 ns 304.62 1.59 1.4648 - 7816 B NA
ItemList_WithSeparator 100 100 10,761.7 ns 27.46 ns 24.34 ns 29.82 0.11 1.7395 0.0305 9162 B NA
Metadata_Unqualified 100 100 825.2 ns 1.30 ns 1.15 ns 2.29 0.01 - - - NA
Metadata_Qualified 100 100 1,035.7 ns 2.98 ns 2.79 ns 2.87 0.02 0.0076 - 40 B NA
Metadata_Multiple 100 100 1,271.4 ns 3.37 ns 3.15 ns 3.52 0.02 - - - NA
Mixed_PropertyAndItem 100 100 72,998.2 ns 198.24 ns 185.43 ns 202.24 1.08 1.4648 - 7806 B NA
Mixed_PropertyAndMetadata 100 100 2,250.0 ns 5.41 ns 5.06 ns 6.23 0.03 - - - NA
Mixed_All 100 100 75,466.4 ns 111.68 ns 104.47 ns 209.08 0.96 1.4648 - 8022 B NA
NoExpansion 100 100 361.0 ns 1.66 ns 1.56 ns 1.00 0.00 - - - NA
PropertyAndUnescape_Multiple 100 100 1,780.2 ns 6.08 ns 5.69 ns 4.93 0.03 - - - NA
ItemListAndUnescape_WithTransform 100 100 109,854.9 ns 162.79 ns 152.28 ns 304.35 1.32 1.4648 - 7816 B NA
MetadataAndUnescape_Multiple 100 100 1,256.2 ns 2.39 ns 2.23 ns 3.48 0.02 - - - NA
MixedAndUnescape_All 100 100 74,960.3 ns 121.18 ns 107.42 ns 207.68 0.87 1.4648 - 8022 B NA

Highlights

  • 2–3× faster metadata expansion across both runtimes
  • Zero allocations for unqualified and multiple-metadata scenarios (previously 624–1,248 B on .NET 10, 369–741 B on .NET Framework)
  • Transform-based expansion also benefits slightly (~350 B savings on .NET 10, ~224 B on .NET Framework) due to shared infrastructure improvements
  • No regressions in any other expansion scenario (property, item list, separator)

Copilot AI review requested due to automatic review settings June 19, 2026 21:30

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 PR refactors MSBuild’s evaluation-time metadata expansion to remove regex usage and replace it with a manual, allocation-conscious scanner, aiming to reduce per-expansion overhead in the Expander hot path. It also updates related transform parsing and adds new microbenchmarks to measure the impact.

Changes:

  • Replaced regex-driven %(...) metadata expansion with a manual scanner in Expander.MetadataExpander.
  • Centralized metadata token parsing via ExpressionShredder.TryParseMetadataExpression and updated item transform metadata matching to avoid regex.
  • Added ExpanderBenchmark to quantify perf/alloc changes; removed now-unused regex helper files and project includes.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/MSBuild.Benchmarks/ExpanderBenchmark.cs Adds new benchmarks covering property/item/metadata/mixed expansion scenarios.
src/Build/Microsoft.Build.csproj Removes compile includes for deleted regex-based expander helpers.
src/Build/Evaluation/ExpressionShredder.cs Adds a metadata-expression parser helper and switches to Strings.WeakIntern usage.
src/Build/Evaluation/Expander.RegularExpressions.cs Removes regex infrastructure previously used by metadata expansion.
src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs Removes regex match-evaluator infrastructure used for metadata replacement.
src/Build/Evaluation/Expander.MetadataExpander.cs Rewrites metadata expansion to a manual scanner and gap-handling logic around item vectors.
src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs Updates match container to support manual-scan match info.
src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs Minor formatting/structure updates for match-kind enum.
src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs Replaces regex Match-based storage with index/length storage.
src/Build/Evaluation/Expander.ItemExpander.Transforms.cs Replaces regex matching for quoted expression metadata with manual scanning + TryParseMetadataExpression.

Comment thread src/MSBuild.Benchmarks/ExpanderBenchmark.cs
Comment thread src/MSBuild.Benchmarks/ExpanderBenchmark.cs Outdated
Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs Outdated
Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs
Comment thread src/MSBuild.Benchmarks/ExpanderBenchmark.cs Outdated
Comment thread src/Build/Evaluation/ExpressionShredder.cs Outdated
Comment thread src/Build/Evaluation/ExpressionShredder.cs
Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs

@jankratochvilcz jankratochvilcz 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.

Expert MSBuild review (automated)

High-quality, faithful refactor. The hand-written ref struct scanner reproduces the previous regex behavior across the edge cases I checked (qualified vs. unqualified metadata, internal/surrounding whitespace, nested %(a%(b)), malformed %()/%(, built-in-vs-custom gating, truncation, and self-reference logging). I verified parity empirically: the project builds with 0 warnings, and Expander_Tests + ExpressionShredder_Tests produce the same pre-existing macOS-platform failures on this branch as on origin/main — i.e. no regressions introduced.

String-comparison correctness is preserved (MSBuildNameIgnoreCaseComparer for the metadata dictionary, IsItemSpecModifier case-insensitive, ordinal for the self-reference item-type check and the %(/@( pre-scans). Name-character validation via XmlUtilities.IsValid*ElementNameCharacter is equivalent to the old regex's [A-Za-z_][A-Za-z_0-9\-]*.

The one substantive item worth addressing before merge is the avoidable double-parse of the item-vector expressions on the mixed @(%( path (mildly counter to the PR's perf goal). The rest are nits. Nice addition of ExpanderBenchmark to back the perf claims. No blocking issues.

Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs Outdated
Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs
Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs Outdated
Comment thread src/Build/Evaluation/ExpressionShredder.cs
Comment thread src/Build/Evaluation/ExpressionShredder.cs Outdated
@DustinCampbell
DustinCampbell requested a review from a team as a code owner June 22, 2026 17:16
…sion

Introduces ExpanderBenchmark to MSBuild.Benchmarks covering:
- Property expansion (single, multiple, nested, concatenation)
- Item list expansion (simple, transform, custom separator)
- Metadata expansion (unqualified, qualified, multiple)
- Mixed expressions (property+item, property+metadata, all)
- ExpandIntoStringAndUnescape variants for unescape overhead
- NoExpansion baseline

Parameterized on PropertyCount and ItemCount to measure scaling.
Eliminate all regex usage from metadata expansion by replacing it with a manual span-based scanner that delegates to a shared parsing method.

- Extract TryParseMetadataExpression on ExpressionShredder as the single shared metadata parsing implementation, using the existing Sink pattern and Strings.WeakIntern to avoid allocations. Refactor ExpressionShredder's own inline metadata parsing to call the new method.
- Refactor MetadataExpander from a static class to a readonly ref struct, capturing expansion context in fields and converting static methods to instance methods. Also, annotate for nullability.
- Inline MetadataMatchEvaluator logic into MetadataExpander and delete the type
- Rewrite Transforms.GetQuotedExpressionMatches to use manual scanning instead of ItemMetadataRegex
- Delete Expander.RegularExpressions.cs (entirely dead code) and Expander.MetadataMatchEvaluator.cs
- Replace s_invariantCompareInfo.IndexOf with string.Contains for simple ASCII substring checks
Declare return type of MetadataExpander.ExpandMetadataLeaveEscaped as `string` rather than `string?`.
ScanAndExpandMetadataInGaps re-ran ExpressionShredder.GetReferencedItemExpressions from scratch even though Expand had already shredded the expression to test the single-item-vector fast path. For any expression containing "@(" that isn't a lone full vector (e.g. "prefix@(Compile)" or "@(A);@(B)"), that parsed the first capture and interned its "@(...)" substring twice, throwing away the first pass.

Thread the already-advanced enumerator into ScanAndExpandMetadataInGaps so the shred happens exactly once, restoring the single-pass behavior of the original ExpandMetadataLeaveEscaped. The enumerator is passed by ref and consumed with a do/while since it is positioned at the first capture on entry.

The no-capture case (string contains "@(" but no well-formed item vector, e.g. "%(Culture)@(") is handled explicitly by scanning the whole string for metadata, so malformed-but-benign input keeps working rather than throwing.
The PR that replaced the metadata-expansion regex with a hand-written character scanner added only a benchmark, leaving the scanner's branches without explicit coverage. Add focused Theories over Expander.MetadataExpander that pin the exact expanded result so future edits can't silently regress them:

- ExpandMetadata_ScannerEdgeCases: surrounding/internal whitespace, malformed references (%(, %(), %( ), %(.x)), nested references (%(Culture%(Foo))), qualified vs. unqualified names, and missing metadata.
- ExpandMetadata_ItemVectorGapsAndSeparators: metadata before/after/between item vectors, the lone-vector fast path, metadata embedded in a separator, and a stray "@(" that doesn't form a valid item vector.
- ExpandMetadata_BuiltInVsCustomGating: built-in vs. custom expansion is gated on the matching ExpanderOptions flag (declared internal since ExpanderOptions is internal; xUnit discovers non-public test methods).
Replacing the metadata-expansion regex with a hand-written scanner dropped the "keep in sync with ProjectWriter" note that the deleted Expander.RegularExpressions.cs carried. The scanner validates item type and metadata names via ExpressionShredder.SinkValidName (XmlUtilities.IsValidInitial/SubsequentElementNameCharacter), which is exactly [A-Za-z_][A-Za-z_0-9\-]* — equivalent to ProjectWriter.itemTypeOrMetadataNameSpecification today.

Restore the coupling note in three places so the two grammars can't silently diverge:
- ExpressionShredder.SinkValidName (the shared validator)
- Expander.MetadataExpander.ScanAndExpandMetadata remarks
- ProjectWriter.itemTypeOrMetadataNameSpecification (back-reference)

Documentation only; no behavior change.
The end parameter was only respected by SinkValidName; SinkWhitespace and the single-char Sink bounded on expression.Length, so the metadata parser could read past end. This was benign for current callers (the gap scan always has '@' at end, which fails the name/'.'/')' checks before any over-read), but it was a latent footgun now that this is a shared, named internal helper with an explicit end.

Add end-aware overloads Sink(expression, ref i, end, c) and SinkWhitespace(expression, ref i, end), and route TryParseMetadataExpression through them so no character at or beyond end is read. The existing Length-bounded overloads now delegate to the end-aware versions, removing duplicated bounds logic. Also tidy the surrounding XML doc comments.

No behavior change for existing callers.
The de-regexed transform scanner (GetQuotedExpressionMatches) had no test covering its qualified-metadata error path. Add a Theory asserting that a metadata reference qualified with an item name inside a transform throws InvalidProjectFileException, and that the message reports both the offending reference and the suggested unqualified form. Cover the internal-whitespace variant (%( i . Meta0 )) so the scanner's whitespace handling on this path is pinned too.
@DustinCampbell
DustinCampbell force-pushed the metadata-expansion-perf branch from 820a99f to 140da40 Compare June 22, 2026 17:17
Comment thread src/Build/Evaluation/Expander.MetadataExpander.cs Outdated
Replace the character-by-character for-loop in ScanAndExpandMetadata with a while loop driven by string.IndexOf("%(", ...), so the search for the next metadata reference benefits from vectorized scanning instead of inspecting each character in managed code. Behavior is unchanged: a "%(" at endIndex - 1 still can't form a reference, failed parses resume past the "%(", and successful expansions resume after the reference.
@AlesProkop

Copy link
Copy Markdown
Member

@jankratochvilcz triage: Do you approve?

@AR-May AR-May left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. I think it is worth to get a bit more test coverage, so I triggered experimental VS insertion for this PR.

Upd: No failing tests, insertion shows a good improvement of BytesAllocated metrics. Ready to merge.

@AR-May
AR-May merged commit 608629d into dotnet:main Jun 24, 2026
14 checks passed
@DustinCampbell
DustinCampbell deleted the metadata-expansion-perf branch June 24, 2026 16:20
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.

6 participants