Sitelet https://github.com/coverlet-coverage/coverlet/commit/1f20869de5147d6723a3cbf4df40ded92dfe2ca2
Skip to content

Commit 1f20869

Browse files
committed
Add AutoProps benchmarks for SkipAutoProps perf analysis
Introduce AutoPropsBenchmarks to measure performance of SkipInlineAssignedAutoProperty and SkipGeneratedBackingFieldAssignment logic using new subject types with dense auto-properties and record patterns. Update Program.cs to run the new benchmarks with CPU usage profiling enabled via Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers. Add required dependencies and subject workload types to support comprehensive instrumentation performance testing.
1 parent a5581e8 commit 1f20869

5 files changed

Lines changed: 469 additions & 0 deletions

File tree

‎Directory.Packages.props‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
2222
<PackageVersion Include="BenchmarkDotNet.TestAdapter" Version="0.15.8" />
2323
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" />
24+
<PackageVersion Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.7.37220.1" />
2425
<!--<PackageVersion Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="17.13.35606.1" />-->
2526
<PackageVersion Include="DotNetConfig" Version="1.2.0" />
2627
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.8" /> <!-- latest stable version netstandard2.0, netstandard2.1, net462 -->
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Copyright (c) Toni Solarin-Sodara
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
// AutoPropsWorkload.cs
5+
// Purpose: exercises SkipInlineAssignedAutoProperty and SkipGeneratedBackingFieldAssignment
6+
// during instrumentation. Contains types that trigger the backing-field enumeration path:
7+
// - Classes with many inline-initialised auto-properties (high field-scan cost per instruction)
8+
// - Records without primary constructors (issue-1633 scenario)
9+
// - Abstract record hierarchies
10+
// - Records with explicit property bodies that produce real branch points
11+
// These types are intentionally NOT exercised at runtime in the benchmark; the benchmark
12+
// measures only the instrumentation (PrepareModules) phase.
13+
14+
using System;
15+
using System.Collections.Generic;
16+
17+
namespace coverlet.benchmark.subject
18+
{
19+
// ── Classes with many inline-initialised auto-properties ─────────────────
20+
// Each property contributes one k__BackingField. The constructor body gains
21+
// one ldarg/stfld pair per property, so SkipGeneratedBackingFieldAssignment
22+
// is invoked O(props × ctor-instructions) times.
23+
24+
/// <summary>Ten auto-properties initialised inline — baseline density.</summary>
25+
public class AutoProps10
26+
{
27+
public string Prop01 { get; set; } = "v01";
28+
public string Prop02 { get; set; } = "v02";
29+
public string Prop03 { get; set; } = "v03";
30+
public string Prop04 { get; set; } = "v04";
31+
public string Prop05 { get; set; } = "v05";
32+
public string Prop06 { get; set; } = "v06";
33+
public string Prop07 { get; set; } = "v07";
34+
public string Prop08 { get; set; } = "v08";
35+
public string Prop09 { get; set; } = "v09";
36+
public string Prop10 { get; set; } = "v10";
37+
}
38+
39+
/// <summary>Twenty-five auto-properties — amplifies the O(N×M) scan.</summary>
40+
public class AutoProps25
41+
{
42+
public string P01 { get; set; } = "a";
43+
public string P02 { get; set; } = "b";
44+
public string P03 { get; set; } = "c";
45+
public string P04 { get; set; } = "d";
46+
public string P05 { get; set; } = "e";
47+
public string P06 { get; set; } = "f";
48+
public string P07 { get; set; } = "g";
49+
public string P08 { get; set; } = "h";
50+
public string P09 { get; set; } = "i";
51+
public string P10 { get; set; } = "j";
52+
public string P11 { get; set; } = "k";
53+
public string P12 { get; set; } = "l";
54+
public string P13 { get; set; } = "m";
55+
public string P14 { get; set; } = "n";
56+
public string P15 { get; set; } = "o";
57+
public string P16 { get; set; } = "p";
58+
public string P17 { get; set; } = "q";
59+
public string P18 { get; set; } = "r";
60+
public string P19 { get; set; } = "s";
61+
public string P20 { get; set; } = "t";
62+
public string P21 { get; set; } = "u";
63+
public string P22 { get; set; } = "v";
64+
public string P23 { get; set; } = "w";
65+
public string P24 { get; set; } = "x";
66+
public string P25 { get; set; } = "y";
67+
}
68+
69+
// ── Records without primary constructors (issue-1633) ────────────────────
70+
// These are the exact patterns that the PR fixes. The compiler emits a
71+
// parameterless .ctor() that contains only ldarg.0 / call System.Object::.ctor() / ret.
72+
// The removed SkipDefaultInitializationSystemObject used to short-circuit this pattern;
73+
// now each ldarg instruction falls through to SkipGeneratedBackingFieldAssignment.
74+
75+
/// <summary>Record without primary-constructor parens — the issue-1633 trigger.</summary>
76+
public record RecordNoCtor
77+
{
78+
public string Name { get; init; } = string.Empty;
79+
public int Age { get; init; }
80+
public bool IsActive { get; init; } = true;
81+
82+
public string Display() => $"{Name}/{Age}/{IsActive}";
83+
}
84+
85+
/// <summary>Record with empty primary constructor — the user workaround.</summary>
86+
public record RecordEmptyCtor()
87+
{
88+
public string Name { get; init; } = string.Empty;
89+
public int Age { get; init; }
90+
public bool IsActive { get; init; } = true;
91+
92+
public string Display() => $"{Name}/{Age}/{IsActive}";
93+
}
94+
95+
/// <summary>Record with primary constructor parameters and additional properties.</summary>
96+
public record RecordWithPrimaryCtor(string Name, int Age)
97+
{
98+
public bool IsActive { get; init; } = true;
99+
public string Category { get; init; } = "default";
100+
101+
public string Display() => $"{Name}/{Age}/{IsActive}/{Category}";
102+
}
103+
104+
// ── Abstract record hierarchies ──────────────────────────────────────────
105+
106+
public abstract record AbstractShapeRecord
107+
{
108+
public string Color { get; init; } = "white";
109+
public abstract double Area();
110+
}
111+
112+
public abstract record AbstractShapeWithCtor()
113+
{
114+
public string Color { get; init; } = "white";
115+
public abstract double Area();
116+
}
117+
118+
public record CircleRecord : AbstractShapeRecord
119+
{
120+
public double Radius { get; init; }
121+
public override double Area() => Math.PI * Radius * Radius;
122+
}
123+
124+
public record RectangleRecord : AbstractShapeWithCtor
125+
{
126+
public double Width { get; init; }
127+
public double Height { get; init; }
128+
public override double Area() => Width * Height;
129+
}
130+
131+
// ── Records with branches (triggers branch-point instrumentation path) ───
132+
// These records produce real branch-points in their methods so that the
133+
// benchmark covers both SkipAutoProps logic AND the branch-instrumentation path.
134+
135+
public record OrderRecord(string Id, decimal Amount, string Status)
136+
{
137+
public bool IsValid => !string.IsNullOrWhiteSpace(Id) && Amount > 0;
138+
139+
public string Classify() => Amount switch
140+
{
141+
<= 0 => "invalid",
142+
< 100 => "small",
143+
< 1_000 => "medium",
144+
< 10_000 => "large",
145+
_ => "enterprise",
146+
};
147+
148+
public string Describe()
149+
{
150+
if (!IsValid)
151+
return "invalid order";
152+
153+
string tier = Classify();
154+
return Status switch
155+
{
156+
"pending" => $"{tier}: awaiting payment",
157+
"paid" => $"{tier}: processing",
158+
"shipped" => $"{tier}: in transit",
159+
"delivered" => $"{tier}: complete",
160+
_ => $"{tier}: unknown status",
161+
};
162+
}
163+
}
164+
165+
public record ProductRecord(string Sku, string Name, decimal Price, int Stock)
166+
{
167+
public bool InStock => Stock > 0;
168+
public bool IsAffordable => Price < 50m;
169+
170+
public string StockStatus()
171+
{
172+
if (Stock <= 0)
173+
return "out-of-stock";
174+
if (Stock < 5)
175+
return "low-stock";
176+
if (Stock < 20)
177+
return "limited";
178+
return "available";
179+
}
180+
181+
public string PriceTier() => Price switch
182+
{
183+
< 10m => "budget",
184+
< 50m => "value",
185+
< 200m => "standard",
186+
< 500m => "premium",
187+
_ => "luxury",
188+
};
189+
}
190+
191+
// ── Mixed class/record scenario ──────────────────────────────────────────
192+
// A class that owns record-typed auto-properties; used to verify that
193+
// SkipGeneratedBackingFieldAssignment correctly handles field references
194+
// to record types and doesn't misclassify them.
195+
196+
public class RecordAggregator
197+
{
198+
public RecordNoCtor? NoCtor { get; set; }
199+
public RecordEmptyCtor? EmptyCtor { get; set; }
200+
public RecordWithPrimaryCtor? WithCtor { get; set; }
201+
202+
public string Summary()
203+
{
204+
if (NoCtor is null && EmptyCtor is null && WithCtor is null)
205+
return "empty";
206+
207+
var parts = new List<string>(3);
208+
if (NoCtor is not null)
209+
parts.Add(NoCtor.Display());
210+
if (EmptyCtor is not null)
211+
parts.Add(EmptyCtor.Display());
212+
if (WithCtor is not null)
213+
parts.Add(WithCtor.Display());
214+
215+
return string.Join(", ", parts);
216+
}
217+
}
218+
}

0 commit comments

Comments
 (0)