-
-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #45 from chocola-mint/godot-observable-tracker
Add Observable Tracker to Godot
- Loading branch information
Showing
6 changed files
with
454 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
#if TOOLS | ||
#nullable enable | ||
|
||
using Godot; | ||
|
||
namespace R3; | ||
|
||
[Tool] | ||
public partial class GodotR3Plugin : EditorPlugin | ||
{ | ||
static ObservableTrackerDebuggerPlugin? observableTrackerDebugger; | ||
public override void _EnterTree() | ||
{ | ||
observableTrackerDebugger ??= new ObservableTrackerDebuggerPlugin(); | ||
AddDebuggerPlugin(observableTrackerDebugger); | ||
// Automatically install autoloads here for ease of use. | ||
AddAutoloadSingleton(nameof(FrameProviderDispatcher), "res://addons/R3.Godot/FrameProviderDispatcher.cs"); | ||
AddAutoloadSingleton(nameof(ObservableTrackerRuntimeHook), "res://addons/R3.Godot/ObservableTrackerRuntimeHook.cs"); | ||
} | ||
|
||
public override void _ExitTree() | ||
{ | ||
if (observableTrackerDebugger != null) | ||
{ | ||
RemoveDebuggerPlugin(observableTrackerDebugger); | ||
observableTrackerDebugger = null; | ||
} | ||
RemoveAutoloadSingleton(nameof(FrameProviderDispatcher)); | ||
RemoveAutoloadSingleton(nameof(ObservableTrackerRuntimeHook)); | ||
} | ||
} | ||
#endif |
148 changes: 148 additions & 0 deletions
148
src/R3.Godot/addons/R3.Godot/ObservableTrackerDebuggerPlugin.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
#if TOOLS | ||
#nullable enable | ||
|
||
using Godot; | ||
using System; | ||
using System.Collections.Generic; | ||
using GDArray = Godot.Collections.Array; | ||
|
||
namespace R3; | ||
|
||
// ObservableTrackerDebuggerPlugin creates the Observable Tracker tab in the debugger, and communicates with ObservableTrackerRuntimeHook via EditorDebuggerSessions. | ||
[Tool] | ||
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_RequestActiveTasks = "RequestActiveTasks"; | ||
public const string Message_SetEnableStates = "SetEnableStates"; | ||
public const string Message_InvokeGCCollect = "InvokeGCCollect"; | ||
|
||
// Implemented by ObservableTrackerDebuggerPlugin. | ||
public const string Message_ReceiveActiveTasks = "ReceiveActiveTasks"; | ||
|
||
// 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(); | ||
|
||
private class TrackerSession | ||
{ | ||
public readonly EditorDebuggerSession 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 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. | ||
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 SubscriptionTracker. | ||
tab.NotifyOnSessionStart(); | ||
} | ||
}; | ||
currentSession.Stopped += () => | ||
{ | ||
if (IsInstanceValid(tab)) | ||
{ | ||
tab.SetProcess(false); | ||
} | ||
}; | ||
} | ||
|
||
public override bool _HasCapture(string capture) | ||
{ | ||
return capture == MessageHeader; | ||
} | ||
|
||
public override bool _Capture(string message, GDArray data, int sessionId) | ||
{ | ||
// When EditorDebuggerPlugin._Capture receives messages, the header isn't trimmed (unlike how it is in EngineDebugger), | ||
// so we need to trim it here. | ||
string messageWithoutHeader = message.Substring(message.IndexOf(':') + 1); | ||
//GD.Print(nameof(ObservableTrackerDebuggerPlugin) + " received " + messageWithoutHeader); | ||
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()) | ||
{ | ||
var state = new TrackingState() | ||
{ | ||
TrackingId = item[0].AsInt32(), | ||
FormattedType = item[1].AsString(), | ||
AddTime = new DateTime(item[2].AsInt64()), | ||
StackTrace = item[3].AsString(), | ||
};; | ||
session.states.Add(state); | ||
} | ||
session.InvokeReceivedActiveTasks(); | ||
} | ||
return true; | ||
} | ||
return base._Capture(message, data, sessionId); | ||
} | ||
|
||
public void RegisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action) | ||
{ | ||
sessions[sessionId].ReceivedActiveTasks += action; | ||
} | ||
|
||
public void UnregisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action) | ||
{ | ||
sessions[sessionId].ReceivedActiveTasks -= action; | ||
} | ||
|
||
public void UpdateTrackingStates(int sessionId, bool forceUpdate = false) | ||
{ | ||
if (sessions[sessionId].debuggerSession.IsActive()) | ||
{ | ||
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_RequestActiveTasks, new () { forceUpdate }); | ||
} | ||
} | ||
|
||
public void SetEnableStates(int sessionId, bool enableTracking, bool enableStackTrace) | ||
{ | ||
if (sessions[sessionId].debuggerSession.IsActive()) | ||
{ | ||
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_SetEnableStates, new () { enableTracking, enableStackTrace}); | ||
} | ||
} | ||
|
||
public void InvokeGCCollect(int sessionId) | ||
{ | ||
if (sessions[sessionId].debuggerSession.IsActive()) | ||
{ | ||
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_InvokeGCCollect); | ||
} | ||
} | ||
} | ||
#endif |
53 changes: 53 additions & 0 deletions
53
src/R3.Godot/addons/R3.Godot/ObservableTrackerRuntimeHook.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
#nullable enable | ||
|
||
using Godot; | ||
using System; | ||
using GDArray = Godot.Collections.Array; | ||
|
||
namespace R3; | ||
|
||
// Sends runtime SubscriptionTracker information to ObservableTrackerDebuggerPlugin. | ||
// Needs to be an Autoload. Should not be instantiated manually. | ||
public partial class ObservableTrackerRuntimeHook : Node | ||
{ | ||
public override void _Ready() | ||
{ | ||
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 (SubscriptionTracker.CheckAndResetDirty() || data[0].AsBool()) | ||
{ | ||
GDArray states = new(); | ||
SubscriptionTracker.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 }); | ||
} | ||
else | ||
{ | ||
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { false, }); | ||
} | ||
break; | ||
case ObservableTrackerDebuggerPlugin.Message_SetEnableStates: | ||
SubscriptionTracker.EnableTracking = data[0].AsBool(); | ||
SubscriptionTracker.EnableStackTrace = data[1].AsBool(); | ||
break; | ||
case ObservableTrackerDebuggerPlugin.Message_InvokeGCCollect: | ||
GC.Collect(0); | ||
break; | ||
} | ||
return true; | ||
})); | ||
} | ||
|
||
public override void _ExitTree() | ||
{ | ||
EngineDebugger.UnregisterMessageCapture(ObservableTrackerDebuggerPlugin.MessageHeader); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
#if TOOLS | ||
#nullable enable | ||
|
||
using Godot; | ||
using System; | ||
|
||
namespace R3; | ||
|
||
[Tool] | ||
public partial class ObservableTrackerTab : VBoxContainer | ||
{ | ||
public const string EnableAutoReloadKey = "ObservableTracker_EnableAutoReloadKey"; | ||
public const string EnableTrackingKey = "ObservableTracker_EnableTrackingKey"; | ||
public const string EnableStackTraceKey = "ObservableTracker_EnableStackTraceKey"; | ||
bool enableAutoReload, enableTracking, enableStackTrace; | ||
ObservableTrackerTree? tree; | ||
ObservableTrackerDebuggerPlugin? debuggerPlugin; | ||
int interval = 0; | ||
int sessionId = 0; | ||
|
||
public void NotifyOnSessionSetup(ObservableTrackerDebuggerPlugin debuggerPlugin, int sessionId) | ||
{ | ||
this.debuggerPlugin = debuggerPlugin; | ||
this.sessionId = sessionId; | ||
tree ??= new ObservableTrackerTree(); | ||
tree.NotifyOnSessionSetup(debuggerPlugin!, sessionId); | ||
} | ||
|
||
public void NotifyOnSessionStart() | ||
{ | ||
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace); | ||
} | ||
|
||
public override void _Ready() | ||
{ | ||
Name = "Observable Tracker"; | ||
|
||
tree ??= new ObservableTrackerTree(); | ||
|
||
// Head panel | ||
var headPanelLayout = new HBoxContainer(); | ||
headPanelLayout.SetAnchor(Side.Left, 0); | ||
headPanelLayout.SetAnchor(Side.Right, 0); | ||
AddChild(headPanelLayout); | ||
|
||
// Toggle buttons (top left) | ||
var enableAutoReloadToggle = new CheckButton | ||
{ | ||
Text = "Enable AutoReload", | ||
TooltipText = "Reload automatically." | ||
}; | ||
var enableTrackingToggle = new CheckButton | ||
{ | ||
Text = "Enable Tracking", | ||
TooltipText = "Start to track Observable subscription. Performance impact: low" | ||
}; | ||
var enableStackTraceToggle = new CheckButton | ||
{ | ||
Text = "Enable StackTrace", | ||
TooltipText = "Capture StackTrace when subscribed. Performance impact: high" | ||
}; | ||
|
||
// For every button: Initialize pressed state and subscribe to Toggled event. | ||
EditorSettings settings = EditorInterface.Singleton.GetEditorSettings(); | ||
enableAutoReloadToggle.ButtonPressed = enableAutoReload = GetSettingOrDefault(settings, EnableAutoReloadKey, false).AsBool(); | ||
enableAutoReloadToggle.Toggled += toggledOn => | ||
{ | ||
settings.SetSetting(EnableAutoReloadKey, toggledOn); | ||
enableAutoReload = toggledOn; | ||
}; | ||
enableTrackingToggle.ButtonPressed = enableTracking = GetSettingOrDefault(settings, EnableTrackingKey, false).AsBool(); | ||
enableTrackingToggle.Toggled += toggledOn => | ||
{ | ||
settings.SetSetting(EnableTrackingKey, toggledOn); | ||
enableTracking = toggledOn; | ||
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace); | ||
}; | ||
enableStackTraceToggle.ButtonPressed = enableStackTrace = GetSettingOrDefault(settings, EnableStackTraceKey, false).AsBool(); | ||
enableStackTraceToggle.Toggled += toggledOn => | ||
{ | ||
settings.SetSetting(EnableStackTraceKey, toggledOn); | ||
enableStackTrace = toggledOn; | ||
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace); | ||
}; | ||
|
||
// Regular buttons (top right) | ||
var reloadButton = new Button | ||
{ | ||
Text = "Reload", | ||
TooltipText = "Reload View." | ||
}; | ||
var GCButton = new Button | ||
{ | ||
Text = "GC.Collect", | ||
TooltipText = "Invoke GC.Collect." | ||
}; | ||
|
||
reloadButton.Pressed += () => | ||
{ | ||
debuggerPlugin!.UpdateTrackingStates(sessionId, true); | ||
}; | ||
GCButton.Pressed += () => | ||
{ | ||
debuggerPlugin!.InvokeGCCollect(sessionId); | ||
}; | ||
|
||
// Button layout. | ||
headPanelLayout.AddChild(enableAutoReloadToggle); | ||
headPanelLayout.AddChild(enableTrackingToggle); | ||
headPanelLayout.AddChild(enableStackTraceToggle); | ||
// Kind of like Unity's FlexibleSpace. Pushes the first three buttons to the left, and the remaining buttons to the right. | ||
headPanelLayout.AddChild(new Control() | ||
{ | ||
SizeFlagsHorizontal = SizeFlags.Expand, | ||
}); | ||
headPanelLayout.AddChild(reloadButton); | ||
headPanelLayout.AddChild(GCButton); | ||
|
||
// Tree goes last. | ||
AddChild(tree); | ||
} | ||
|
||
public override void _Process(double delta) | ||
{ | ||
if (enableAutoReload) | ||
{ | ||
if (interval++ % 120 == 0) | ||
{ | ||
debuggerPlugin!.UpdateTrackingStates(sessionId); | ||
} | ||
} | ||
} | ||
|
||
static Variant GetSettingOrDefault(EditorSettings settings, string key, Variant @default) | ||
{ | ||
if (settings.HasSetting(key)) | ||
{ | ||
return settings.GetSetting(key); | ||
} | ||
else | ||
{ | ||
return @default; | ||
} | ||
} | ||
} | ||
#endif |
Oops, something went wrong.