# How CeresGraph Compilation and Runtime Initialization Works: A Deep Dive into the JIT Pipeline

> Explore the CeresGraph JIT pipeline for efficient compilation and runtime initialization. Understand how serialized graphs become executable objects using pooled compilers and lazy evaluation.

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

---

**CeresGraph compilation and runtime initialization employ a just-in-time (JIT) model that transforms serialized graph descriptions into fully wired executable objects through pooled compilers, topological sorting, and lazy evaluation on first access.**

The **akikurisu/ceres** repository implements this system to convert static `FlowGraphData` assets into live execution engines. Understanding **CeresGraph compilation and runtime initialization** is critical for optimizing performance and minimizing garbage collection in high-frequency graph executions.

## Phase 1: Deserialization into Persistent Graph Objects

The process begins when a serialized graph definition—typically a `CeresGraphData` or derived type like `FlowGraphData`—is loaded into memory. During this phase, the raw data structure is converted into a concrete `CeresGraph` or `FlowGraph` instance.

According to the source code in [`Runtime/Core/Models/Graph/CeresGraph.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/Graph/CeresGraph.cs) (lines 45-66), the `BuildGraph` method handles the initial wiring of nodes, variables, and sub-graph slots. This creates the persistent graph object that serves as the foundation for subsequent compilation steps, establishing the node list and variable containers without yet preparing them for execution.

## Phase 2: JIT Compilation with CeresGraphCompiler

When a graph is first accessed, the **just-in-time compilation** phase begins. The `Compile` method (e.g., `FlowGraph.Compile`) triggers the transformation from static description to executable runtime.

The compilation infrastructure centers on `CeresGraphCompiler`, located in [`Runtime/Core/Models/Graph/CeresGraphCompiler.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/Graph/CeresGraphCompiler.cs). The static method `GetPooled` (lines 20-50) retrieves a compiler instance from an object pool, initializing it with references to both the **source graph** (`Source`) and the **target graph** (`Target`)—typically the same object for standard graphs. This design minimizes allocation overhead by reusing compiler state across multiple graph instances.

The compiler also supports extensibility through `ICeresGraphCompilationContext` callbacks, allowing custom logic to hook into the compilation pipeline.

### The Six-Step Compilation Sequence

Inside `CeresGraph.Compile` ([`Runtime/Core/Models/Graph/CeresGraph.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/Graph/CeresGraph.cs)), the JIT process executes six distinct phases:

1. **`SetCompilerTarget`** (lines 28-32): Assigns the `Target` reference on the compiler, enabling nodes to reference the compiled graph instance during initialization.

2. **`InitVariables`** (lines 59-71): Collects every `SharedVariable` declared by nodes, registers them on the graph's blackboard, and establishes links to their source definitions.

3. **`InitPorts`** (lines 74-88): Calls `InitializePorts` on each node instance, then links every port to its counterpart—including delegate ports—via `LinkPort` operations.

4. **`CollectDependencyPath`** (lines 93-101): Performs a topological sort of the node dependency graph and caches the execution order as `_nodeDependencyPath`. This pre-computation eliminates runtime sorting overhead during event execution.

5. **`CompileNodes`** (lines 123-131): Iterates over every node and, for those implementing `IRuntimeCompiledNode`, invokes their `Compile` method. This step handles node-specific JIT work, such as building expression delegates or optimizing internal state machines.

6. **`Blackboard.LinkToGlobal`** (lines 133-135): Connects the graph's local blackboard to any global blackboard system, enabling cross-graph variable access and shared state management.

After these steps complete, the `CeresGraphCompiler` automatically returns to the pool when the `using` block disposes it (see the `Dispose` method in [`CeresGraphCompiler.cs`](https://github.com/akikurisu/ceres/blob/main/CeresGraphCompiler.cs), lines 52-62).

## Phase 3: Runtime Execution and Event Handling

Once compilation finishes, the graph enters the execution phase. The compiled `FlowGraph` maintains an array of `ExecutableEvent` objects representing entry points such as functions and custom events (defined in [`Runtime/Flow/Models/Nodes/CustomFunctions/FlowNode_ExecuteCustomFunction.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/Nodes/CustomFunctions/FlowNode_ExecuteCustomFunction.cs), lines 30-38).

Runtime components implement `IFlowGraphRuntime` (typically MonoBehaviours) to interact with the graph. When executing an event, the extension method `ProcessEvent` from `FlowGraphRuntimeExtensions` creates an `ExecuteFlowEvent` instance, obtains a callback handler via `FlowGraph.GetOrCreateEventHandler`, and forwards execution through an `ExecutionContext` (see [`Runtime/Flow/Models/FlowGraph.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/FlowGraph.cs), lines 31-40).

The handler extracts the event name from the `EventBase` using `CustomExecutionEvent.GetEventName`, locates the matching `ExecutableEvent` in the `FlowGraph.Events` array, and creates a pooled `ExecutionContext`. The system then asynchronously traverses the node chain using the pre-computed dependency path from Phase 2.

## Key Optimizations in CeresGraph Runtime

The **CeresGraph compilation and runtime initialization** pipeline implements several performance-critical optimizations:

- **Lazy compilation**: Graphs compile only when first accessed via the `Graph` property or `GetOrCreateEventHandler`, deferring initialization costs until actually needed.

- **Object pooling**: Both `CeresGraphCompiler` instances and per-execution `ExecutionContext` objects utilize `ObjectPool<T>` and `ListPool<T>` to eliminate GC pressure during repeated graph executions.

- **Dependency path caching**: The topological sort result (`_nodeDependencyPath`) persists on the graph instance, allowing event execution to walk a pre-computed index list rather than recalculating dependencies dynamically.

- **Blackboard linking**: Post-compilation linking to global blackboards enables efficient variable sharing across graph boundaries without marshaling overhead.

## Practical Implementation Example

The following pattern demonstrates explicit compilation and event execution:

```csharp
// Load serialized asset
var data = myFlowGraphAsset; // FlowGraphData
var flowGraph = data.CreateFlowGraphInstance();

// Explicit compilation (optional - happens automatically on first use)
using var compiler = CeresGraphCompiler.GetPooled(flowGraph, null);
flowGraph.Compile(compiler);

// Runtime execution from a MonoBehaviour
public class MyBehaviour : MonoBehaviour, IFlowGraphRuntime
{
    public FlowGraph Graph => flowGraph;
    public UnityEngine.Object Object => this;

    void Start()
    {
        // Fires "OnStart" event using CallerMemberName attribute
        this.ProcessEvent();
    }

    void Update()
    {
        // Pass parameters to the graph execution
        this.ProcessEvent(Time.deltaTime, "player");
    }
}

```

## Summary

- **CeresGraph compilation and runtime initialization** follow a three-phase pipeline: deserialization, JIT compilation via `CeresGraphCompiler`, and event-driven execution.
- The `CeresGraphCompiler` uses object pooling and implements a six-step sequence including variable initialization, port linking, topological sorting, and node-specific compilation.
- Runtime execution leverages pre-cached dependency paths and pooled `ExecutionContext` instances to minimize overhead.
- Graphs link to global blackboards after compilation, enabling cross-graph variable sharing.
- Compilation occurs lazily on first access, optimizing memory usage for inactive graph instances.

## Frequently Asked Questions

### What triggers CeresGraph compilation?

Compilation triggers automatically upon first access to the `Graph` property or when calling `GetOrCreateEventHandler`. This lazy initialization pattern ensures that graphs incur initialization costs only when actively used, improving scene load times and memory efficiency for dormant graph assets.

### How does CeresGraph minimize garbage collection during execution?

The system employs extensive object pooling through `ObjectPool<T>` and `ListPool<T>` for both the `CeresGraphCompiler` instances and per-execution `ExecutionContext` objects. Additionally, the topological dependency path is computed once during compilation and cached as `_nodeDependencyPath`, eliminating the need for runtime list allocations during event processing.

### Can I customize the compilation process for specific node types?

Yes. Nodes implementing `IRuntimeCompiledNode` receive a `Compile` callback during the `CompileNodes` phase (lines 123-131 of [`CeresGraph.cs`](https://github.com/akikurisu/ceres/blob/main/CeresGraph.cs)). This allows node implementations to perform JIT optimizations such as pre-building expression delegates or caching reflection-heavy lookups before runtime execution begins.

### What is the relationship between Source and Target in CeresGraphCompiler?

In most graph configurations, `Source` and `Target` reference the same graph instance. The `Source` represents the original graph definition being compiled, while `Target` represents the compiled output. This separation supports advanced scenarios where compilation might generate a different runtime representation, though standard flows maintain identity between both references.