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);
+
}