Graph Tracker Guide
FlowGraphTracker is a powerful debugging and analysis tool that allows you to track and monitor the execution of Flow graphs. This guide explains how to use and create custom trackers for advanced debugging scenarios.
Basic Usage
Using TrackerAutoScope
The easiest way to use a tracker is with the Auto() method, which returns a TrackerAutoScope:
using Ceres.Graph.Flow;
using Ceres.Graph.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
graph.ExecuteEventAsync(context, "Start", evt);
// Clean up when done
tracker.Dispose();
Built-in Tracker: FlowGraphDependencyTracker
Ceres provides a built-in tracker that logs node execution and dependencies:
using Ceres.Graph.Flow;
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())
{
graph.ExecuteEventAsync(context, "Start", evt);
}
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.Graph.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())
{
graph.ExecuteEventAsync(context, "Start", evt);
}
// 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.Graph.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())
{
graph.ExecuteEventAsync(context, "Start", evt);
}
// 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.Graph.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
Example 4: Error Tracker
Track errors and exceptions during execution:
using System;
using System.Collections.Generic;
using Ceres.Graph.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ErrorTracker : FlowGraphTracker
{
private readonly List<ErrorEntry> _errors = new();
private struct ErrorEntry
{
public string NodeName;
public string NodeGuid;
public string ErrorMessage;
public DateTime Timestamp;
}
public override UniTask EnterNode(ExecutableNode node)
{
try
{
// Node execution happens here
// We can't catch exceptions here, but we can track which node was executing
}
catch (Exception ex)
{
_errors.Add(new ErrorEntry
{
NodeName = node.GetTypeName(),
NodeGuid = node.Guid,
ErrorMessage = ex.Message,
Timestamp = DateTime.Now
});
Debug.LogError($"Error in {node.GetTypeName()}: {ex.Message}");
}
return UniTask.CompletedTask;
}
public void PrintErrors()
{
if (_errors.Count == 0)
{
Debug.Log("No errors detected");
return;
}
Debug.LogWarning($"=== {_errors.Count} Errors Detected ===");
foreach (var error in _errors)
{
Debug.LogError($"[{error.Timestamp:HH:mm:ss}] {error.NodeName}: {error.ErrorMessage}");
}
}
public override void Dispose()
{
PrintErrors();
_errors.Clear();
base.Dispose();
}
}
Advanced: Conditional Tracking
Track only specific nodes or conditions:
using Ceres.Graph.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())
{
graph.ExecuteEventAsync(context, "Start", evt);
}
Best Practices
1. Use TrackerAutoScope
Always use Auto() for automatic cleanup:
// Good
using (tracker.Auto())
{
graph.ExecuteEventAsync(context, "Start", evt);
}
// Avoid
FlowGraphTracker.SetActiveTracker(tracker);
graph.ExecuteEventAsync(context, "Start", evt);
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())
{
graph.ExecuteEventAsync(context, "Start", evt);
}
#else
graph.ExecuteEventAsync(context, "Start", evt);
#endif
Integration with Editor Debugging
The Flow editor uses a built-in tracker for debugging. You can access it:
// In editor, the debugger tracker is automatically set
// Your custom tracker will work alongside it
Performance Considerations
- Overhead: Trackers add minimal overhead (~0.001ms per node)
- Memory: Keep tracked data minimal
- Disable in Release: Consider disabling detailed tracking in release builds
Next Steps
- Learn about Custom Nodes for creating reusable logic
- Explore Debugging for editor debugging features
- Check Advanced Features for more patterns