mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-05 21:08:29 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
402 lines
12 KiB
C#
402 lines
12 KiB
C#
using Facepunch.ActionGraphs;
|
|
using Sandbox.Internal;
|
|
using Sentry;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Reflection;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace Sandbox;
|
|
|
|
[SkipHotload]
|
|
internal class HotloadManager : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Assert if any one type takes more than this duration to process, AND more than
|
|
/// <see cref="PerTypeAssertThresholdFraction"/> of the total time.
|
|
/// </summary>
|
|
private const int PerTypeAssertThresholdMillis = 500;
|
|
|
|
/// <summary>
|
|
/// Assert if any one type takes more than this fraction of the total time to process,
|
|
/// AND more than <see cref="PerTypeAssertThresholdMillis"/>.
|
|
/// </summary>
|
|
private const float PerTypeAssertThresholdFraction = 0.8f;
|
|
|
|
static HotloadManager()
|
|
{
|
|
Hotload.AssemblyNameFormatter = FormatAssemblyName;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
OnSuccess = null;
|
|
|
|
//Hotload?.Dispose();
|
|
Hotload = null;
|
|
}
|
|
|
|
private static string FormatAssemblyName( AssemblyName name )
|
|
{
|
|
if ( name == null )
|
|
{
|
|
return "null";
|
|
}
|
|
|
|
if ( name.Name.StartsWith( "package.", StringComparison.OrdinalIgnoreCase ) )
|
|
{
|
|
return $"{name.Name} v{name.Version}";
|
|
}
|
|
|
|
return name.Name;
|
|
}
|
|
|
|
Logger log;
|
|
|
|
[ConVar( ConVarFlags.Protected, Min = 0, Max = 2, Help = "Hotload log level (0: none, 1: simple, 2: full)" )]
|
|
public static int hotload_log { get; set; }
|
|
|
|
[ConVar( "hotload_fast", ConVarFlags.Saved | ConVarFlags.Protected, Help = "Experimental fast hotloads if only method bodies change" )]
|
|
public static bool hotload_fast { get; set; } = true;
|
|
|
|
public string Name { get; }
|
|
|
|
public bool LoggingEnabled => hotload_log > 0;
|
|
public bool VerboseLoggingEnabled => hotload_log > 1;
|
|
|
|
public bool NeedsSwap { get; set; }
|
|
public Hotload Hotload { get; protected set; }
|
|
public event Action OnSuccess;
|
|
public event Action OnFail;
|
|
public event Action PreSwap;
|
|
|
|
public Mono.Cecil.IAssemblyResolver AssemblyResolver
|
|
{
|
|
get => Hotload.AssemblyResolver;
|
|
set => Hotload.AssemblyResolver = value;
|
|
}
|
|
|
|
public HotloadManager( string name = "Hotload" )
|
|
{
|
|
log = new Logger( $"hotload/{name}" );
|
|
Name = name;
|
|
|
|
Hotload = new Hotload( logger: log );
|
|
|
|
//
|
|
// This will skip types that it automatically decides are safe. This is
|
|
// just for performance, so if it seems over-zealous just comment out this
|
|
// line until it's fixed.
|
|
//
|
|
Hotload.AddUpgrader<Upgraders.AutoSkipUpgrader>();
|
|
|
|
//
|
|
// Sandbox.Reflection has some instance upgraders we need to add.
|
|
//
|
|
Hotload.AddUpgraders( typeof( TypeLibrary ).Assembly );
|
|
|
|
//
|
|
// During hotloads, recurse into the static fields of types in these assemblies to
|
|
// find instances to upgrade
|
|
//
|
|
Hotload.WatchAssembly( "Sandbox.Engine" );
|
|
Hotload.WatchAssembly( "Sandbox.System" );
|
|
Hotload.WatchAssembly( typeof( Sandbox.Internal.EventSystem ).Assembly );
|
|
|
|
//
|
|
// Ignore these assemblies because they can't hold anything
|
|
// that can be swappable.
|
|
//
|
|
Hotload.IgnoreAssembly<NLog.Logger>();
|
|
Hotload.IgnoreAssembly<Sentry.SentryStackFrame>();
|
|
Hotload.IgnoreAssembly<System.Net.Http.HttpClient>();
|
|
Hotload.IgnoreAssembly<System.Text.RegularExpressions.Regex>();
|
|
Hotload.IgnoreAssembly<LiteDB.LiteDatabase>();
|
|
Hotload.IgnoreAssembly<SkiaSharp.SKBitmap>();
|
|
Hotload.IgnoreAssembly<Microsoft.CodeAnalysis.Diagnostic>();
|
|
Hotload.IgnoreAssembly<Refit.PostAttribute>();
|
|
|
|
var skipUpgrader = Hotload.GetUpgrader<Upgraders.SkipUpgrader>();
|
|
|
|
//
|
|
// Skip types that reference things that often cause errors during hotload.
|
|
//
|
|
skipUpgrader.AddSkippedType<ExceptionDispatchInfo>();
|
|
skipUpgrader.AddSkippedType<NodeLibrary>();
|
|
skipUpgrader.AddSkippedType<NodeDefinition>();
|
|
|
|
//
|
|
// These are safe to skip for speed.
|
|
//
|
|
skipUpgrader.AddSkippedType<JsonNode>();
|
|
skipUpgrader.AddSkippedType<JsonElement>();
|
|
skipUpgrader.AddSkippedType<JsonObject>();
|
|
skipUpgrader.AddSkippedType<JsonArray>();
|
|
}
|
|
|
|
internal void Watch( Assembly assembly )
|
|
{
|
|
log.Trace( $"Watching {assembly}" );
|
|
Hotload.WatchAssembly( assembly );
|
|
}
|
|
|
|
internal void Ignore( Assembly gameAssembly )
|
|
{
|
|
if ( gameAssembly == null ) return;
|
|
|
|
log.Trace( $"Ignoring {gameAssembly}" );
|
|
Hotload.IgnoreAssembly( gameAssembly );
|
|
}
|
|
|
|
internal void Ignore( string asmName )
|
|
{
|
|
if ( asmName == null ) return;
|
|
|
|
log.Trace( $"Ignoring {asmName}" );
|
|
Hotload.IgnoreAssembly( asmName );
|
|
}
|
|
|
|
internal void Ignore<T>()
|
|
{
|
|
Ignore( typeof( T ).Assembly );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does the actual hotload
|
|
/// </summary>
|
|
public void DoSwap()
|
|
{
|
|
log.Trace( "DoSwap Start" );
|
|
MainThread.RunQueues(); // dispose of any threaded IDisposables waiting
|
|
|
|
var timer = Stopwatch.StartNew();
|
|
var eventRecord = new Api.Events.EventRecord( $"Hotload.{Name}" );
|
|
|
|
using var sentryScope = SentrySdk.PushScope();
|
|
|
|
lock ( Hotload )
|
|
{
|
|
using var _timer = eventRecord.ScopeTimer( "Time" );
|
|
|
|
NeedsSwap = false;
|
|
|
|
{
|
|
using var timerThreads = eventRecord.ScopeTimer( "PreSwap" );
|
|
PreSwap?.Invoke();
|
|
}
|
|
|
|
var workerThreadsStarted = WorkerThread.HasStarted;
|
|
|
|
// Need worker threads to stop while hotloading
|
|
{
|
|
using var timerThreads = eventRecord.ScopeTimer( "WorkerThreadStop" );
|
|
WorkerThread.Stop( 1000 );
|
|
}
|
|
|
|
var unloads = Hotload.GetOutgoingAssemblies();
|
|
|
|
foreach ( var assm in unloads )
|
|
{
|
|
log.Trace( $"Will Unload {assm}" );
|
|
}
|
|
|
|
Hotload.TracePaths = true;
|
|
Hotload.TraceRoots = VerboseLoggingEnabled;
|
|
Hotload.IncludeTypeTimings = VerboseLoggingEnabled;
|
|
Hotload.IncludeProcessorTimings = VerboseLoggingEnabled;
|
|
|
|
//
|
|
// Give hotload special info in sentry, searchable by group:hotload
|
|
//
|
|
SentrySdk.ConfigureScope( x =>
|
|
{
|
|
x.SetTag( "group", "hotload" );
|
|
|
|
x.Contexts["Hotload"] = new
|
|
{
|
|
HotloadManager = Name,
|
|
FastEnabled = hotload_fast
|
|
};
|
|
} );
|
|
|
|
try
|
|
{
|
|
HotloadResult info = null;
|
|
|
|
using ( var gr = new HeavyGarbageRegion() )
|
|
using ( var t = eventRecord.ScopeTimer( "UpdateReferences" ) )
|
|
{
|
|
|
|
info = Hotload.UpdateReferences();
|
|
}
|
|
|
|
if ( info.NoAction )
|
|
{
|
|
eventRecord = null; // don't bother with the event
|
|
return;
|
|
}
|
|
|
|
// stats
|
|
// list of assembies that are hotloading? count of assemblies?
|
|
eventRecord.SetValue( "InstancesProcessed", info.InstancesProcessed );
|
|
eventRecord.SetValue( "ProcessingTime", info.ProcessingTime );
|
|
eventRecord.SetValue( "InstanceQueueTime", info.InstanceQueueTime );
|
|
eventRecord.SetValue( "StaticFieldTime", info.StaticFieldTime );
|
|
eventRecord.SetValue( "WatchedInstanceTime", info.WatchedInstanceTime );
|
|
eventRecord.SetValue( "DiagnosticsTime", info.DiagnosticsTime );
|
|
eventRecord.SetValue( "Success", info.Success );
|
|
|
|
foreach ( var (typeName, entry) in info.TypeTimings )
|
|
{
|
|
var fraction = entry.Milliseconds / info.ProcessingTime;
|
|
|
|
if ( fraction >= PerTypeAssertThresholdFraction && entry.Milliseconds >= PerTypeAssertThresholdMillis )
|
|
{
|
|
Log.Error( new Exception( $"Type {typeName} is taking too long to hotload" ), $"Type {typeName} is taking too long to hotload\n Processing time: {entry.Milliseconds:F1}ms, {entry.Milliseconds * 100d / info.ProcessingTime:F1}% of the total" );
|
|
}
|
|
}
|
|
|
|
if ( info.InstancesProcessed > 0 && LoggingEnabled )
|
|
{
|
|
log.Info( $"{Name} processed {info.InstancesProcessed:n0} instances in {(info.ProcessingTime / 1000):0.00}s" );
|
|
|
|
if ( VerboseLoggingEnabled )
|
|
{
|
|
log.Info( $" Instance queue: {info.InstanceQueueTime:0.0}ms" );
|
|
log.Info( $" Diagnostics: {info.DiagnosticsTime:0.0}ms" );
|
|
log.Info( $" Static fields: {info.StaticFieldTime:0.0}ms" );
|
|
log.Info( $"Watched instances: {info.WatchedInstanceTime:0.0}ms" );
|
|
|
|
LogVerboseTimingInfo( info.TypeTimings );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Trace( $"processed {info.InstancesProcessed:n0} instances in {(info.ProcessingTime / 1000):0.00}s" );
|
|
}
|
|
|
|
{
|
|
using var timerSuccess = eventRecord.ScopeTimer( "OnSuccess" );
|
|
OnSuccess?.Invoke();
|
|
}
|
|
}
|
|
catch ( System.Exception e )
|
|
{
|
|
log.Warning( e, "Hotload exception" );
|
|
Sentry.SentrySdk.CaptureException( e );
|
|
|
|
OnFail?.Invoke();
|
|
}
|
|
finally
|
|
{
|
|
if ( workerThreadsStarted )
|
|
{
|
|
using var timerThreads = eventRecord?.ScopeTimer( "WorkerThreadStart" );
|
|
// Resume worker threads
|
|
WorkerThread.Start();
|
|
}
|
|
|
|
eventRecord?.Submit();
|
|
log.Trace( "DoSwap Finished" );
|
|
|
|
CodeIterate.Hint( $"{Name}Hotload", timer.Elapsed.TotalMilliseconds );
|
|
|
|
if ( Name == "Client" )
|
|
{
|
|
CodeIterate.Finish();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string FormatMessageGroup( string message, IEnumerable<HotloadResultEntry> entries )
|
|
{
|
|
var countString = entries.Count() == 1 ? "" : $" (x{entries.Count()})";
|
|
|
|
return $"{message}{countString}{Environment.NewLine} {string.Join( $"{Environment.NewLine}{Environment.NewLine} ", entries.Select( x => x.Context?.Replace( "\n", "\n " ) ) )}";
|
|
}
|
|
|
|
private static string EscapeForLog( string value )
|
|
{
|
|
return value
|
|
.Replace( "<", "[" )
|
|
.Replace( ">", "]" );
|
|
}
|
|
|
|
private void LogVerboseTimingInfo( Dictionary<string, InstanceTimingEntry> entries )
|
|
{
|
|
const int maxEntries = 20;
|
|
const int maxRoots = 10;
|
|
|
|
var timingEntries = entries
|
|
.OrderByDescending( x => x.Value.Milliseconds )
|
|
.Take( maxEntries )
|
|
.Select( x => new
|
|
{
|
|
x.Key,
|
|
x.Value,
|
|
Roots = x.Value.Roots
|
|
.OrderByDescending( y => y.Value.Milliseconds )
|
|
.Take( maxRoots )
|
|
.ToArray()
|
|
} )
|
|
.ToArray();
|
|
|
|
var nameWidth = timingEntries.Max( x => Math.Max( x.Key.Length + 2, x.Roots.Max( y => y.Key.Length ) + 2 ) );
|
|
var instancesWidth = timingEntries.Max( x => Math.Max( x.Value.Instances, x.Roots.Max( y => y.Value.Instances ) ) ).ToString().Length;
|
|
var timeWidth = timingEntries.Max( x => Math.Max( x.Value.Milliseconds, x.Roots.Max( y => y.Value.Milliseconds ) ) ).ToString( "0.0" ).Length;
|
|
var totalWidth = nameWidth + instancesWidth + timeWidth + 12;
|
|
var hdiv = new string( '-', totalWidth );
|
|
|
|
log.Info( hdiv );
|
|
|
|
foreach ( var timingEntry in timingEntries )
|
|
{
|
|
log.Info( $"| {EscapeForLog( timingEntry.Key.PadRight( nameWidth ) )} " +
|
|
$"| {timingEntry.Value.Instances.ToString().PadLeft( instancesWidth )} " +
|
|
$"| {timingEntry.Value.Milliseconds.ToString( "0.0" ).PadLeft( timeWidth )}ms |" );
|
|
|
|
if ( timingEntry.Value.Roots == null ) continue;
|
|
|
|
foreach ( var rootEntry in timingEntry.Roots )
|
|
{
|
|
log.Info( $"| {EscapeForLog( rootEntry.Key.PadRight( nameWidth - 2 ) )} " +
|
|
$"| {rootEntry.Value.Instances.ToString().PadLeft( instancesWidth )} " +
|
|
$"| {rootEntry.Value.Milliseconds.ToString( "0.0" ).PadLeft( timeWidth )}ms |" );
|
|
}
|
|
|
|
if ( timingEntry.Value.Roots.Count > maxRoots )
|
|
{
|
|
log.Info( $"| ... and {timingEntry.Value.Roots.Count - maxRoots} other roots".PadRight( totalWidth - 1 ) + "|" );
|
|
}
|
|
|
|
log.Info( hdiv );
|
|
}
|
|
|
|
if ( entries.Count > maxEntries )
|
|
{
|
|
log.Info( $"| ... and {entries.Count - maxEntries} other types".PadRight( totalWidth - 1 ) + "|" );
|
|
log.Info( hdiv );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lets the hotload system know that something has changed. If we detect that we are
|
|
/// replacing a dll (instead of just adding one) we'll queue up a swap. We should still
|
|
/// call this even if <paramref name="oldAssembly"/> or <paramref name="newAssembly"/>
|
|
/// is null, since that will tell the hotload system to start / stop watching static
|
|
/// members of the assembly.
|
|
/// </summary>
|
|
internal void Replace( [AllowNull] Assembly oldAssembly, [AllowNull] Assembly newAssembly )
|
|
{
|
|
log.Trace( $"Replace {FormatAssemblyName( oldAssembly?.GetName() )} with {FormatAssemblyName( newAssembly?.GetName() )}" );
|
|
|
|
if ( Hotload.ReplacingAssembly( oldAssembly, newAssembly ) )
|
|
{
|
|
NeedsSwap = true;
|
|
}
|
|
}
|
|
}
|