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

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, 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, 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 (lines 219-225). Python bindings expose the same functionality through GPipeline::__perf_4py in 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 AspectGPerf::inject allocates a GPerfInfo object for each element and attaches GPerfAspect<CFMSec, GPerfInfoPtr>, capturing the pipeline start time once.

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

  3. Identify Longest PathGPerf::markLongestPath walks all execution paths collected by GOptimizer::collectPaths and flags the sequence with the greatest total accu_cost_ts_.

  4. Dump Visualizationpipeline->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 (lines 426-434).

  5. Recover StateGPerf::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:

// 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:

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 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 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, use the command-line dot -Tpng graph.dot -o graph.png, or import it into any Graphviz-compatible visualization tool.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →