using Microsoft.Diagnostics.Tracing; using Microsoft.Diagnostics.Tracing.Parsers; using Microsoft.Diagnostics.Tracing.Parsers.Clr; using Microsoft.Diagnostics.Tracing.Session; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Sandbox; class Launcher { private static TraceEventSession _kernelSession; private static TraceEventSession _userSession; private static int _targetPid; private static ManualResetEventSlim _shutdownEvent = new ManualResetEventSlim( false ); private static string _baseEtlFileName; private static string _kernelEtlFileName; private static string _rundownEtlFileName; private static string _additionalSymbolPath; private static bool _noUpload = false; private static void Init( int targetPid ) { _targetPid = targetPid; Commands.Log( $"Target PID: {_targetPid}" ); string baseName = $"../../profiler_captures/sbox_{DateTime.Now:yyyy-MM-dd_HH_mm_ss}"; Directory.CreateDirectory( Path.GetDirectoryName( baseName ) ); _baseEtlFileName = $"{baseName}.etl"; _kernelEtlFileName = $"{baseName}.kernel.etl"; _rundownEtlFileName = $"{baseName}.clrRundown.etl"; float cpuSampleIntervalMs = 0.2f; int circularBufferMB = 0; bool stackCompression = false; _kernelSession = new TraceEventSession( _kernelEtlFileName, _kernelEtlFileName ); _kernelSession.CircularBufferMB = circularBufferMB; _kernelSession.CpuSampleIntervalMSec = cpuSampleIntervalMs; _kernelSession.StackCompression = stackCompression; //// Filter user provider for the target process var userProviderOptions = new TraceEventProviderOptions { StacksEnabled = true, }; userProviderOptions.ProcessIDFilter = new List { _targetPid }; _userSession = new TraceEventSession( _baseEtlFileName, _baseEtlFileName ); _userSession.CircularBufferMB = circularBufferMB; _userSession.CpuSampleIntervalMSec = cpuSampleIntervalMs; var kernelEvents = KernelTraceEventParser.Keywords.Profile | KernelTraceEventParser.Keywords.ContextSwitch | KernelTraceEventParser.Keywords.ImageLoad | KernelTraceEventParser.Keywords.Process | KernelTraceEventParser.Keywords.Thread; _kernelSession.EnableKernelProvider( kernelEvents, KernelTraceEventParser.Keywords.Profile ); Commands.Log( $"Kernel ETW session started. Outputting to {_kernelEtlFileName}." ); // Superluminal events _userSession.EnableProvider( "PerformanceAPI", TraceEventLevel.Verbose, ulong.MaxValue, userProviderOptions ); var jitEvents = ClrTraceEventParser.Keywords.JITSymbols | ClrTraceEventParser.Keywords.Exception | ClrTraceEventParser.Keywords.GC | ClrTraceEventParser.Keywords.GCHeapAndTypeNames | ClrTraceEventParser.Keywords.Interop | ClrTraceEventParser.Keywords.Binder | ClrTraceEventParser.Keywords.Stack; _userSession.EnableProvider( ClrTraceEventParser.ProviderGuid, TraceEventLevel.Verbose, (ulong)jitEvents, options: userProviderOptions ); Commands.Log( $"User ETW session started. Outputting to {_baseEtlFileName}." ); } private static void Shutdown() { try { // Stop the sessions and cleanup _kernelSession?.Stop(); _userSession?.Stop(); _kernelSession?.Dispose(); _userSession?.Dispose(); Commands.Log( "Sessions stopped." ); Commands.Log( "Triggering coreclr rundown..." ); var rundownProviderOptions = new TraceEventProviderOptions { StacksEnabled = true, }; rundownProviderOptions.ProcessIDFilter = new List { _targetPid }; using ( var rundownSession = new TraceEventSession( _rundownEtlFileName, _rundownEtlFileName ) ) { rundownSession.StopOnDispose = true; rundownSession.CircularBufferMB = 0; var keywords = (ulong)ClrRundownTraceEventParser.Keywords.ForceEndRundown + (ulong)ClrRundownTraceEventParser.Keywords.Jit + (ulong)ClrRundownTraceEventParser.Keywords.SupressNGen + (ulong)ClrRundownTraceEventParser.Keywords.JittedMethodILToNativeMap + (ulong)ClrRundownTraceEventParser.Keywords.Loader + (ulong)ClrRundownTraceEventParser.Keywords.Stack; rundownSession.EnableProvider( ClrRundownTraceEventParser.ProviderGuid, TraceEventLevel.Verbose, keywords, rundownProviderOptions ); // Poll until time goes by without growth. for ( var prevLength = new FileInfo( _rundownEtlFileName ).Length; ; ) { Thread.Sleep( 500 ); var newLength = new FileInfo( _rundownEtlFileName ).Length; if ( newLength == prevLength ) break; prevLength = newLength; } } Commands.Log( "coreclr rundown complete." ); Commands.Log( "Merging etl files, this may take a while..." ); TraceEventSession.MergeInPlace( _baseEtlFileName, null ); Commands.Log( "Merge complete." ); Commands.Log( "Processing to Firefox Profiler format..." ); var firefoxFile = SaveFirefoxProfile( _baseEtlFileName, _targetPid, _additionalSymbolPath ); var firefoxUrl = UploadFirefoxProfile( firefoxFile ); Commands.Log( "Profiler finished." ); var result = $"{Path.GetFullPath( _baseEtlFileName )};{Path.GetFullPath( firefoxFile )}"; if ( firefoxUrl != null ) result += $";{firefoxUrl}"; Commands.Finish( result ); } finally { // Signal the main thread to exit _shutdownEvent.Set(); } } private static string SaveFirefoxProfile( string etlFileName, int targetPid, string symbolPath ) { // Convert ETW data to Firefox profile format var profile = EtwConverterToFirefox.Convert( etlFileName, [targetPid], symbolPath ); // Determine output filename var etlFileNameWithoutExtension = Path.GetFileNameWithoutExtension( etlFileName ); var jsonFinalFileName = $"{Path.GetDirectoryName( etlFileName )}/{etlFileNameWithoutExtension}.json.gz"; // Save and compress the profile using ( var stream = File.Create( jsonFinalFileName ) ) using ( var gzipStream = new GZipStream( stream, CompressionLevel.Optimal ) ) { JsonSerializer.Serialize( gzipStream, profile, FirefoxProfiler.JsonProfilerContext.Default.Profile ); gzipStream.Flush(); } Commands.Log( $"Profile saved to {jsonFinalFileName}" ); return jsonFinalFileName; } private static string UploadFirefoxProfile( string firefoxProfileFile ) { if ( !_noUpload ) { try { // Upload the profile to Firefox Profiler Commands.Log( "Uploading profile to Firefox Profiler..." ); byte[] compressedData = File.ReadAllBytes( firefoxProfileFile ); // Run the upload asynchronously but wait for it to complete var uploadTask = UploadProfileAsync( compressedData ); uploadTask.Wait(); string jwtToken = uploadTask.Result; string profileToken = ExtractProfileToken( jwtToken ); string profileUrl = $"https://profiler.firefox.com/public/{profileToken}"; Commands.Log( $"Profile uploaded. Hosted at: {profileUrl}" ); return profileUrl; } catch ( Exception ex ) { Commands.Log( $"Error uploading profile: {ex.Message}" ); } } return null; } private static async Task UploadProfileAsync( byte[] compressedData ) { using ( var httpClient = new HttpClient() ) { httpClient.DefaultRequestHeaders.Accept.ParseAdd( "application/vnd.firefox-profiler+json;version=1.0" ); var content = new ByteArrayContent( compressedData ); var response = await httpClient.PostAsync( "https://api.profiler.firefox.com/compressed-store", content ); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } private static string ExtractProfileToken( string jwtToken ) { // Split the JWT token into its parts string[] parts = jwtToken.Split( '.' ); if ( parts.Length != 3 ) { throw new ArgumentException( "Invalid JWT token format" ); } // Get the payload part (second part) string payload = parts[1]; // Add padding if needed int padding = payload.Length % 4; if ( padding > 0 ) { payload += new string( '=', 4 - padding ); } // Decode the Base64Url encoded payload byte[] payloadBytes = Convert.FromBase64String( payload.Replace( '-', '+' ).Replace( '_', '/' ) ); string payloadJson = Encoding.UTF8.GetString( payloadBytes ); // Parse the JSON using ( JsonDocument doc = JsonDocument.Parse( payloadJson ) ) { if ( doc.RootElement.TryGetProperty( "profileToken", out JsonElement tokenElement ) ) { return tokenElement.GetString(); } else { throw new Exception( "Profile token not found in the response" ); } } } public static int Main() { Console.WriteLine( "Starting ETW profiler..." ); var args = Environment.GetCommandLineArgs(); if ( args.Length < 2 ) { Console.Error.WriteLine( "Missing pipe handle argument." ); return 1; } Console.WriteLine( $"Pipe handle: {args[1]}" ); Commands.OnResponse = ( string commandName, string contents ) => { Console.WriteLine( $"Received command: {commandName} {contents}" ); switch ( commandName ) { case "PID": Init( int.Parse( contents ) ); break; case "SHUTDOWN": Shutdown(); break; case "SYMBOLPATH": _additionalSymbolPath = contents; break; case "NOUPLOAD": _noUpload = true; break; } }; Commands.Init( args[1] ); // Wait for the shutdown signal Console.WriteLine( "Waiting for commands..." ); _shutdownEvent.Wait(); // Clean up resources Commands.Close(); _shutdownEvent.Dispose(); Console.WriteLine( "Exiting." ); return 0; } }