# How FlowGraphTracker Enables Advanced Debugging and Execution Tracking in Ceres

> Learn how FlowGraphTracker in Ceres enables advanced debugging and execution tracking via a pluggable hook system. Monitor and profile Ceres Flow Graphs easily.

- Repository: [AkiKurisu/ceres](https://github.com/akikurisu/ceres)
- Tags: deep-dive
- Published: 2026-02-24

---

**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`](https://github.com/akikurisu/ceres/blob/main/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`](https://github.com/akikurisu/ceres/blob/main/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`):

```csharp
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:

```csharp
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`:

```csharp
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:**

```csharp
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:

```csharp
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:

```csharp
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`](https://github.com/akikurisu/ceres/blob/main/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.