Graph Tracker Guide
FlowGraphTracker observes entry and exit for executable nodes. Use it for diagnostics that cannot be obtained from the Flow editor debugger.
Basic Usage
Using TrackerAutoScope
The easiest way to use a tracker is with the Auto() method, which returns a TrackerAutoScope:
using Ceres;
using Ceres.Flow;
using Ceres.Flow.Annotations;
public class MyFlowObject : FlowGraphObject
{
[ImplementableEvent]
private void Start()
{
// Create a tracker and use it for this execution
using (new FlowGraphDependencyTracker(this.GetRuntimeFlowGraph()).Auto())
{
// Execute the graph - tracker will monitor execution
this.ProcessEvent();
}
// Tracker is automatically disposed when scope ends
}
}
Setting Active Tracker
You can also set a tracker as the active tracker for all graph executions:
var tracker = new FlowGraphDependencyTracker(graph);
FlowGraphTracker.SetActiveTracker(tracker);
// All graph executions will use this tracker
runtime.ProcessEvent(eventName: "Start");
// Clean up when done
tracker.Dispose();
Built-in Tracker: FlowGraphDependencyTracker
Ceres provides a built-in tracker that logs node execution and dependencies:
using Ceres;
using Ceres.Flow;
using Cysharp.Threading.Tasks;
public class FlowGraphDependencyTracker : FlowGraphTracker
{
private readonly FlowGraph _flowGraph;
public FlowGraphDependencyTracker(FlowGraph flowGraph)
{
_flowGraph = flowGraph;
}
public override UniTask EnterNode(ExecutableNode node)
{
CeresLogger.Log($"Enter node >>> [{node.GetTypeName()}]({node.Guid})");
var dependencies = node.NodeData.GetDependencies();
if (dependencies != null)
{
foreach (var dependency in dependencies)
{
var dependencyNode = _flowGraph.FindNode(dependency);
if (dependencyNode != null)
{
CeresLogger.Log($"Find dependency node [{dependencyNode.GetTypeName()}]({dependencyNode.Guid})");
}
}
}
return UniTask.CompletedTask;
}
public override UniTask ExitNode(ExecutableNode node)
{
CeresLogger.Log($"Exit node <<< [{node.GetTypeName()}]({node.Guid})");
return UniTask.CompletedTask;
}
}
Usage:
using (new FlowGraphDependencyTracker(graph).Auto())
{
runtime.ProcessEvent(eventName: "Start");
}
Output:
Enter node >>> [FlowNode_Log](abc123)
Find dependency node [FlowNode_GetVariable](def456)
Exit node <<< [FlowNode_Log](abc123)
Example 1: Execution Logger
Create a tracker that logs all node executions with timestamps:
using System;
using System.Collections.Generic;
using Ceres.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ExecutionLoggerTracker : FlowGraphTracker
{
private readonly List<LogEntry> _logEntries = new();
private struct LogEntry
{
public string NodeName;
public string NodeGuid;
public DateTime Timestamp;
public bool IsEnter;
}
public override UniTask EnterNode(ExecutableNode node)
{
_logEntries.Add(new LogEntry
{
NodeName = node.GetTypeName(),
NodeGuid = node.Guid,
Timestamp = DateTime.Now,
IsEnter = true
});
Debug.Log($"[{DateTime.Now:HH:mm:ss.fff}] Enter: {node.GetTypeName()}");
return UniTask.CompletedTask;
}
public override UniTask ExitNode(ExecutableNode node)
{
_logEntries.Add(new LogEntry
{
NodeName = node.GetTypeName(),
NodeGuid = node.Guid,
Timestamp = DateTime.Now,
IsEnter = false
});
Debug.Log($"[{DateTime.Now:HH:mm:ss.fff}] Exit: {node.GetTypeName()}");
return UniTask.CompletedTask;
}
public void PrintSummary()
{
Debug.Log($"Total nodes executed: {_logEntries.Count / 2}");
foreach (var entry in _logEntries)
{
Debug.Log($"{entry.Timestamp:HH:mm:ss.fff} - {(entry.IsEnter ? "Enter" : "Exit")}: {entry.NodeName}");
}
}
public override void Dispose()
{
PrintSummary();
_logEntries.Clear();
base.Dispose();
}
}
Usage:
var logger = new ExecutionLoggerTracker();
using (logger.Auto())
{
runtime.ProcessEvent(eventName: "Start");
}
// Summary is printed automatically on dispose
Example 2: Performance Profiler
Track execution time for each node:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Ceres.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class PerformanceProfilerTracker : FlowGraphTracker
{
private readonly Dictionary<string, NodeProfile> _profiles = new();
private readonly Stack<NodeProfile> _executionStack = new();
private class NodeProfile
{
public string NodeName;
public string NodeGuid;
public Stopwatch Stopwatch = new();
public int ExecutionCount;
public long TotalTicks;
}
public override UniTask EnterNode(ExecutableNode node)
{
var guid = node.Guid;
if (!_profiles.TryGetValue(guid, out var profile))
{
profile = new NodeProfile
{
NodeName = node.GetTypeName(),
NodeGuid = guid
};
_profiles[guid] = profile;
}
profile.ExecutionCount++;
profile.Stopwatch.Restart();
_executionStack.Push(profile);
return UniTask.CompletedTask;
}
public override UniTask ExitNode(ExecutableNode node)
{
if (_executionStack.Count > 0)
{
var profile = _executionStack.Pop();
profile.Stopwatch.Stop();
profile.TotalTicks += profile.Stopwatch.ElapsedTicks;
}
return UniTask.CompletedTask;
}
public void PrintReport()
{
Debug.Log("=== Performance Profile ===");
foreach (var kvp in _profiles)
{
var profile = kvp.Value;
var avgMs = (profile.TotalTicks / (double)Stopwatch.Frequency) / profile.ExecutionCount * 1000;
var totalMs = (profile.TotalTicks / (double)Stopwatch.Frequency) * 1000;
Debug.Log($"{profile.NodeName}: " +
$"{profile.ExecutionCount} executions, " +
$"Avg: {avgMs:F3}ms, " +
$"Total: {totalMs:F3}ms");
}
}
public override void Dispose()
{
PrintReport();
_profiles.Clear();
_executionStack.Clear();
base.Dispose();
}
}
Usage:
var profiler = new PerformanceProfilerTracker();
using (profiler.Auto())
{
runtime.ProcessEvent(eventName: "Start");
}
// Report is printed automatically
Output:
=== Performance Profile ===
FlowNode_Log: 1 executions, Avg: 0.123ms, Total: 0.123ms
FlowNode_GetVariable: 1 executions, Avg: 0.045ms, Total: 0.045ms
FlowNode_Calculate: 5 executions, Avg: 0.234ms, Total: 1.170ms
Example 3: Execution Flow Visualizer
Track execution order and create a visual representation:
using System;
using System.Collections.Generic;
using System.Text;
using Ceres.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ExecutionFlowTracker : FlowGraphTracker
{
private readonly List<string> _executionOrder = new();
private int _indentLevel = 0;
public override UniTask EnterNode(ExecutableNode node)
{
var indent = new string(' ', _indentLevel * 2);
_executionOrder.Add($"{indent}→ {node.GetTypeName()}");
_indentLevel++;
return UniTask.CompletedTask;
}
public override UniTask ExitNode(ExecutableNode node)
{
_indentLevel--;
var indent = new string(' ', _indentLevel * 2);
_executionOrder.Add($"{indent}← {node.GetTypeName()}");
return UniTask.CompletedTask;
}
public void PrintFlow()
{
var sb = new StringBuilder();
sb.AppendLine("=== Execution Flow ===");
foreach (var entry in _executionOrder)
{
sb.AppendLine(entry);
}
Debug.Log(sb.ToString());
}
public override void Dispose()
{
PrintFlow();
_executionOrder.Clear();
base.Dispose();
}
}
Output:
=== Execution Flow ===
→ FlowNode_Start
→ FlowNode_GetVariable
← FlowNode_GetVariable
→ FlowNode_Log
← FlowNode_Log
← FlowNode_Start
Advanced: Conditional Tracking
Track only specific nodes or conditions:
using Ceres.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ConditionalTracker : FlowGraphTracker
{
private readonly System.Func<ExecutableNode, bool> _condition;
private int _matchedCount = 0;
public ConditionalTracker(System.Func<ExecutableNode, bool> condition)
{
_condition = condition;
}
public override UniTask EnterNode(ExecutableNode node)
{
if (_condition(node))
{
_matchedCount++;
Debug.Log($"Matched node: {node.GetTypeName()}");
}
return UniTask.CompletedTask;
}
public override void Dispose()
{
Debug.Log($"Total matched nodes: {_matchedCount}");
base.Dispose();
}
}
Usage:
// Track only nodes with "Log" in the name
var tracker = new ConditionalTracker(node => node.GetTypeName().Contains("Log"));
using (tracker.Auto())
{
runtime.ProcessEvent(eventName: "Start");
}
Constraints
1. Use TrackerAutoScope
Always use Auto() for automatic cleanup:
// Good
using (tracker.Auto())
{
runtime.ProcessEvent(eventName: "Start");
}
// Avoid
FlowGraphTracker.SetActiveTracker(tracker);
runtime.ProcessEvent(eventName: "Start");
tracker.Dispose(); // Easy to forget
2. Keep Trackers Lightweight
Trackers are called for every node execution, so keep them fast:
// Good: Simple logging
public override UniTask EnterNode(ExecutableNode node)
{
Debug.Log(node.GetTypeName());
return UniTask.CompletedTask;
}
// Avoid: Heavy operations
public override UniTask EnterNode(ExecutableNode node)
{
File.WriteAllText("log.txt", node.GetTypeName()); // Too slow!
return UniTask.CompletedTask;
}
3. Handle Async Properly
If you need async operations, use UniTask:
public override async UniTask EnterNode(ExecutableNode node)
{
await SomeAsyncOperation();
Debug.Log(node.GetTypeName());
}
4. Clean Up Resources
Always clean up in Dispose():
public override void Dispose()
{
_data.Clear();
_cache = null;
base.Dispose();
}
5. Use Conditional Compilation
Disable trackers in release builds if needed:
#if DEVELOPMENT_BUILD || UNITY_EDITOR
using (tracker.Auto())
{
runtime.ProcessEvent(eventName: "Start");
}
#else
runtime.ProcessEvent(eventName: "Start");
#endif
Performance Considerations
- Tracker callbacks run for every executable node unless
CERES_DISABLE_TRACKERis defined. - Keep callbacks lightweight and bound retained diagnostic data.
FlowGraphTrackerhas one active tracker; setting a custom tracker replaces the active tracker rather than composing with it.- An
Auto()scope disposes its tracker when the scope ends. Keep the scope alive for the complete asynchronous graph execution, or manage a long-lived tracker explicitly.
Related guides
- Learn about Custom Nodes for creating reusable logic
- Explore Debugging for editor debugging features
- Check Advanced Features for more patterns