How FlowGraphTracker Enables Advanced Debugging and Execution Tracking in Ceres

FlowGraphTracker provides a pluggable, asynchronous hook system that intercepts every node execution in Ceres Flow Graphs, enabling precise monitoring, profiling, and debugging without modifying generated graph code.

FlowGraphTracker is the core abstraction in Ceres—a Unity-based visual-scripting framework—that allows developers to monitor every step of Flow Graph execution. By implementing the EnterNode and ExitNode callbacks defined in Runtime/Flow/Models/FlowGraphTracker.cs, you can build sophisticated debugging tools, performance profilers, and execution loggers that operate transparently alongside your graph logic.

Core Architecture of FlowGraphTracker

The tracking system centers on an abstract base class that defines two critical lifecycle hooks. Every node in a Flow Graph invokes these hooks immediately before and after execution, creating a universal interception point.

The Abstract Base Class

In Runtime/Flow/Models/FlowGraphTracker.cs, the FlowGraphTracker class declares two virtual methods:

  • EnterNode(ExecutableNode node): Called immediately before a node begins execution.
  • ExitNode(ExecutableNode node): Called immediately after a node completes execution.

Both methods return UniTask, allowing trackers to perform asynchronous work—such as writing to disk or sending network requests—without blocking the graph's execution flow. When no asynchronous work is required, implementations return UniTask.CompletedTask for zero-allocation synchronous completion.

Disposable Scopes with TrackerAutoScope

The Auto() method returns a TrackerAutoScope instance that implements the disposable pattern. This scope automatically manages the active tracker stack (_activeTracer):

using (new FlowGraphDependencyTracker(graph).Auto())
{
    // All node executions within this block are tracked.
    await graph.ExecuteEventAsync(context, "Start", evt);
}
// Previous tracker is automatically restored here.

When the scope is entered, it stores the previous active tracker and installs the new one. Upon disposal, it restores the previous tracker and disposes the current instance. This design guarantees deterministic cleanup even with deeply nested debugging sessions, preventing state leakage between unit tests or editor sessions.

The Active Tracker Mechanism

Static methods SetActiveTracker() and GetActiveTracker() maintain a global reference to the current tracker. The runtime queries FlowGraphTracker.GetActiveTracker() before each node execution. If no tracker is explicitly set, the system falls back to FlowGraphTracker.Empty—a singleton implementation with null methods that incurs virtually zero runtime overhead.

Built-in Implementation: FlowGraphDependencyTracker

Ceres provides FlowGraphDependencyTracker in the same source file as a ready-to-use reference implementation. This concrete tracker overrides EnterNode and ExitNode to log each node's type, GUID, and declared dependencies by resolving them through the supplied FlowGraph instance. It demonstrates the standard pattern for inspecting node metadata without interfering with execution logic.

Why FlowGraphTracker Enables Advanced Debugging

The architecture enables sophisticated debugging scenarios through six key design decisions:

  • Fine-grained Hooks: Every individual node triggers EnterNode and ExitNode, allowing per-node timing, dependency inspection, or error capture at the exact moment of execution.

  • Scoped Lifetime: The using (tracker.Auto()) pattern automatically restores previous trackers, ensuring nested debugging sessions never leak state or interfere with each other.

  • Asynchronous Support: Because callbacks return UniTask, trackers can perform asynchronous I/O operations while the graph continues execution, enabling remote logging or database persistence without frame drops.

  • Global vs. Local Control: Developers can set a tracker globally via SetActiveTracker() for all graph runs, or wrap a single execution with a scoped tracker for precise, targeted monitoring.

  • Zero-Overhead Default: When no tracker is active, FlowGraphTracker.Empty provides null implementations that return UniTask.CompletedTask, eliminating performance costs for production builds.

  • Customizable and Reusable: The system supports mixing and extending patterns—such as combining logging with profiling—through simple subclassing, making it plug-and-play for various debugging scenarios.

Practical Code Examples

Logging Node Dependencies

Use the built-in FlowGraphDependencyTracker to automatically record every node's metadata during execution:

using (new FlowGraphDependencyTracker(graph).Auto())
{
    // All node entries/exits and their dependencies are logged automatically.
    await graph.ExecuteEventAsync(context, "Start", evt);
}

Profiling Node Execution Time

Create a custom PerformanceProfilerTracker by subclassing FlowGraphTracker and measuring elapsed time between EnterNode and ExitNode:

public class PerformanceProfilerTracker : FlowGraphTracker
{
    private readonly Dictionary<string, NodeProfile> _profiles = new();

    private class NodeProfile
    {
        public string NodeName;
        public Stopwatch Stopwatch = new();
        public int ExecutionCount;
        public long TotalTicks;
    }

    public override UniTask EnterNode(ExecutableNode node)
    {
        if (!_profiles.TryGetValue(node.Guid, out var profile))
        {
            profile = new NodeProfile { NodeName = node.GetTypeName() };
            _profiles[node.Guid] = profile;
        }
        profile.ExecutionCount++;
        profile.Stopwatch.Restart();
        return UniTask.CompletedTask;
    }

    public override UniTask ExitNode(ExecutableNode node)
    {
        var profile = _profiles[node.Guid];
        profile.Stopwatch.Stop();
        profile.TotalTicks += profile.Stopwatch.ElapsedTicks;
        return UniTask.CompletedTask;
    }

    public override void Dispose()
    {
        foreach (var kvp in _profiles)
        {
            var p = kvp.Value;
            var avgMs = (p.TotalTicks / (double)Stopwatch.Frequency) /
                        p.ExecutionCount * 1000;
            Debug.Log($"{p.NodeName}: {p.ExecutionCount} runs, avg {avgMs:F3} ms");
        }
        base.Dispose();
    }
}

Usage:

var profiler = new PerformanceProfilerTracker();
using (profiler.Auto())
{
    await graph.ExecuteEventAsync(context, "Start", evt);
}
// Report prints automatically on Dispose().

Conditional Debugging

Track only specific nodes using a conditional wrapper that filters by node type or name:

var tracker = new ConditionalTracker(node => node.GetTypeName().Contains("Log"));
using (tracker.Auto())
{
    await graph.ExecuteEventAsync(context, "Start", evt);
}

Integration with the Ceres Runtime

According to the Ceres source code, the Flow Graph executor integrates tracking by invoking FlowGraphTracker.GetActiveTracker() before and after each node's logic. The executor emits calls similar to:

await FlowGraphTracker.GetActiveTracker().EnterNode(node);
// ... node execution logic ...
await FlowGraphTracker.GetActiveTracker().ExitNode(node);

This integration point is documented in Documentation~/docs/flow_graph_tracker.md, which provides additional ready-made examples including error collectors and visual debuggers.

Summary

  • FlowGraphTracker provides asynchronous EnterNode and ExitNode hooks that intercept every node execution in Ceres Flow Graphs.
  • TrackerAutoScope ensures deterministic cleanup and safe nesting of tracking sessions via disposable scopes.
  • The Empty implementation guarantees zero runtime overhead when tracking is disabled.
  • Concrete implementations like FlowGraphDependencyTracker demonstrate dependency logging and metadata inspection patterns.
  • All core functionality resides in Runtime/Flow/Models/FlowGraphTracker.cs, with comprehensive examples in the documentation.

Frequently Asked Questions

What is FlowGraphTracker in Ceres?

FlowGraphTracker is an abstract base class in the Ceres Unity framework that defines virtual hooks (EnterNode and ExitNode) invoked before and after every node executes in a Flow Graph. It serves as the foundation for building custom debugging, profiling, and monitoring tools.

How do I create a custom execution tracker?

Subclass FlowGraphTracker and override EnterNode and ExitNode to implement your tracking logic. Wrap your graph execution in a using block with tracker.Auto() to automatically manage the tracker's lifetime and ensure proper cleanup. Refer to Documentation~/docs/flow_graph_tracker.md for templates covering loggers, profilers, and conditional trackers.

Does FlowGraphTracker impact runtime performance?

No. When no tracker is active, the system uses FlowGraphTracker.Empty, which returns UniTask.CompletedTask and performs no allocations or operations. This null-object pattern ensures production builds incur no debugging overhead.

Can FlowGraphTracker handle asynchronous operations?

Yes. Both EnterNode and ExitNode return UniTask, allowing trackers to perform asynchronous I/O—such as writing to remote log servers or databases—without blocking the graph's execution thread. The graph awaits these tasks, ensuring tracking completes before proceeding.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →