1
+ using System ;
2
+ using System . Collections . Generic ;
3
+ using System . Diagnostics ;
4
+ using System . IO ;
5
+ using System . Linq ;
6
+ using BenchmarkDotNet . Analysers ;
7
+ using BenchmarkDotNet . Engines ;
8
+ using BenchmarkDotNet . Exporters ;
9
+ using BenchmarkDotNet . Extensions ;
10
+ using BenchmarkDotNet . Helpers ;
11
+ using BenchmarkDotNet . Jobs ;
12
+ using BenchmarkDotNet . Loggers ;
13
+ using BenchmarkDotNet . Portability ;
14
+ using BenchmarkDotNet . Reports ;
15
+ using BenchmarkDotNet . Running ;
16
+ using BenchmarkDotNet . Toolchains ;
17
+ using BenchmarkDotNet . Toolchains . CoreRun ;
18
+ using BenchmarkDotNet . Toolchains . CsProj ;
19
+ using BenchmarkDotNet . Toolchains . DotNetCli ;
20
+ using BenchmarkDotNet . Toolchains . NativeAot ;
21
+ using BenchmarkDotNet . Validators ;
22
+ using JetBrains . Annotations ;
23
+ using Mono . Unix . Native ;
24
+
25
+ namespace BenchmarkDotNet . Diagnosers
26
+ {
27
+ public class PerfCollectProfiler : IProfiler
28
+ {
29
+ public static readonly IDiagnoser Default = new PerfCollectProfiler ( new PerfCollectProfilerConfig ( performExtraBenchmarksRun : false ) ) ;
30
+
31
+ private readonly PerfCollectProfilerConfig config ;
32
+ private readonly DateTime creationTime = DateTime . Now ;
33
+ private readonly Dictionary < BenchmarkCase , FileInfo > benchmarkToTraceFile = new ( ) ;
34
+ private readonly HashSet < string > cliPathWithSymbolsInstalled = new ( ) ;
35
+ private FileInfo perfCollectFile ;
36
+ private Process perfCollectProcess ;
37
+
38
+ [ PublicAPI ]
39
+ public PerfCollectProfiler ( PerfCollectProfilerConfig config ) => this . config = config ;
40
+
41
+ public string ShortName => "perf" ;
42
+
43
+ public IEnumerable < string > Ids => new [ ] { nameof ( PerfCollectProfiler ) } ;
44
+
45
+ public IEnumerable < IExporter > Exporters => Array . Empty < IExporter > ( ) ;
46
+
47
+ public IEnumerable < IAnalyser > Analysers => Array . Empty < IAnalyser > ( ) ;
48
+
49
+ public IEnumerable < Metric > ProcessResults ( DiagnoserResults results ) => Array . Empty < Metric > ( ) ;
50
+
51
+ public RunMode GetRunMode ( BenchmarkCase benchmarkCase ) => config . RunMode ;
52
+
53
+ public IEnumerable < ValidationError > Validate ( ValidationParameters validationParameters )
54
+ {
55
+ if ( ! RuntimeInformation . IsLinux ( ) )
56
+ {
57
+ yield return new ValidationError ( true , "The PerfCollectProfiler works only on Linux!" ) ;
58
+ yield break ;
59
+ }
60
+
61
+ if ( Syscall . getuid ( ) != 0 )
62
+ {
63
+ yield return new ValidationError ( true , "You must run as root to use PerfCollectProfiler." ) ;
64
+ yield break ;
65
+ }
66
+
67
+ if ( validationParameters . Benchmarks . Any ( ) && ! TryInstallPerfCollect ( validationParameters ) )
68
+ {
69
+ yield return new ValidationError ( true , "Failed to install perfcollect script. Please follow the instructions from https://github.com/dotnet/runtime/blob/main/docs/project/linux-performance-tracing.md" ) ;
70
+ }
71
+ }
72
+
73
+ public void DisplayResults ( ILogger logger )
74
+ {
75
+ if ( ! benchmarkToTraceFile . Any ( ) )
76
+ return ;
77
+
78
+ logger . WriteLineInfo ( $ "Exported { benchmarkToTraceFile . Count } trace file(s). Example:") ;
79
+ logger . WriteLineInfo ( benchmarkToTraceFile . Values . First ( ) . FullName ) ;
80
+ }
81
+
82
+ public void Handle ( HostSignal signal , DiagnoserActionParameters parameters )
83
+ {
84
+ if ( signal == HostSignal . BeforeProcessStart )
85
+ perfCollectProcess = StartCollection ( parameters ) ;
86
+ else if ( signal == HostSignal . AfterProcessExit )
87
+ StopCollection ( parameters ) ;
88
+ }
89
+
90
+ private bool TryInstallPerfCollect ( ValidationParameters validationParameters )
91
+ {
92
+ var scriptInstallationDirectory = new DirectoryInfo ( validationParameters . Config . ArtifactsPath ) . CreateIfNotExists ( ) ;
93
+
94
+ perfCollectFile = new FileInfo ( Path . Combine ( scriptInstallationDirectory . FullName , "perfcollect" ) ) ;
95
+ if ( perfCollectFile . Exists )
96
+ {
97
+ return true ;
98
+ }
99
+
100
+ var logger = validationParameters . Config . GetCompositeLogger ( ) ;
101
+
102
+ string script = ResourceHelper . LoadTemplate ( perfCollectFile . Name ) ;
103
+ File . WriteAllText ( perfCollectFile . FullName , script ) ;
104
+
105
+ if ( Syscall . chmod ( perfCollectFile . FullName , FilePermissions . S_IXUSR ) != 0 )
106
+ {
107
+ logger . WriteError ( $ "Unable to make perfcollect script an executable, the last error was: { Syscall . GetLastError ( ) } ") ;
108
+ }
109
+ else
110
+ {
111
+ ( int exitCode , var output ) = ProcessHelper . RunAndReadOutputLineByLine ( perfCollectFile . FullName , "install -force" , perfCollectFile . Directory . FullName , null , includeErrors : true , logger ) ;
112
+
113
+ if ( exitCode == 0 )
114
+ {
115
+ logger . WriteLine ( "Successfully installed perfcollect" ) ;
116
+ return true ;
117
+ }
118
+
119
+ logger . WriteLineError ( "Failed to install perfcollect" ) ;
120
+ foreach ( var outputLine in output )
121
+ {
122
+ logger . WriteLine ( outputLine ) ;
123
+ }
124
+ }
125
+
126
+ if ( perfCollectFile . Exists )
127
+ {
128
+ perfCollectFile . Delete ( ) ; // if the file exists it means that perfcollect is installed
129
+ }
130
+
131
+ return false ;
132
+ }
133
+
134
+ private Process StartCollection ( DiagnoserActionParameters parameters )
135
+ {
136
+ EnsureSymbolsForNativeRuntime ( parameters ) ;
137
+
138
+ var traceName = GetTraceFile ( parameters , extension : null ) . Name ;
139
+
140
+ var start = new ProcessStartInfo
141
+ {
142
+ FileName = perfCollectFile . FullName ,
143
+ Arguments = $ "collect \" { traceName } \" ",
144
+ UseShellExecute = false ,
145
+ RedirectStandardOutput = true ,
146
+ CreateNoWindow = true ,
147
+ WorkingDirectory = perfCollectFile . Directory . FullName
148
+ } ;
149
+
150
+ return Process . Start ( start ) ;
151
+ }
152
+
153
+ private void StopCollection ( DiagnoserActionParameters parameters )
154
+ {
155
+ var logger = parameters . Config . GetCompositeLogger ( ) ;
156
+
157
+ try
158
+ {
159
+ if ( ! perfCollectProcess . HasExited )
160
+ {
161
+ if ( Syscall . kill ( perfCollectProcess . Id , Signum . SIGINT ) != 0 )
162
+ {
163
+ var lastError = Stdlib . GetLastError ( ) ;
164
+ logger . WriteLineError ( $ "kill(perfcollect, SIGINT) failed with { lastError } ") ;
165
+ }
166
+
167
+ if ( ! perfCollectProcess . WaitForExit ( ( int ) config . Timeout . TotalMilliseconds ) )
168
+ {
169
+ logger . WriteLineError ( $ "The perfcollect script did not stop in { config . Timeout . TotalSeconds } s. It's going to be force killed now.") ;
170
+ logger . WriteLineInfo ( "You can create PerfCollectProfiler providing PerfCollectProfilerConfig with custom timeout value." ) ;
171
+
172
+ perfCollectProcess . KillTree ( ) ; // kill the entire process tree
173
+ }
174
+
175
+ FileInfo traceFile = GetTraceFile ( parameters , "trace.zip" ) ;
176
+ if ( traceFile . Exists )
177
+ {
178
+ benchmarkToTraceFile [ parameters . BenchmarkCase ] = traceFile ;
179
+ }
180
+ }
181
+ else
182
+ {
183
+ logger . WriteLineError ( "For some reason the perfcollect script has finished sooner than expected." ) ;
184
+ logger . WriteLineInfo ( $ "Please run '{ perfCollectFile . FullName } install' as root and re-try.") ;
185
+ }
186
+ }
187
+ finally
188
+ {
189
+ perfCollectProcess . Dispose ( ) ;
190
+ }
191
+ }
192
+
193
+ private void EnsureSymbolsForNativeRuntime ( DiagnoserActionParameters parameters )
194
+ {
195
+ string cliPath = parameters . BenchmarkCase . GetToolchain ( ) switch
196
+ {
197
+ CsProjCoreToolchain core => core . CustomDotNetCliPath ,
198
+ CoreRunToolchain coreRun => coreRun . CustomDotNetCliPath . FullName ,
199
+ NativeAotToolchain nativeAot => nativeAot . CustomDotNetCliPath ,
200
+ _ => DotNetCliCommandExecutor . DefaultDotNetCliPath . Value
201
+ } ;
202
+
203
+ if ( ! cliPathWithSymbolsInstalled . Add ( cliPath ) )
204
+ {
205
+ return ;
206
+ }
207
+
208
+ string sdkPath = DotNetCliCommandExecutor . GetSdkPath ( cliPath ) ; // /usr/share/dotnet/sdk/
209
+ string dotnetPath = Path . GetDirectoryName ( sdkPath ) ; // /usr/share/dotnet/
210
+ string [ ] missingSymbols = Directory . GetFiles ( dotnetPath , "lib*.so" , SearchOption . AllDirectories )
211
+ . Where ( nativeLibPath => ! nativeLibPath . Contains ( "FallbackFolder" ) && ! File . Exists ( Path . ChangeExtension ( nativeLibPath , "so.dbg" ) ) )
212
+ . Select ( Path . GetDirectoryName )
213
+ . Distinct ( )
214
+ . ToArray ( ) ;
215
+
216
+ if ( ! missingSymbols . Any ( ) )
217
+ {
218
+ return ; // the symbol files are already where we need them!
219
+ }
220
+
221
+ ILogger logger = parameters . Config . GetCompositeLogger ( ) ;
222
+ // We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs.
223
+ string toolPath = Path . Combine ( Path . GetTempPath ( ) , "BenchmarkDotNet" , "symbols" ) ;
224
+ DotNetCliCommand cliCommand = new (
225
+ cliPath : cliPath ,
226
+ arguments : $ "tool install dotnet-symbol --tool-path \" { toolPath } \" ",
227
+ generateResult : null ,
228
+ logger : logger ,
229
+ buildPartition : null ,
230
+ environmentVariables : Array . Empty < EnvironmentVariable > ( ) ,
231
+ timeout : TimeSpan . FromMinutes ( 3 ) ,
232
+ logOutput : true ) ; // the following commands might take a while and fail, let's log them
233
+
234
+ var installResult = DotNetCliCommandExecutor . Execute ( cliCommand ) ;
235
+ if ( ! installResult . IsSuccess )
236
+ {
237
+ logger . WriteError ( "Unable to install dotnet symbol." ) ;
238
+ return ;
239
+ }
240
+
241
+ DotNetCliCommandExecutor . Execute ( cliCommand
242
+ . WithCliPath ( Path . Combine ( toolPath , "dotnet-symbol" ) )
243
+ . WithArguments ( $ "--recurse-subdirectories --symbols \" { dotnetPath } /dotnet\" \" { dotnetPath } /lib*.so\" ") ) ;
244
+
245
+ DotNetCliCommandExecutor . Execute ( cliCommand . WithArguments ( $ "tool uninstall dotnet-symbol --tool-path \" { toolPath } \" ") ) ;
246
+ }
247
+
248
+ private FileInfo GetTraceFile ( DiagnoserActionParameters parameters , string extension )
249
+ => new ( ArtifactFileNameHelper . GetTraceFilePath ( parameters , creationTime , extension )
250
+ . Replace ( " " , "_" ) ) ; // perfcollect does not allow for spaces in the trace file name
251
+ }
252
+ }
0 commit comments