Files
sbox-public/engine/Launcher/SboxProfiler/Program.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

313 lines
9.4 KiB
C#

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<int>
{
_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<int>
{
_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<string> 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;
}
}