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