Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bj2pr6ffwgemb
1 change: 1 addition & 0 deletions src/R3.Godot/addons/R3.Godot/GodotFrameProvider.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://75ldbv0bhjrh
1 change: 1 addition & 0 deletions src/R3.Godot/addons/R3.Godot/GodotNodeExtensions.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c8wf0jglyrrwj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c80jh7i0rbq60
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cb611m8ceqd7c
14 changes: 13 additions & 1 deletion src/R3.Godot/addons/R3.Godot/GodotR3Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ namespace R3;
[Tool]
public partial class GodotR3Plugin : EditorPlugin
{
static ObservableTrackerDebuggerPlugin? observableTrackerDebugger;
ObservableTrackerDebuggerPlugin? observableTrackerDebugger = null!;
ObservableTrackerTab? tab = null!;

public override void _EnterTree()
{
tab ??= new ObservableTrackerTab();
observableTrackerDebugger ??= new ObservableTrackerDebuggerPlugin();
observableTrackerDebugger.SetTab(tab);
AddDebuggerPlugin(observableTrackerDebugger);
// Automatically install autoloads here for ease of use.
AddAutoloadSingleton(nameof(FrameProviderDispatcher), "res://addons/R3.Godot/FrameProviderDispatcher.cs");
Expand All @@ -23,8 +27,16 @@ public override void _ExitTree()
if (observableTrackerDebugger != null)
{
RemoveDebuggerPlugin(observableTrackerDebugger);
observableTrackerDebugger.Clear();
observableTrackerDebugger = null;
}

if (tab != null)
{
tab.QueueFree();
tab = null;
}

RemoveAutoloadSingleton(nameof(FrameProviderDispatcher));
RemoveAutoloadSingleton(nameof(ObservableTrackerRuntimeHook));
}
Expand Down
1 change: 1 addition & 0 deletions src/R3.Godot/addons/R3.Godot/GodotR3Plugin.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dhkfk30bunbun
1 change: 1 addition & 0 deletions src/R3.Godot/addons/R3.Godot/GodotSignalMapper.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://42r56ib4418x
1 change: 1 addition & 0 deletions src/R3.Godot/addons/R3.Godot/GodotTimeProvider.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cjsdk285epyja
1 change: 1 addition & 0 deletions src/R3.Godot/addons/R3.Godot/GodotUINodeExtensions.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://1078dvflnl2b
86 changes: 58 additions & 28 deletions src/R3.Godot/addons/R3.Godot/ObservableTrackerDebuggerPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#if TOOLS
#nullable enable

using Godot;
using System;
using System.Collections.Generic;
using Godot;
using GDArray = Godot.Collections.Array;

namespace R3;
Expand All @@ -13,9 +13,11 @@ namespace R3;
public partial class ObservableTrackerDebuggerPlugin : EditorDebuggerPlugin
{
// Shared header used in IPC by ObservableTracker classes.

public const string MessageHeader = "ObservableTracker";

// Implemented by ObservableTrackerRuntimeHook.
public const string Message_NotifyReady = "NotifyReady";
public const string Message_RequestActiveTasks = "RequestActiveTasks";
public const string Message_SetEnableStates = "SetEnableStates";
public const string Message_InvokeGCCollect = "InvokeGCCollect";
Expand All @@ -26,45 +28,46 @@ public partial class ObservableTrackerDebuggerPlugin : EditorDebuggerPlugin
// A TrackerSession isolates each debugger session's states.
// There's no way to know if a session has been disposed for good, so we will never remove anything from this dictionary.
// This is similar to how it is handled in the Godot core (see: https://github.com/godotengine/godot/blob/master/modules/multiplayer/editor/multiplayer_editor_plugin.cpp)
readonly Dictionary<int, TrackerSession> sessions = new();
readonly Dictionary<int, TrackerSession> sessions = [];
ObservableTrackerTab tab = null!;

private class TrackerSession
class TrackerSession(EditorDebuggerSession debuggerSession)
{
public readonly EditorDebuggerSession debuggerSession;
public readonly EditorDebuggerSession debuggerSession = debuggerSession;
public readonly List<TrackingState> states = new();
public event Action<IEnumerable<TrackingState>>? ReceivedActiveTasks;

public TrackerSession(EditorDebuggerSession debuggerSession)
{
this.debuggerSession = debuggerSession;
}

public void InvokeReceivedActiveTasks()
{
ReceivedActiveTasks?.Invoke(states);
}
}

public void SetTab(ObservableTrackerTab tab)
{
this.tab = tab;
}

public override void _SetupSession(int sessionId)
{
var currentSession = GetSession(sessionId);
sessions[sessionId] = new TrackerSession(currentSession);

// NotifyOnSessionSetup gives the tab a reference to the debugger plugin, as well as the sessionId which is needed for messages.
var tab = new ObservableTrackerTab();
tab.NotifyOnSessionSetup(this, sessionId);
currentSession.AddSessionTab(tab);

// As sessions don't seem to be ever disposed, we don't need to unregister these callbacks either.
// Set the process to false to avoid unnecessary processing before the session is ready.
tab.SetProcess(false);

currentSession.Started += () =>
{
if (IsInstanceValid(tab))
{
tab.SetProcess(true);
// Important! We need to tell the tab the session has started, so it can initialize the enabled states of the runtime ObservableTracker.
tab.NotifyOnSessionStart();
}
};

currentSession.Stopped += () =>
{
if (IsInstanceValid(tab))
Expand All @@ -73,7 +76,7 @@ public override void _SetupSession(int sessionId)
}
};
}

public override bool _HasCapture(string capture)
{
return capture == MessageHeader;
Expand All @@ -85,13 +88,20 @@ public override bool _Capture(string message, GDArray data, int sessionId)
// so we need to trim it here.
string messageWithoutHeader = message.Substring(message.IndexOf(':') + 1);
//GD.Print(nameof(ObservableTrackerDebuggerPlugin) + " received " + messageWithoutHeader);
switch(messageWithoutHeader)

if (!sessions.TryGetValue(sessionId, out var session))
{
// This should never happen, but just in case.
GD.PrintErr(nameof(ObservableTrackerDebuggerPlugin) + " received message for unknown sessionId " + sessionId);
return false;
}

switch (messageWithoutHeader)
{
case Message_ReceiveActiveTasks:
// Only invoke event if updated.
if (data[0].AsBool())
{
var session = sessions[sessionId];
session.states.Clear();
foreach (GDArray item in data[1].AsGodotArray())
{
Expand All @@ -101,14 +111,20 @@ public override bool _Capture(string message, GDArray data, int sessionId)
FormattedType = item[1].AsString(),
AddTime = new DateTime(item[2].AsInt64()),
StackTrace = item[3].AsString(),
};;
};

session.states.Add(state);
}
session.InvokeReceivedActiveTasks();
}

return true;
case Message_NotifyReady:
tab.NotifyOnReady();
return true;
}
return base._Capture(message, data, sessionId);

return default;
}

public void RegisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action)
Expand All @@ -123,28 +139,42 @@ public void UnregisterReceivedActiveTasks(int sessionId, Action<IEnumerable<Trac
sessions[sessionId].ReceivedActiveTasks -= action;
}

public void SetEnableStates(int sessionId, bool enableTracking, bool enableStackTrace)
{
SendMessage(sessionId, MessageHeader + ":" + Message_SetEnableStates, new GDArray() { enableTracking, enableStackTrace });
}

public void UpdateTrackingStates(int sessionId, bool forceUpdate = false)
{
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
{
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_RequestActiveTasks, new () { forceUpdate });
}
SendMessage(sessionId, MessageHeader + ":" + Message_RequestActiveTasks, new GDArray() { forceUpdate });
}

public void SetEnableStates(int sessionId, bool enableTracking, bool enableStackTrace)
public void InvokeGCCollect(int sessionId)
{
SendMessage(sessionId, MessageHeader + ":" + Message_InvokeGCCollect, new GDArray() { });
}

void SendMessage(int sessionId, string message, GDArray data)
{
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
// Runtime Session and Debugger Session are not the same.
if (!sessions.TryGetValue(sessionId, out var session))
{
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_SetEnableStates, new () { enableTracking, enableStackTrace});
session = sessions[sessionId] = new TrackerSession(GetSession(sessionId));
tab.ReSetupSession(this, sessionId);
}

if (session.debuggerSession.IsActive())
session.debuggerSession.SendMessage(message, data);
}

public void InvokeGCCollect(int sessionId)
internal void Clear()
{
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
foreach (var session in sessions)
{
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_InvokeGCCollect);
session.Value.debuggerSession.RemoveSessionTab(tab);
}
sessions.Clear();
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bibad6kxe8i1c
31 changes: 21 additions & 10 deletions src/R3.Godot/addons/R3.Godot/ObservableTrackerRuntimeHook.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

#nullable enable

using Godot;
using System;
using Godot;
using GDArray = Godot.Collections.Array;

namespace R3;
Expand All @@ -15,37 +15,48 @@ public override void _Ready()
{
#if TOOLS
EngineDebugger.RegisterMessageCapture(ObservableTrackerDebuggerPlugin.MessageHeader, Callable.From((string message, GDArray data) =>
{
{
//GD.Print(nameof(ObservableTrackerRuntimeHook) + " received " + message);
switch (message)
{
case ObservableTrackerDebuggerPlugin.Message_RequestActiveTasks:
// data[0]: If true, force an update anyway.
if (ObservableTracker.CheckAndResetDirty() || data[0].AsBool())
{
GDArray states = new();
ObservableTracker.ForEachActiveTask(state =>
{
// DateTime is not a Variant type, so we serialize it using Ticks instead.
states.Add(new GDArray { state.TrackingId, state.FormattedType, state.AddTime.Ticks, state.StackTrace });
});
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { true, states });
GDArray states = [];

// DateTime is not a Variant type, so we serialize it using Ticks instead.
ObservableTracker.ForEachActiveTask(state => states.Add(new GDArray { state.TrackingId, state.FormattedType, state.AddTime.Ticks, state.StackTrace }));
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, [true, states]);
}
else
{
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { false, });
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, [false]);
}
break;
case ObservableTrackerDebuggerPlugin.Message_SetEnableStates:
GD.Print("Setting enable states: " + data);
ObservableTracker.EnableTracking = data[0].AsBool();
ObservableTracker.EnableStackTrace = data[1].AsBool();
break;
case ObservableTrackerDebuggerPlugin.Message_InvokeGCCollect:
GD.Print("Invoking GC.Collect");
GC.Collect(0);
break;
}
return true;
}));

CallDeferred(nameof(NotifyObservableTrackerReady));
#endif
}

private void NotifyObservableTrackerReady()
{
if (!OS.IsDebugBuild())
return;
#if TOOLS
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_NotifyReady, new GDArray() { true });
#endif
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c8bd5kcavwuvn
Loading