-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathEtwProfiler.cs
More file actions
174 lines (138 loc) · 7.29 KB
/
EtwProfiler.cs
File metadata and controls
174 lines (138 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Diagnostics.Windows.Tracing;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Validators;
using JetBrains.Annotations;
using Microsoft.Diagnostics.Tracing.Session;
namespace BenchmarkDotNet.Diagnostics.Windows
{
public class EtwProfiler : IDiagnoser, IHardwareCountersDiagnoser, IProfiler
{
private readonly EtwProfilerConfig config;
private readonly RunMode runMode;
private readonly Dictionary<BenchmarkCase, string> benchmarkToEtlFile;
private readonly Dictionary<BenchmarkCase, PreciseMachineCounter[]> benchmarkToCounters;
private Session kernelSession, userSession, heapSession;
[PublicAPI] // parameterless ctor required by DiagnosersLoader to support creating this profiler via console line args
public EtwProfiler() : this(new EtwProfilerConfig(performExtraBenchmarksRun: false)) { }
[PublicAPI]
public EtwProfiler(EtwProfilerConfig config)
{
this.config = config;
runMode = config.PerformExtraBenchmarksRun ? RunMode.ExtraRun : RunMode.NoOverhead;
benchmarkToEtlFile = new Dictionary<BenchmarkCase, string>();
benchmarkToCounters = new Dictionary<BenchmarkCase, PreciseMachineCounter[]>();
CreationTime = DateTime.Now;
}
public string ShortName => "ETW";
public IEnumerable<string> Ids => new[] { nameof(EtwProfiler) };
public IEnumerable<IExporter> Exporters => Array.Empty<IExporter>();
public IEnumerable<IAnalyser> Analysers => Array.Empty<IAnalyser>();
public IReadOnlyDictionary<BenchmarkCase, PmcStats> Results => BuildPmcStats();
internal IReadOnlyDictionary<BenchmarkCase, string> BenchmarkToEtlFile => benchmarkToEtlFile;
private DateTime CreationTime { get; }
public RunMode GetRunMode(BenchmarkCase benchmarkCase) => runMode;
public IEnumerable<ValidationError> Validate(ValidationParameters validationParameters)
=> HardwareCounters.Validate(validationParameters, mandatory: false);
public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
// it's crucial to start the trace before the process starts and stop it after the benchmarked process stops to have all of the necessary events in the trace file!
if (signal == HostSignal.BeforeProcessStart)
Start(parameters);
else if (signal == HostSignal.AfterProcessExit)
Stop(parameters);
}
public IEnumerable<Metric> ProcessResults(DiagnoserResults results)
{
if (!benchmarkToEtlFile.TryGetValue(results.BenchmarkCase, out var traceFilePath))
return Array.Empty<Metric>();
// currently TraceLogParser parsers the counters metrics only. So if there are no counters configured, it makes no sense to parse the file
if (!benchmarkToCounters.TryGetValue(results.BenchmarkCase, out var counters) || counters.IsEmpty())
return Array.Empty<Metric>();
return TraceLogParser.Parse(traceFilePath, counters);
}
public void DisplayResults(ILogger logger)
{
if (!benchmarkToEtlFile.Any())
return;
logger.WriteLineInfo($"Exported {benchmarkToEtlFile.Count} trace file(s). Example:");
logger.WriteLineInfo(benchmarkToEtlFile.Values.First());
}
private void Start(DiagnoserActionParameters parameters)
{
// Collect both built-in hardware counters and custom counters
var hardwareCountersList = parameters.Config
.GetHardwareCounters()
.Select(counter => HardwareCounters.FromCounter(counter, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval))
.ToList();
var customCountersList = parameters.Config
.GetCustomCounters()
.Select(customCounter => HardwareCounters.FromCustomCounter(customCounter, GetInterval))
.ToList();
var counters = benchmarkToCounters[parameters.BenchmarkCase] = hardwareCountersList.Concat(customCountersList).ToArray();
if (counters.Any()) // we need to enable the counters before starting the kernel session
HardwareCounters.Enable(counters);
try
{
kernelSession = new KernelSession(parameters, config, CreationTime).EnableProviders();
if (config.CreateHeapSession)
heapSession = new HeapSession(parameters, config, CreationTime).EnableProviders();
userSession = new UserSession(parameters, config, CreationTime).EnableProviders();
}
catch (Exception)
{
userSession?.Dispose();
heapSession?.Dispose();
kernelSession?.Dispose();
throw;
}
}
private void Stop(DiagnoserActionParameters parameters)
{
WaitForDelayedEvents();
string userSessionFile = userSession.FilePath;
kernelSession.Dispose();
heapSession?.Dispose();
userSession.Dispose();
// Merge the 'primary' etl file X.etl (userSession) with any files that match .clr*.etl .user*.etl. and .kernel.etl.
TraceEventSession.MergeInPlace(userSessionFile, TextWriter.Null);
benchmarkToEtlFile[parameters.BenchmarkCase] = userSessionFile;
}
private static int GetInterval(ProfileSourceInfo info) => Math.Min(info.MaxInterval, Math.Max(info.MinInterval, info.Interval));
/// <summary>
/// ETW sessions receive events with a slight delay.
/// This increases the likelihood that all relevant events are processed by the collection thread by the time we are done with the benchmark.
/// </summary>
private static void WaitForDelayedEvents() => Thread.Sleep(TimeSpan.FromMilliseconds(500));
private IReadOnlyDictionary<BenchmarkCase, PmcStats> BuildPmcStats()
{
var builder = ImmutableDictionary.CreateBuilder<BenchmarkCase, PmcStats>();
foreach (var benchmarkToCounter in benchmarkToCounters)
{
var allCounters = benchmarkToCounter.Value;
var builtInCounters = allCounters.Where(x => x.Counter != HardwareCounter.NotSet).ToList();
var customCounters = allCounters.Where(x => x.CustomCounter != null).ToList();
var uniqueHwCounters = builtInCounters.Select(x => x.Counter).Distinct().ToImmutableArray();
var pmcStats = new PmcStats(
uniqueHwCounters,
customCounters,
counter => builtInCounters.Single(pmc => pmc.Counter == counter)
);
builder.Add(benchmarkToCounter.Key, pmcStats);
}
return builder.ToImmutable();
}
}
}