diff --git a/Directory.Packages.props b/Directory.Packages.props index 9e2d5a099a..5a7d08bce0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,8 @@ - - - - - + + + + + + \ No newline at end of file diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 20aacd3428..88f8dfb49a 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -81,15 +81,17 @@ public void StartWorld() { Initialized = true; InitializedTick = _gameTiming.CurTick; - // Call global with waitfor=FALSE - _objectTree.GlobalInitProc?.Spawn(WorldInstance, new()); + using (Profiler.BeginZone("StartWorld", color:(uint)Color.OrangeRed.ToArgb())) { + // Call global with waitfor=FALSE + _objectTree.GlobalInitProc?.Spawn(WorldInstance, new()); - // Call New() on all /area and /turf that exist, each with waitfor=FALSE separately. If created any /area, call New a SECOND TIME - // new() up /objs and /mobs from compiled-in maps [order: (1,1) then (2,1) then (1,2) then (2,2)] - _dreamMapManager.InitializeAtoms(_compiledJson.Maps); + // Call New() on all /area and /turf that exist, each with waitfor=FALSE separately. If created any /area, call New a SECOND TIME + // new() up /objs and /mobs from compiled-in maps [order: (1,1) then (2,1) then (1,2) then (2,2)] + _dreamMapManager.InitializeAtoms(_compiledJson.Maps); - // Call world.New() - WorldInstance.SpawnProc("New"); + // Call world.New() + WorldInstance.SpawnProc("New"); + } } public void Shutdown() { @@ -102,13 +104,26 @@ public void Shutdown() { public void Update() { if (!Initialized) return; + using (Profiler.BeginZone("Tick", color:(uint)Color.OrangeRed.ToArgb())) + { + using (Profiler.BeginZone("DM Execution", color:(uint)Color.LightPink.ToArgb())) + _procScheduler.Process(); + + using (Profiler.BeginZone("Map Update", color:(uint)Color.LightPink.ToArgb())){ + UpdateStat(); + _dreamMapManager.UpdateTiles(); + } + + using (Profiler.BeginZone("Disk IO", color:(uint)Color.LightPink.ToArgb())) + DreamObjectSavefile.FlushAllUpdates(); + + WorldInstance.SetVariableValue("cpu", WorldInstance.GetVariable("tick_usage")); + + using (Profiler.BeginZone("Deletion Queue", color:(uint)Color.LightPink.ToArgb())) + ProcessDelQueue(); + } - _procScheduler.Process(); - UpdateStat(); - _dreamMapManager.UpdateTiles(); - DreamObjectSavefile.FlushAllUpdates(); - WorldInstance.SetVariableValue("cpu", WorldInstance.GetVariable("tick_usage")); - ProcessDelQueue(); + Profiler.EmitFrameMark(); } public void ProcessDelQueue() { diff --git a/OpenDreamRuntime/DreamThread.cs b/OpenDreamRuntime/DreamThread.cs index c9c9212084..b0cd8906b4 100644 --- a/OpenDreamRuntime/DreamThread.cs +++ b/OpenDreamRuntime/DreamThread.cs @@ -137,7 +137,10 @@ public DMError(string message) public abstract class ProcState : IDisposable { private static int _idCounter = 0; public int Id { get; } = ++_idCounter; - + #if TOOLS + public abstract (string SourceFile, int Line) TracyLocationId { get; } + public ProfilerZone? TracyZoneId { get; set; } + #endif public DreamThread Thread { get; set; } [Access(typeof(ProcScheduler))] @@ -210,7 +213,10 @@ public static DreamValue Run(DreamProc proc, DreamObject src, DreamObject? usr, var context = new DreamThread(proc.ToString()); if (proc is NativeProc nativeProc) { - return nativeProc.Call(context, src, usr, new(arguments)); + // ReSharper disable ExplicitCallerInfoArgument + using(Profiler.BeginZone(filePath:"Native Proc", lineNumber:0, memberName:nativeProc.Name)) + return nativeProc.Call(context, src, usr, new(arguments)); + // ReSharper restore ExplicitCallerInfoArgument } var state = proc.CreateState(context, src, usr, new DreamProcArguments(arguments)); @@ -252,6 +258,15 @@ public DreamValue ReentrantResume(ProcState? untilState, out ProcStatus resultSt while (_current != null) { ProcStatus status; try { + #if TOOLS + if (_current.TracyZoneId is null && _current.Proc != null) { + var location =_current.TracyLocationId; + var procpath = (_current.Proc.OwningType.Path.Equals("/") ? "/proc/" : _current.Proc.OwningType.Path+"/") +_current.Proc.Name; + // ReSharper disable ExplicitCallerInfoArgument + _current.TracyZoneId = Profiler.BeginZone(filePath: location.SourceFile, lineNumber: location.Line, memberName: procpath); + // ReSharper restore ExplicitCallerInfoArgument + } + #endif // _current.Resume may mutate our state!!! status = _current.Resume(); } catch (DMError dmError) { @@ -274,8 +289,25 @@ public DreamValue ReentrantResume(ProcState? untilState, out ProcStatus resultSt switch (status) { // The entire Thread is stopping case ProcStatus.Cancelled: + #if TOOLS + if (_current.TracyZoneId is not null) { + _current.TracyZoneId.Value.Dispose(); + _current.TracyZoneId = null; + } + #endif + var current = _current; _current = null; + + #if TOOLS + foreach (var s in _stack) { + if (s.TracyZoneId is null) + continue; + s.TracyZoneId.Value.Dispose(); + s.TracyZoneId = null; + } + #endif + _stack.Clear(); resultStatus = status; return current.Result; @@ -298,6 +330,19 @@ public DreamValue ReentrantResume(ProcState? untilState, out ProcStatus resultSt // The context is done executing for now case ProcStatus.Deferred: + #if TOOLS + if (_current.TracyZoneId is not null) { + _current.TracyZoneId.Value.Dispose(); + _current.TracyZoneId = null; + } + + foreach (var s in _stack) { + if (s.TracyZoneId is null) + continue; + s.TracyZoneId.Value.Dispose(); + s.TracyZoneId = null; + } + #endif // We return the current return value here even though it may not be the final result resultStatus = status; return _current.Result; @@ -334,6 +379,13 @@ public void PushProcState(ProcState state) { } public void PopProcState(bool dispose = true) { + #if TOOLS + if (_current?.TracyZoneId is not null) { + _current.TracyZoneId.Value.Dispose(); + _current.TracyZoneId = null; + } + #endif + if (_current?.WaitFor == false) { _syncCount--; } diff --git a/OpenDreamRuntime/DreamValue.cs b/OpenDreamRuntime/DreamValue.cs index 99102d0b6c..fd109f7dee 100644 --- a/OpenDreamRuntime/DreamValue.cs +++ b/OpenDreamRuntime/DreamValue.cs @@ -66,9 +66,16 @@ public static DreamValue False { private object? _refValue; private readonly float _floatValue; + #if TOOLS + //ReSharper disable once NotAccessedField.Local + private readonly ProfilerMemory? _tracyMemoryId; //only used for strings, since everything else is a value type or handled in DreamObject + #endif public DreamValue(string value) { DebugTools.Assert(value != null); Type = DreamValueType.String; + #if TOOLS + _tracyMemoryId = Profiler.BeginMemoryZone((ulong) (1+value.Length*sizeof(char)), "string"); + #endif _refValue = value; } diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index e797e5151d..967f2c3b49 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -2,7 +2,6 @@ using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs.DebugAdapter; using OpenDreamShared; -using Robust.Server.ServerStatus; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs index cfa73be66b..48da289f0c 100644 --- a/OpenDreamRuntime/Objects/DreamObject.cs +++ b/OpenDreamRuntime/Objects/DreamObject.cs @@ -17,6 +17,9 @@ namespace OpenDreamRuntime.Objects { [Virtual] public class DreamObject { + #if TOOLS + protected ProfilerMemory? _tracyMemoryId; + #endif public DreamObjectDefinition ObjectDefinition; [Access(typeof(DreamObject))] @@ -87,6 +90,10 @@ public DreamObject(DreamObjectDefinition objectDefinition) { if (this is not DreamObjectAtom && IsSubtypeOf(ObjectTree.Datum)) { ObjectDefinition.DreamManager.Datums.AddLast(new WeakDreamRef(this)); } + #if TOOLS + //if it's not null, subclasses have done their own allocation + _tracyMemoryId ??= Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + ObjectDefinition.Variables.Count * Unsafe.SizeOf() ), "/datum"); + #endif } public virtual void Initialize(DreamProcArguments args) { @@ -106,6 +113,9 @@ protected virtual void HandleDeletion(bool possiblyThreaded) { Variables = null; ObjectDefinition = null!; + #if TOOLS + _tracyMemoryId?.ReleaseMemory(); + #endif } /// @@ -145,6 +155,7 @@ public void Delete(bool possiblyThreaded = false) { } public bool IsSubtypeOf(TreeEntry ancestor) { + if(Deleted) return false; return ObjectDefinition.IsSubtypeOf(ancestor); } diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index f2492129f4..9f8345d200 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -139,42 +139,44 @@ public IEnumerable GetAllDescendants(TreeEntry treeEntry) { /// (by calling the result of or ) /// public DreamObject CreateObject(TreeEntry type) { - if (type == List) - return CreateList(); - if (type == Savefile) - return new DreamObjectSavefile(Savefile.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(DatabaseQuery)) - return new DreamObjectDatabaseQuery(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Database)) - return new DreamObjectDatabase(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Matrix)) - return new DreamObjectMatrix(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Sound)) - return new DreamObjectSound(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Regex)) - return new DreamObjectRegex(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Image)) - return new DreamObjectImage(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Icon)) - return new DreamObjectIcon(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Filter)) - return new DreamObjectFilter(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Mob)) - return new DreamObjectMob(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Movable)) - return new DreamObjectMovable(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Area)) - return new DreamObjectArea(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Atom)) - return new DreamObjectAtom(type.ObjectDefinition); - if (type.ObjectDefinition.IsSubtypeOf(Client)) - throw new Exception("Cannot create objects of type /client"); - if (type.ObjectDefinition.IsSubtypeOf(Turf)) - throw new Exception("New turfs must be created by the map manager"); - if (type.ObjectDefinition.IsSubtypeOf(Exception)) - return new DreamObjectException(type.ObjectDefinition); - - return new DreamObject(type.ObjectDefinition); + using(Profiler.BeginZone($"new {type}")){ + if (type == List) + return CreateList(); + if (type == Savefile) + return new DreamObjectSavefile(Savefile.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(DatabaseQuery)) + return new DreamObjectDatabaseQuery(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Database)) + return new DreamObjectDatabase(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Matrix)) + return new DreamObjectMatrix(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Sound)) + return new DreamObjectSound(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Regex)) + return new DreamObjectRegex(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Image)) + return new DreamObjectImage(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Icon)) + return new DreamObjectIcon(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Filter)) + return new DreamObjectFilter(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Mob)) + return new DreamObjectMob(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Movable)) + return new DreamObjectMovable(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Area)) + return new DreamObjectArea(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Atom)) + return new DreamObjectAtom(type.ObjectDefinition); + if (type.ObjectDefinition.IsSubtypeOf(Client)) + throw new Exception("Cannot create objects of type /client"); + if (type.ObjectDefinition.IsSubtypeOf(Turf)) + throw new Exception("New turfs must be created by the map manager"); + if (type.ObjectDefinition.IsSubtypeOf(Exception)) + return new DreamObjectException(type.ObjectDefinition); + + return new DreamObject(type.ObjectDefinition); + } } public T CreateObject(TreeEntry type) where T : DreamObject { diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index 793886360b..898bafe9ca 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -13,7 +13,9 @@ namespace OpenDreamRuntime.Objects.Types; public class DreamList : DreamObject { private readonly List _values; private Dictionary? _associativeValues; - + #if TOOLS + private ProfilerMemory? _tracyContentsMemoryId; + #endif public override bool ShouldCallNew => false; public virtual bool IsAssociative => (_associativeValues != null && _associativeValues.Count > 0); @@ -28,6 +30,10 @@ public DreamList(DreamObjectDefinition listDef, int size) : base(listDef) { public DreamList(DreamObjectDefinition listDef, List values, Dictionary? associativeValues) : base(listDef) { _values = values; _associativeValues = associativeValues; + #if TOOLS + _tracyMemoryId = Profiler.BeginMemoryZone(1, "/list instance"); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } public override void Initialize(DreamProcArguments args) { @@ -127,6 +133,10 @@ public virtual void SetValue(DreamValue key, DreamValue value, bool allowGrowth _associativeValues ??= new Dictionary(1); _associativeValues[key] = value; } + #if TOOLS + _tracyContentsMemoryId?.ReleaseMemory(); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } public virtual void RemoveValue(DreamValue value) { @@ -136,10 +146,18 @@ public virtual void RemoveValue(DreamValue value) { _associativeValues?.Remove(value); _values.RemoveAt(valueIndex); } + #if TOOLS + _tracyContentsMemoryId?.ReleaseMemory(); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } public virtual void AddValue(DreamValue value) { _values.Add(value); + #if TOOLS + _tracyContentsMemoryId?.ReleaseMemory(); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } //Does not include associations @@ -176,10 +194,18 @@ public virtual void Cut(int start = 1, int end = 0) { if (end > start) _values.RemoveRange(start - 1, end - start); + #if TOOLS + _tracyContentsMemoryId?.ReleaseMemory(); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } public void Insert(int index, DreamValue value) { _values.Insert(index - 1, value); + #if TOOLS + _tracyContentsMemoryId?.ReleaseMemory(); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } public void Swap(int index1, int index2) { @@ -199,6 +225,10 @@ public void Resize(int size) { } else { Cut(size + 1); } + #if TOOLS + _tracyContentsMemoryId?.ReleaseMemory(); + _tracyContentsMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + _values.Count * Unsafe.SizeOf() + (_associativeValues?.Count ?? 0) * Unsafe.SizeOf()), "/list contents"); + #endif } public virtual int GetLength() { diff --git a/OpenDreamRuntime/OpenDreamRuntime.csproj b/OpenDreamRuntime/OpenDreamRuntime.csproj index e6858df7e9..bf74ac73dd 100644 --- a/OpenDreamRuntime/OpenDreamRuntime.csproj +++ b/OpenDreamRuntime/OpenDreamRuntime.csproj @@ -14,6 +14,7 @@ + diff --git a/OpenDreamRuntime/Procs/AsyncNativeProc.cs b/OpenDreamRuntime/Procs/AsyncNativeProc.cs index 772921408f..ef0daf21d4 100644 --- a/OpenDreamRuntime/Procs/AsyncNativeProc.cs +++ b/OpenDreamRuntime/Procs/AsyncNativeProc.cs @@ -25,7 +25,9 @@ public sealed class State : ProcState { private AsyncNativeProc? _proc; public override DreamProc? Proc => _proc; - + #if TOOLS + public override (string SourceFile, int Line) TracyLocationId => ("Async Native Proc", 0); + #endif private Func> _taskFunc; private Task? _task; diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index 81f869c924..b5ad9fcc8f 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -50,6 +50,8 @@ public DMProc(int id, TreeEntry owningType, ProcDefinitionJson json, string? nam } public (string Source, int Line) GetSourceAtOffset(int offset) { + if(SourceInfo.Count == 0) + return ("",0); SourceInfoJson current = SourceInfo[0]; string source = ObjectTree.Strings[current.File!.Value]; @@ -150,7 +152,9 @@ public sealed class NullProcState : ProcState { public static readonly Stack Pool = new(); public override DreamProc? Proc => _proc; - + #if TOOLS + public override (string SourceFile, int Line) TracyLocationId => ("",0); + #endif private DreamProc? _proc; public override ProcStatus Resume() { @@ -354,6 +358,9 @@ public sealed class DMProcState : ProcState { private DMProc _proc; public override DMProc Proc => _proc; + #if TOOLS + public override (string SourceFile, int Line) TracyLocationId => _proc.GetSourceAtOffset(_pc+1); + #endif /// Static initializer for maintainer friendly OpcodeHandlers to performance friendly _opcodeHandlers static unsafe DMProcState() { @@ -482,7 +489,10 @@ public void SetReturn(DreamValue value) { public ProcStatus Call(DreamProc proc, DreamObject? src, DreamProcArguments arguments) { if (proc is NativeProc p) { // Skip a whole song and dance. - Push(p.Call(Thread, src, Usr, arguments)); + // ReSharper disable ExplicitCallerInfoArgument + using(Profiler.BeginZone(filePath:"Native Proc", lineNumber:0, memberName:p.Name)) + Push(p.Call(Thread, src, Usr, arguments)); + // ReSharper restore ExplicitCallerInfoArgument return ProcStatus.Continue; } diff --git a/OpenDreamRuntime/Procs/InitDreamObject.cs b/OpenDreamRuntime/Procs/InitDreamObject.cs index e80c67c5b4..3a419780c9 100644 --- a/OpenDreamRuntime/Procs/InitDreamObject.cs +++ b/OpenDreamRuntime/Procs/InitDreamObject.cs @@ -9,7 +9,7 @@ internal sealed class InitDreamObjectState : ProcState { private readonly DreamObjectTree _objectTree; private enum Stage { - // Need to call the object's (init) proc + // Need to call the object's (init) proc Init, // Need to call IDreamMetaObject.OnObjectCreated & New @@ -41,7 +41,9 @@ public void Initialize(DreamThread thread, DreamObject dreamObject, DreamObject? private Stage _stage = Stage.Init; public override DreamProc? Proc => null; - + #if TOOLS + public override (string SourceFile, int Line) TracyLocationId => ($"new {_dreamObject.ObjectDefinition.Type}",0); + #endif public override void AppendStackFrame(StringBuilder builder) { builder.AppendLine($"new {_dreamObject.ObjectDefinition.Type}"); } diff --git a/OpenDreamRuntime/Profile.cs b/OpenDreamRuntime/Profile.cs new file mode 100644 index 0000000000..d41292b365 --- /dev/null +++ b/OpenDreamRuntime/Profile.cs @@ -0,0 +1,249 @@ +using System.Runtime.CompilerServices; +using System.Threading; +using bottlenoselabs.C2CS.Runtime; +using static Tracy.PInvoke; + +namespace OpenDreamRuntime; + +public static class Profiler{ + //internal tracking for unique IDs for memory zones, because we can't use actual object pointers sadly as they are unstable outside of `unsafe` + private static ulong _memoryUid; + + // Plot names need to be cached for the lifetime of the program + // seealso Tracy docs section 3.1 + private static readonly Dictionary PlotNameCache = new(); + + /// + /// Begins a new and returns the handle to that zone. Time + /// spent inside a zone is calculated by Tracy and shown in the profiler. A zone is + /// ended when is called either automatically via + /// disposal scope rules or by calling it manually. + /// + /// A custom name for this zone. + /// Is the zone active. An inactive zone wont be shown in the profiler. + /// An RRGGBB color code that Tracy will use to color the zone in the profiler. + /// Arbitrary text associated with this zone. + /// + /// The source code line number that this zone begins at. + /// If this param is not explicitly assigned the value will provided by . + /// + /// + /// The source code file path that this zone begins at. + /// If this param is not explicitly assigned the value will provided by . + /// + /// + /// The source code member name that this zone begins at. + /// If this param is not explicitly assigned the value will provided by . + /// + /// + public static ProfilerZone? BeginZone( + string? zoneName = null, + bool active = true, + uint color = 0, + string? text = null, + [CallerLineNumber] int lineNumber = 0, + [CallerFilePath] string? filePath = null, + [CallerMemberName] string? memberName = null) + { + #if !TOOLS + return null; + #else + + using var filestr = GetCString(filePath, out var fileln); + using var memberstr = GetCString(memberName, out var memberln); + using var namestr = GetCString(zoneName, out var nameln); + var srcLocId = TracyAllocSrclocName((uint)lineNumber, filestr, fileln, memberstr, memberln, namestr, nameln, color); + var context = TracyEmitZoneBeginAlloc(srcLocId, active ? 1 : 0); + + if (text != null) + { + using var textstr = GetCString(text, out var textln); + TracyEmitZoneText(context, textstr, textln); + } + + return new ProfilerZone(context); + #endif + } + + public static ProfilerMemory? BeginMemoryZone(ulong size, string? name) + { + #if !TOOLS + return null; + #else + var namestr = name is null ? GetPlotCString("null") : GetPlotCString(name); + unsafe { + return new ProfilerMemory((void*)(Interlocked.Add(ref _memoryUid, size)-size), size, namestr); + } + #endif + } + + /// + /// Configure how Tracy will display plotted values. + /// + /// + /// Name of the plot to configure. Each represents a unique plot. + /// + /// + /// Changes how the values in the plot are presented by the profiler. + /// + /// + /// Determines whether the plot will be displayed as a staircase or will smoothly change between plot points + /// + /// + /// If the the area below the plot will not be filled with a solid color. + /// + /// + /// An RRGGBB color code that Tracy will use to color the plot in the profiler. + /// + public static void PlotConfig(string name, PlotType type = PlotType.Number, bool step = false, bool fill = true, uint color = 0){ + var namestr = GetPlotCString(name); + TracyEmitPlotConfig(namestr, (int)type, step ? 1 : 0, fill ? 1 : 0, color); + } + + /// + /// Add a value to a plot. + /// + public static void Plot(string name, double val){ + var namestr = GetPlotCString(name); + TracyEmitPlot(namestr, val); + } + + /// + /// Add a value to a plot. + /// + public static void Plot(string name, int val){ + var namestr = GetPlotCString(name); + TracyEmitPlotInt(namestr, val); + } + + /// + /// Add a value to a plot. + /// + public static void Plot(string name, float val){ + var namestr = GetPlotCString(name); + TracyEmitPlotFloat(namestr, val); + } + + private static CString GetPlotCString(string name){ + if(!PlotNameCache.TryGetValue(name, out var plotCString)) + { + plotCString = CString.FromString(name); + PlotNameCache.Add(name, plotCString); + } + + return plotCString; + } + + /// + /// Emit a string that will be included along with the trace description. + /// + /// + /// Viewable in the Info tab in the profiler. + /// + public static void AppInfo(string appInfo){ + using var infostr = GetCString(appInfo, out var infoln); + TracyEmitMessageAppinfo(infostr, infoln); + } + + /// + /// Emit the top-level frame marker. + /// + /// + /// Tracy Cpp API and docs refer to this as the FrameMark macro. + /// + public static void EmitFrameMark(){ + TracyEmitFrameMark(null); + } + + /// + /// Is the app connected to the external profiler? + /// + /// + public static bool IsConnected(){ + return TracyConnected() != 0; + } + + /// + /// Creates a for use by Tracy. Also returns the + /// length of the string for interop convenience. + /// + public static CString GetCString(string? fromString, out ulong clength){ + if (fromString == null) + { + clength = 0; + return new CString(0); + } + + clength = (ulong)fromString.Length; + return CString.FromString(fromString); + } + + public enum PlotType{ + /// + /// Values will be displayed as plain numbers. + /// + Number = 0, + + /// + /// Treats the values as memory sizes. Will display kilobytes, megabytes, etc. + /// + Memory = 1, + + /// + /// Values will be displayed as percentage (with value 100 being equal to 100%). + /// + Percentage = 2, + } +} + +public readonly struct ProfilerZone : IDisposable{ + public readonly TracyCZoneCtx Context; + + public uint Id => Context.Data.Id; + + public int Active => Context.Data.Active; + + internal ProfilerZone(TracyCZoneCtx context){ + Context = context; + } + + public void EmitName(string name){ + using var namestr = Profiler.GetCString(name, out var nameln); + TracyEmitZoneName(Context, namestr, nameln); + } + + public void EmitColor(uint color){ + TracyEmitZoneColor(Context, color); + } + + public void EmitText(string text){ + using var textstr = Profiler.GetCString(text, out var textln); + TracyEmitZoneText(Context, textstr, textln); + } + + public void Dispose(){ + TracyEmitZoneEnd(Context); + } +} + +public sealed unsafe class ProfilerMemory { + private readonly void* _ptr; + private readonly CString _name; + private int _hasRun; + + internal ProfilerMemory(void* pointer, ulong size, CString name){ + _ptr = pointer; + _name = name; + TracyEmitMemoryAllocNamed(_ptr, size, 0, _name); + } + + public void ReleaseMemory(){ + if (Interlocked.Exchange(ref _hasRun, 1) == 0){ // only run once + TracyEmitMemoryFreeNamed(_ptr, 0, _name); + } + } + + ~ProfilerMemory(){ + ReleaseMemory(); + } +} diff --git a/OpenDreamRuntime/Resources/DreamResource.cs b/OpenDreamRuntime/Resources/DreamResource.cs index 44ad6a5a94..df3d85aed5 100644 --- a/OpenDreamRuntime/Resources/DreamResource.cs +++ b/OpenDreamRuntime/Resources/DreamResource.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Runtime.CompilerServices; using System.Text; namespace OpenDreamRuntime.Resources; @@ -19,16 +20,25 @@ public byte[]? ResourceData { private readonly string? _filePath; private byte[]? _resourceData; + #if TOOLS + private ProfilerMemory? _tracyMemoryId; + #endif public DreamResource(int id, string? filePath, string? resourcePath) { Id = id; ResourcePath = resourcePath; _filePath = filePath; + #if TOOLS + _tracyMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + (ResourceData?.Length ?? 0)), "resource"); + #endif } public DreamResource(int id, byte[] data) { Id = id; _resourceData = data; + #if TOOLS + _tracyMemoryId = Profiler.BeginMemoryZone((ulong)(Unsafe.SizeOf() + (ResourceData is null? 0 : ResourceData.Length)), "resource"); + #endif } public virtual string? ReadAsString() { diff --git a/OpenDreamShared/OpenDreamCVars.cs b/OpenDreamShared/OpenDreamCVars.cs index cc87102218..99acef2fc3 100644 --- a/OpenDreamShared/OpenDreamCVars.cs +++ b/OpenDreamShared/OpenDreamCVars.cs @@ -1,62 +1,63 @@ using System; using Robust.Shared.Configuration; -namespace OpenDreamShared { - [CVarDefs] - public abstract class OpenDreamCVars { - public static readonly CVarDef JsonPath = - CVarDef.Create("opendream.json_path", String.Empty, CVar.SERVERONLY); - - public static readonly CVarDef DownloadTimeout = - CVarDef.Create("opendream.download_timeout", 30, CVar.CLIENTONLY); - - public static readonly CVarDef AlwaysShowExceptions = - CVarDef.Create("opendream.always_show_exceptions", false, CVar.SERVERONLY); - - public static readonly CVarDef DebugAdapterLaunched = - CVarDef.Create("opendream.debug_adapter_launched", 0, CVar.SERVERONLY); - - public static readonly CVarDef SpoofIEUserAgent = - CVarDef.Create("opendream.spoof_ie_user_agent", true, CVar.CLIENTONLY); - - public static readonly CVarDef WorldParams = - CVarDef.Create("opendream.world_params", string.Empty, CVar.SERVERONLY); - - public static readonly CVarDef TopicPort = - CVarDef.Create("opendream.topic_port", 25567, CVar.SERVERONLY); - - /* - * INFOLINKS - */ - - /// - /// Link to Discord server to show in the launcher. - /// - public static readonly CVarDef InfoLinksDiscord = - CVarDef.Create("infolinks.discord", "", CVar.SERVER | CVar.REPLICATED); - - /// - /// Link to forum to show in the launcher. - /// - public static readonly CVarDef InfoLinksForum = - CVarDef.Create("infolinks.forum", "", CVar.SERVER | CVar.REPLICATED); - - /// - /// Link to GitHub page to show in the launcher. - /// - public static readonly CVarDef InfoLinksGithub = - CVarDef.Create("infolinks.github", "", CVar.SERVER | CVar.REPLICATED); - - /// - /// Link to website to show in the launcher. - /// - public static readonly CVarDef InfoLinksWebsite = - CVarDef.Create("infolinks.website", "", CVar.SERVER | CVar.REPLICATED); - - /// - /// Link to wiki to show in the launcher. - /// - public static readonly CVarDef InfoLinksWiki = - CVarDef.Create("infolinks.wiki", "", CVar.SERVER | CVar.REPLICATED); - } +namespace OpenDreamShared; + +[CVarDefs] +public abstract class OpenDreamCVars { + public static readonly CVarDef JsonPath = + CVarDef.Create("opendream.json_path", String.Empty, CVar.SERVERONLY); + + public static readonly CVarDef DownloadTimeout = + CVarDef.Create("opendream.download_timeout", 30, CVar.CLIENTONLY); + + public static readonly CVarDef AlwaysShowExceptions = + CVarDef.Create("opendream.always_show_exceptions", false, CVar.SERVERONLY); + + public static readonly CVarDef DebugAdapterLaunched = + CVarDef.Create("opendream.debug_adapter_launched", 0, CVar.SERVERONLY); + + public static readonly CVarDef SpoofIEUserAgent = + CVarDef.Create("opendream.spoof_ie_user_agent", true, CVar.CLIENTONLY); + + public static readonly CVarDef WorldParams = + CVarDef.Create("opendream.world_params", string.Empty, CVar.SERVERONLY); + + public static readonly CVarDef TopicPort = + CVarDef.Create("opendream.topic_port", 25567, CVar.SERVERONLY); + + /* + * INFOLINKS + */ + + /// + /// Link to Discord server to show in the launcher. + /// + public static readonly CVarDef InfoLinksDiscord = + CVarDef.Create("infolinks.discord", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to forum to show in the launcher. + /// + public static readonly CVarDef InfoLinksForum = + CVarDef.Create("infolinks.forum", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to GitHub page to show in the launcher. + /// + public static readonly CVarDef InfoLinksGithub = + CVarDef.Create("infolinks.github", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to website to show in the launcher. + /// + public static readonly CVarDef InfoLinksWebsite = + CVarDef.Create("infolinks.website", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to wiki to show in the launcher. + /// + public static readonly CVarDef InfoLinksWiki = + CVarDef.Create("infolinks.wiki", "", CVar.SERVER | CVar.REPLICATED); + }