# How to Use CGraph's Perf Profiling for Performance Bottleneck Analysis

> Uncover performance bottlenecks with CGraph perf profiling. Automatically identify critical paths and visualize compute time spent to optimize your code. Integrate GPerfAspect seamlessly.

- Repository: [Chunel/cgraph](https://github.com/chunelfeng/cgraph)
- Tags: performance
- Published: 2026-02-27

---

**TLDR:** CGraph's built-in `perf()` API injects a `GPerfAspect` into every pipeline element to record execution timestamps, automatically identifies the longest critical path, and generates a Graphviz DOT file that visualizes exactly where compute time is spent.

CGraph is an open-source C++ DAG framework maintained at `chunelfeng/cgraph`. Its **perf profiling** system allows developers to analyze pipeline performance without modifying individual node implementations. By capturing fine-grained timing data across the graph execution, you can pinpoint hot spots and understand end-to-end latency drivers.

## Core Profiling Architecture

The profiling system operates through three coordinated components that inject instrumentation, collect metrics, and render results.

### GPerfAspect: The Timing Instrumentation

Located in [`src/GraphCtrl/GraphPipeline/_GPerf/GPerfAspect.h`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphPipeline/_GPerf/GPerfAspect.h), the `GPerfAspect` class is a template-aspect that attaches to every `GElement`. It stores a `GPerfInfo` struct for each node and updates timing fields—`first_start_ts_`, `last_finish_ts_`, `accu_cost_ts_`, and loop counts—during the `beginRun` and `finishRun` lifecycle hooks.

### GPerf: The Orchestration Engine

The static helper class `GPerf`, implemented in [`src/GraphCtrl/GraphPipeline/_GPerf/GPerf.cpp`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphPipeline/_GPerf/GPerf.cpp), manages the full profiling workflow. It handles aspect injection via `GPerf::inject`, executes the pipeline, marks the longest critical path with `GPerf::markLongestPath`, dumps the collected data, and finally restores the original pipeline state through `GPerf::recover`.

### Public API Entry Points

For C++ applications, the entry point is `GPipeline::perf`, defined in [`src/GraphCtrl/GraphPipeline/GPipeline.cpp`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphPipeline/GPipeline.cpp) (lines 219-225). Python bindings expose the same functionality through `GPipeline::__perf_4py` in [`src/python/PyCGraph.cpp`](https://github.com/chunelfeng/cgraph/blob/main/src/python/PyCGraph.cpp) (lines 219-447), allowing `pipeline.perf()` calls from Python scripts.

## The Five-Step Profiling Workflow

When you invoke `pipeline->perf(oss)`, the following sequence executes:

1. **Inject Aspect** – `GPerf::inject` allocates a `GPerfInfo` object for each element and attaches `GPerfAspect<CFMSec, GPerfInfoPtr>`, capturing the pipeline start time once.

2. **Run Pipeline** – `pipeline->process()` executes the graph normally. The aspect records first start, last finish, loop count, and accumulated cost for every node.

3. **Identify Longest Path** – `GPerf::markLongestPath` walks all execution paths collected by `GOptimizer::collectPaths` and flags the sequence with the greatest total `accu_cost_ts_`.

4. **Dump Visualization** – `pipeline->dump(oss)` emits a Graphviz DOT description. Each node label includes average cost, total cost, and loop count. Nodes on the longest path receive a distinct visual style (red border) as implemented in [`src/GraphCtrl/GraphElement/GElement.cpp`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphElement/GElement.cpp) (lines 426-434).

5. **Recover State** – `GPerf::recover` removes the injected aspects and frees temporary `GPerfInfo` objects, leaving the pipeline unchanged for subsequent production runs.

## Profiling a C++ Pipeline

The following minimal example demonstrates how to profile a three-node pipeline:

```cpp
// tutorial_perf.cpp
#include "MyGNode/MyNode1.h"
#include "MyGNode/MyNode2.h"

using namespace CGraph;

int main() {
    // 1. Build a simple pipeline
    GPipelinePtr pipeline = GPipelineFactory::create();

    GElementPtr a, b, c = nullptr;
    a = pipeline->createGNode<MyNode1>(GNodeInfo({}, "nodeA", 1));
    b = pipeline->createGNode<MyNode2>(GNodeInfo({a}, "nodeB", 2));   // runs twice
    c = pipeline->createGNode<MyNode1>(GNodeInfo({b}, "nodeC", 1));

    // 2. Register the nodes (optional, same as createGNode)
    pipeline->registerGElement<MyNode1>(&a, {}, "nodeA", 1);
    pipeline->registerGElement<MyNode2>(&b, {a}, "nodeB", 2);
    pipeline->registerGElement<MyNode1>(&c, {b}, "nodeC", 1);

    // 3. Execute the pipeline normally
    CStatus status = pipeline->process();
    CGRAPH_ECHO("process status: %d", status.getCode());

    // 4. Run the profiler
    std::ostringstream oss;
    status = pipeline->perf(oss);          // ← core profiling call
    CGRAPH_ECHO("perf status: %d", status.getCode());

    // 5. Show the DOT output (or write to file)
    std::cout << oss.str() << std::endl;
    // Paste the output into https://dreampuf.github.io/GraphvizOnline/ to view the graph.
    GPipelineFactory::remove(pipeline);
    return 0;
}

```

The key invocation is `pipeline->perf(oss)`, which triggers the complete profiling chain and writes the Graphviz description to the output stream.

## Python Profiling Usage

For Python workflows, the profiler is accessible through the PyCGraph wrapper:

```python
from pycgraph import GPipelineFactory

pipeline = GPipelineFactory.create()

# … build pipeline with Python wrapper (see pycgraph docs) …

dot = pipeline.perf()                     # returns a string containing DOT

print(dot)                               # or write to "graph.dot"

# Visualise with Graphviz online or `dot -Tpng graph.dot -o graph.png`

```

The `perf()` method forwards to `GPipeline::__perf_4py`, executing the same C++ instrumentation layer while returning the DOT string directly to the Python environment.

## Interpreting Performance Results

The generated DOT file contains node labels with precise timing statistics. A typical entry appears as:

```

nodeB [label="nodeB
[start 12.34ms, finish 45.67ms,
per_cost 33.33ms, total_cost 66.66ms, loop 2]"];

```

**Key metrics to analyze:**

- **per_cost** – Average execution time per loop iteration. High values indicate CPU-intensive nodes.
- **total_cost** – Accumulated wall-clock time across all executions (`accu_cost_ts_`).
- **loop** – Number of times the node was invoked.
- **longest path highlighting** – Nodes with `in_longest_path_=true` are rendered with a red border, revealing which sequential chain dominates end-to-end latency.

Paste the DOT output into a Graphviz renderer such as [GraphvizOnline](https://dreampuf.github.io/GraphvizOnline/) to visualize the critical path overlay.

## Summary

- **CGraph's perf profiling** requires no code changes to individual nodes; it works via aspect injection through `GPerfAspect`.
- The profiler captures `first_start_ts_`, `last_finish_ts_`, and `accu_cost_ts_` for every `GElement` during `beginRun`/`finishRun`.
- The `GPerf` helper in [`src/GraphCtrl/GraphPipeline/_GPerf/GPerf.cpp`](https://github.com/chunelfeng/cgraph/blob/main/src/GraphCtrl/GraphPipeline/_GPerf/GPerf.cpp) orchestrates injection, execution, longest-path analysis, and cleanup.
- Invoke profiling in C++ via `pipeline->perf(oss)` or in Python via `pipeline.perf()`, both defined in their respective `GPipeline` implementations.
- Output is a standard Graphviz DOT file that highlights the longest critical path and per-node timing metrics for immediate bottleneck identification.

## Frequently Asked Questions

### How do I enable profiling without modifying my node implementations?

CGraph's aspect-oriented design allows `GPerfAspect` to attach transparently to any `GElement`. Simply call `pipeline->perf(oss)` after building your pipeline; the framework automatically injects timing instrumentation into every node and removes it after the profiling run, leaving your node code untouched.

### What does the "longest path" represent in the DOT output?

The longest path is the execution sequence with the maximum cumulative `accu_cost_ts_` from start to finish. `GPerf::markLongestPath` calculates this by walking all paths collected via `GOptimizer::collectPaths`. Nodes on this path are visually highlighted (typically with red borders) because they represent the bottleneck chain that determines total pipeline latency.

### Can I profile a pipeline that runs multiple iterations or has dynamic elements?

Yes. The profiler records `first_start_ts_` on initial entry and `last_finish_ts_` on final exit, while accumulating total cost and loop counts. This design supports nodes that execute multiple times (e.g., `loop > 1`) or pipelines with conditional logic, providing accurate average costs (`per_cost`) across all invocations.

### Where can I visualize the generated Graphviz DOT file?

The string returned by `pipeline->perf()` or `pipeline.perf()` is a valid Graphviz DOT description. You can paste it directly into online renderers like [GraphvizOnline](https://dreampuf.github.io/GraphvizOnline/), use the command-line `dot -Tpng graph.dot -o graph.png`, or import it into any Graphviz-compatible visualization tool.