mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-04 20:38:24 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
313 lines
9.4 KiB
C#
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;
|
|
}
|
|
}
|