Skip to content

Commit 861226a

Browse files
CopilotstephentoubadamsitnikEgorBo
authored
Add performance-benchmark skill for ad hoc benchmarking with EgorBot (#123319)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
1 parent bb5ea9b commit 861226a

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

  • .github/skills/performance-benchmark
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
---
2+
name: performance-benchmark
3+
description: Generate and run ad hoc performance benchmarks to validate code changes. Use this when asked to benchmark, profile, or validate the performance impact of a code change in dotnet/runtime.
4+
---
5+
6+
# Ad Hoc Performance Benchmarking
7+
8+
When you need to validate the performance impact of a code change, follow this process to write a BenchmarkDotNet benchmark and trigger EgorBot to run it.
9+
10+
## Step 1: Write the Benchmark
11+
12+
Create a BenchmarkDotNet benchmark that tests the specific operation being changed. Follow these guidelines:
13+
14+
### Benchmark Structure
15+
16+
```csharp
17+
using BenchmarkDotNet.Attributes;
18+
using BenchmarkDotNet.Running;
19+
20+
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
21+
22+
public class Bench
23+
{
24+
// Add setup/cleanup if needed
25+
[GlobalSetup]
26+
public void Setup()
27+
{
28+
// Initialize test data
29+
}
30+
31+
[Benchmark]
32+
public void MyOperation()
33+
{
34+
// Test the operation
35+
}
36+
}
37+
```
38+
39+
### Best Practices
40+
41+
For comprehensive guidance, see the [Microbenchmark Design Guidelines](https://github.com/dotnet/performance/blob/main/docs/microbenchmark-design-guidelines.md).
42+
43+
Key principles:
44+
45+
- **Move initialization to `[GlobalSetup]`**: Separate setup logic from the measured code to avoid measuring allocation/initialization overhead
46+
- **Return values** from benchmark methods to prevent dead code elimination
47+
- **Avoid loops**: BenchmarkDotNet invokes the benchmark many times automatically; adding manual loops distorts measurements
48+
- **No side effects**: Benchmarks should be pure and produce consistent results
49+
- **Focus on common cases**: Benchmark hot paths and typical usage, not edge cases or error paths
50+
- **Use consistent input data**: Always use the same test data for reproducible comparisons
51+
- **Avoid `[DisassemblyDiagnoser]`**: It causes crashes on Linux. Use `--envvars DOTNET_JitDisasm:MethodName` instead
52+
- **Benchmark class requirements**: Must be `public`, not `sealed`, not `static`, and must be a `class` (not struct)
53+
54+
### Example: String Operation Benchmark
55+
56+
```csharp
57+
using BenchmarkDotNet.Attributes;
58+
using BenchmarkDotNet.Running;
59+
60+
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
61+
62+
[MemoryDiagnoser]
63+
public class Bench
64+
{
65+
private string _testString = default!;
66+
67+
[Params(10, 100, 1000)]
68+
public int Length { get; set; }
69+
70+
[GlobalSetup]
71+
public void Setup()
72+
{
73+
_testString = new string('a', Length);
74+
}
75+
76+
[Benchmark]
77+
public int StringOperation()
78+
{
79+
return _testString.IndexOf('z');
80+
}
81+
}
82+
```
83+
84+
### Example: Collection Operation Benchmark
85+
86+
```csharp
87+
using System.Linq;
88+
using BenchmarkDotNet.Attributes;
89+
using BenchmarkDotNet.Running;
90+
91+
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
92+
93+
[MemoryDiagnoser]
94+
public class Bench
95+
{
96+
private int[] _array = default!;
97+
private List<int> _list = default!;
98+
99+
[Params(100, 1000, 10000)]
100+
public int Count { get; set; }
101+
102+
[GlobalSetup]
103+
public void Setup()
104+
{
105+
_array = Enumerable.Range(0, Count).ToArray();
106+
_list = _array.ToList();
107+
}
108+
109+
[Benchmark]
110+
public bool AnyArray() => _array.Any();
111+
112+
[Benchmark]
113+
public bool AnyList() => _list.Any();
114+
115+
[Benchmark]
116+
public int SumArray() => _array.Sum();
117+
118+
[Benchmark]
119+
public int SumList() => _list.Sum();
120+
}
121+
```
122+
123+
## Step 2: Post the EgorBot Comment
124+
125+
Post a comment on the PR to trigger EgorBot with your benchmark. The general format is:
126+
127+
```
128+
@EgorBot [target flags] [options] [BenchmarkDotNet args]
129+
130+
```cs
131+
// Your benchmark code here
132+
```
133+
```
134+
135+
### Target Flags (Required - Choose at Least One)
136+
137+
| Flag | Architecture | Description |
138+
|------|--------------|-------------|
139+
| `-x64` or `-amd` | x64 | Linux Azure Genoa (AMD EPYC) - default x64 target |
140+
| `-arm` | ARM64 | Linux Azure Cobalt100 (Neoverse-N2) |
141+
| `-intel` | x64 | Azure Cascade Lake (more flaky due to JCC Erratum and loop alignment sensitivity) |
142+
| `-windows_x64` | x64 | Windows x64 (when Windows-specific testing is needed) |
143+
144+
**Choosing targets:**
145+
146+
- **Default for most changes**: Use `-x64` for quick verification of non-architecture/non-OS specific changes
147+
- **Default when ARM might differ**: Use `-x64 -arm` if there's any suspicion the change might behave differently on ARM
148+
- **Windows-specific changes**: Use `-windows_x64` when Windows behavior needs testing
149+
- **Noisy results suspected**: Use `-arm -intel -amd` to get results from multiple x64 CPUs (note: `-intel` targets are more flaky)
150+
151+
### Common Options
152+
153+
| Option | Description |
154+
|--------|-------------|
155+
| `-profiler` | Collect flamegraph/hot assembly using perf record |
156+
| `--envvars KEY:VALUE` | Set environment variables (e.g., `DOTNET_JitDisasm:MethodName`) |
157+
| `-commit <hash>` | Run against a specific commit |
158+
| `-commit <hash1> vs <hash2>` | Compare two commits |
159+
| `-commit <hash> vs previous` | Compare commit with its parent |
160+
161+
### Example: Basic PR Benchmark
162+
163+
To benchmark the current PR changes against the base branch:
164+
165+
```
166+
@EgorBot -x64 -arm
167+
168+
```cs
169+
using BenchmarkDotNet.Attributes;
170+
using BenchmarkDotNet.Running;
171+
172+
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
173+
174+
[MemoryDiagnoser]
175+
public class Bench
176+
{
177+
[Benchmark]
178+
public int MyOperation()
179+
{
180+
// Your benchmark code
181+
return 42;
182+
}
183+
}
184+
```
185+
```
186+
187+
### Example: Benchmark with Profiling and Disassembly
188+
189+
```
190+
@EgorBot -x64 -profiler --envvars DOTNET_JitDisasm:SumArray
191+
192+
```cs
193+
using System.Linq;
194+
using BenchmarkDotNet.Attributes;
195+
using BenchmarkDotNet.Running;
196+
197+
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
198+
199+
public class Bench
200+
{
201+
private int[] _data = Enumerable.Range(0, 1000).ToArray();
202+
203+
[Benchmark]
204+
public int SumArray() => _data.Sum();
205+
}
206+
```
207+
```
208+
209+
### Example: Compare Two Commits
210+
211+
```
212+
@EgorBot -amd -commit abc1234 vs def5678
213+
214+
```cs
215+
using BenchmarkDotNet.Attributes;
216+
using BenchmarkDotNet.Running;
217+
218+
BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);
219+
220+
public class Bench
221+
{
222+
[Benchmark]
223+
public void TestMethod()
224+
{
225+
// Benchmark code
226+
}
227+
}
228+
```
229+
```
230+
231+
### Example: Run Existing dotnet/performance Benchmarks
232+
233+
To run benchmarks from the dotnet/performance repository (no code snippet needed):
234+
235+
```
236+
@EgorBot -arm -intel --filter `*TryGetValueFalse<String, String>*`
237+
```
238+
239+
**Note**: Surround filter expressions with backticks to avoid issues with special characters.
240+
241+
## Important Notes
242+
243+
- **Bot response time**: EgorBot uses polling and may take up to 30 seconds to respond
244+
- **Supported repositories**: EgorBot monitors `dotnet/runtime` and `EgorBot/runtime-utils`
245+
- **PR mode (default)**: When posting in a PR, EgorBot automatically compares the PR changes against the base branch
246+
- **Results variability**: Results may vary between runs due to VM differences. Do not compare results across different architectures or cloud providers
247+
- **Check the manual**: EgorBot replies include a link to the [manual](https://github.com/EgorBot/runtime-utils) for advanced options
248+
249+
## Additional Resources
250+
251+
- [Microbenchmark Design Guidelines](https://github.com/dotnet/performance/blob/main/docs/microbenchmark-design-guidelines.md) - Essential reading for writing effective benchmarks
252+
- [BenchmarkDotNet CLI Arguments](https://github.com/dotnet/BenchmarkDotNet/blob/master/docs/articles/guides/console-args.md)
253+
- [EgorBot Manual](https://github.com/EgorBot/runtime-utils)
254+
- [BenchmarkDotNet Filter Simulator](http://egorbot.westus2.cloudapp.azure.com:5042/microbenchmarks)

0 commit comments

Comments
 (0)