Skip to content

Commit

Permalink
Merge pull request #45 from chocola-mint/godot-observable-tracker
Browse files Browse the repository at this point in the history
Add Observable Tracker to Godot
  • Loading branch information
neuecc authored Jan 16, 2024
2 parents e4f73eb + 7db85c6 commit 239723e
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 0 deletions.
32 changes: 32 additions & 0 deletions src/R3.Godot/addons/R3.Godot/GodotR3Plugin.cs
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 src/R3.Godot/addons/R3.Godot/ObservableTrackerDebuggerPlugin.cs
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 src/R3.Godot/addons/R3.Godot/ObservableTrackerRuntimeHook.cs
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);
}
}
146 changes: 146 additions & 0 deletions src/R3.Godot/addons/R3.Godot/ObservableTrackerTab.cs
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
Loading

0 comments on commit 239723e

Please sign in to comment.