Continuous Profiling
π Quick Start
This guide uses build/docker/docker-compose.yml to start all services, create a host CPU profiling job, and view the flame graph in Grafana.
1. Start Services
First, configure Elasticsearch credentials so that huatuo-bamai and huatuo-apiserver can persist profile data. The config files are volume-mounted into the containers, so edit them directly in the project root.
In huatuo-bamai.conf:
[Storage]
[Storage.Elasticsearch]
Address = "http://127.0.0.1:9200"
Index = "huatuo_bamai"
Username = "elastic"
Password = "huatuo-bamai"
In huatuo-apiserver.conf:
[Elasticsearch]
Address = "http://127.0.0.1:9200"
Username = "elastic"
Password = "huatuo-bamai"
Index = "huatuo_bamai"
[Auth]
[[Auth.Users]]
ID = "administrator"
BearerToken = "REPLACE_WITH_RANDOM_HEX"
Admin = true
Then start all services from the project root:
docker compose --project-directory ./build/docker up
Run without
-dto observe startup logs. Add-dfor background mode.
| Service | Role | Default Port |
|---|---|---|
huatuo-bamai |
Agent, runs profiler sampling | 19704 |
huatuo-apiserver |
API entry point, creates and dispatches jobs | 12740 |
elasticsearch |
Stores profile data (index: huatuo_bamai) |
9200 |
grafana |
Flame graph visualization | 3000 |
2. Verify Services
In a new terminal, confirm all services are ready:
# Agent health check
$ curl -s http://localhost:19704/version | jq .data.name
"huatuo-bamai"
# API Server health check
$ curl -s http://localhost:12740/version | jq .data.name
"huatuo-apiserver"
# ES index status
$ curl -s -u elastic:huatuo-bamai "http://localhost:9200/_cat/indices/huatuo_bamai?v"
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open huatuo_bamai 147fzHJhQ820GjCKFLh5ZQ 1 1 42 0 297.8kb 297.8kb 297.8kb
Set environment variables for subsequent API calls:
API_BASE="http://127.0.0.1:12740"
API_TOKEN="REPLACE_WITH_RANDOM_HEX"
3. Create a Host CPU Profiling Job
Use c language (native, covers C/C++/Go) to sample the entire host for 30 seconds:
# Use the actual hostname of the node
HOSTNAME=$(hostname)
JOB_ID=$(curl -s -X POST \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"cpu\",
\"language\": \"c\",
\"duration_seconds\": 30,
\"hostname\": \"${HOSTNAME}\"
}" \
"${API_BASE}/v1/profiles" | jq -r .data.id)
echo "Job ID: $JOB_ID"
4. Verify Data in Elasticsearch
Each aggregation window is 10 seconds. A 30-second job produces approximately 3 profile documents. Wait for completion, then verify:
$ curl -s -u elastic:huatuo-bamai "http://localhost:9200/huatuo_bamai/_count" \
-H "Content-Type: application/json" \
-d '{"query":{"exists":{"field":"tracer_data.flamedata"}}}' | jq .count
3
5. View the Flame Graph in Grafana
Open the Continuous Profiling (host) dashboard:
- URL: http://localhost:3000/d/continuous-profiling-host (replace
localhost:3000with your environment) - Credentials:
admin / admin(skip the default password change prompt)
Steps:
- Select a time range covering the profiling period
- Choose your
hostnameand settypetoprocess_cpu:cpu:nanoseconds:cpu:nanoseconds - The flame graph loads aggregated call stacks for the selected time range and updates dynamically
- Click a frame to zoom in; use the top table for symbol sorting, filtering, and statistics

For more profiling dimensions, see the Profiles API section below.
π Profiles API
huatuo-apiserver exposes /v1/profiles for service-based continuous profiling. Clients can create CPU or memory profiling jobs, query job status and results, and stop or delete jobs. huatuo-apiserver schedules each job on the HUATUO Agent running on the specified node. Profiling results are available through the returned Grafana URL or the raw data endpoint.
1. Request Conventions
By default, huatuo-apiserver listens on :12740. The following examples use environment variables for the server address and bearer token:
API_BASE="http://127.0.0.1:12740"
API_TOKEN="REPLACE_WITH_RANDOM_HEX"
Every request must pass the configured bearer token:
Authorization: Bearer REPLACE_WITH_RANDOM_HEX
A non-administrator user requires both /v1/profiles and
/v1/profiles/** permissions. Permissions may include an HTTP method, such as
GET /v1/profiles/**. The API uses the following common JSON response
envelope:
{
"code": 0,
"message": "success",
"data": {}
}
2. Query Profiling Capabilities
Before creating a job, query the profiling types, languages, CPU modes, memory modes, and runtime settings supported by the server:
curl -sS \
-H "Authorization: Bearer ${API_TOKEN}" \
"${API_BASE}/v1/profiles/capabilities"
The data object contains these fields:
| Field | Description |
|---|---|
types |
Supported profiling types: cpu and memory |
cpu_languages |
Languages supported by CPU profiling |
cpu_modes |
CPU profiling modes grouped by language |
memory_languages |
Languages supported by memory profiling |
memory_modes |
Memory profiling modes grouped by language; values are accepted by job creation |
aggregation_interval_seconds |
Server-side data aggregation interval |
max_concurrent_profilers |
Maximum number of concurrent profiler processes; 0 disables the limit |
CPU profiling supports oncpu and offcpu for c, c++, and go;
java and python support only oncpu. Memory profiling supports these combinations:
| Language | memory_mode |
Description |
|---|---|---|
c, c++, go |
virtual_alloc |
Virtual address-space allocation |
c, c++, go |
physical_alloc |
Physical page allocation |
c, c++, go |
physical_usage |
Current physical page residency |
java |
object_alloc |
JVM object allocation |
java |
object_usage |
JVM live objects |
3. Create a Profiling Job
POST /v1/profiles accepts the following JSON fields:
| Field | Required | Description |
|---|---|---|
type |
Yes | Profiling type: cpu or memory |
language |
Yes | Target process language; it must support the selected profiling type |
duration_seconds |
Yes | Profiling duration in seconds |
hostname |
Yes | Hostname of the node running the target process; used for job scheduling |
container_id |
No | Target container ID; omit it to profile the host |
binary_match_path |
No | Executable path matcher for Java/Python CPU profiling; native profiling does not support it |
memory_mode |
For memory profiling | Memory profiling mode; it must be supported by language |
duration_seconds must cover at least two aggregation_interval_seconds periods, and their sum must be less than 3600 seconds. If the same user already has a running profiling job on the same node, the server returns 409 Conflict.
Create a Go CPU profiling job on a host:
curl -sS -i \
-X POST \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"type": "cpu",
"language": "go",
"duration_seconds": 60,
"hostname": "node-01"
}' \
"${API_BASE}/v1/profiles"
Create a Java live-object profiling job in a container:
curl -sS -i \
-X POST \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"type": "memory",
"language": "java",
"memory_mode": "object_usage",
"duration_seconds": 60,
"container_id": "9f4c2f1a8b7d",
"hostname": "node-01"
}' \
"${API_BASE}/v1/profiles"
A successful request returns 201 Created. The Location response header identifies the new job, and the response body contains the job ID used by subsequent requests:
{
"code": 0,
"message": "success",
"data": {
"id": "<profile-job-id>"
}
}
JOB_ID="<profile-job-id>"
4. List Profiling Jobs
GET /v1/profiles supports these query parameters:
| Parameter | Default | Description |
|---|---|---|
container_id |
None | Exact container ID filter (containerID remains accepted for compatibility) |
hostname |
None | Exact node hostname filter |
status |
None | pending, running, completed, failed, stopped, or timeout |
type |
None | cpu or memory; omit it to return both types |
limit |
50 |
Page size; must be greater than 0 and is capped at 500 |
offset |
0 |
Starting offset; must be greater than or equal to 0 |
sort |
-created_at |
created_at, finished_at, hostname, container_id, id, status, or type; prefix with - for descending order |
List the 20 most recent running CPU profiling jobs on node-01:
curl -sS -G \
-H "Authorization: Bearer ${API_TOKEN}" \
--data-urlencode "hostname=node-01" \
--data-urlencode "status=running" \
--data-urlencode "type=cpu" \
--data-urlencode "limit=20" \
--data-urlencode "offset=0" \
--data-urlencode "sort=-created_at" \
"${API_BASE}/v1/profiles"
data.items contains the job array. data.total is the number of matching jobs before pagination, while data.limit and data.offset are the effective pagination parameters. Non-administrator users can list only jobs they created.
5. Get a Profiling Job
curl -sS \
-H "Authorization: Bearer ${API_TOKEN}" \
"${API_BASE}/v1/profiles/${JOB_ID}"
The data object contains the job details:
| Field | Description |
|---|---|
id |
Profiles API job ID |
container_id |
Target container ID; omitted for host jobs |
hostname |
Target node hostname |
type |
cpu or memory |
language |
Target process language |
memory_mode |
Memory profiling mode; omitted for CPU jobs |
binary_match_path |
Executable path matcher; omitted when unused |
status |
Current job status |
duration_seconds |
Requested profiling duration in seconds |
created_at |
Job creation time |
finished_at |
Terminal status time; null while the job is active |
result_url |
Grafana URL for the result; null until available |
status_reason |
Terminal status details; null when no explanation is needed |
Profiling jobs use these statuses:
| Status | Description |
|---|---|
pending |
The job has been created and is waiting for the Agent |
running |
The Agent is collecting profiling data |
completed |
The job completed successfully |
stopped |
The user or job manager stopped the job |
failed |
The job failed; inspect status_reason for the cause |
timeout |
The job exceeded its allowed execution time |
6. Get Raw Profiling Data
GET /v1/profiles/:id/raw returns the raw profiling windows associated with the job. The response can be large, so it can be written directly to a file:
curl -sS \
-H "Authorization: Bearer ${API_TOKEN}" \
-o profile-raw.json \
"${API_BASE}/v1/profiles/${JOB_ID}/raw?limit=100&offset=0"
The profiling windows are in data.items; data.limit, data.offset, and
data.has_more describe the page. Each item contains uploaded_at,
captured_at, profile_type, and the pprof-compatible profile payload.
7. Stop a Profiling Job
Only jobs in pending or running status can be stopped. The PATCH request accepts only stopped as the status value:
curl -sS \
-X PATCH \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status":"stopped"}' \
"${API_BASE}/v1/profiles/${JOB_ID}"
A successful stop returns 200 OK. A job that has already ended returns 400 Bad Request.
8. Delete a Profiling Job
Deletion removes only the job record. Jobs in pending or running status cannot be deleted directly and must be stopped first:
curl -sS -i \
-X DELETE \
-H "Authorization: Bearer ${API_TOKEN}" \
"${API_BASE}/v1/profiles/${JOB_ID}"
A successful deletion returns 204 No Content with no response body. If the job is still active, the endpoint returns 409 Conflict.
π profiler CLI Overview
profiler is HUATUO’s standalone performance profiling CLI. It samples host processes or processes inside containers without requiring huatuo-apiserver, Elasticsearch, or Grafana. The tool supports C, C++, Go, Java, and Python processes and writes call stacks as folded stacks or SVG flame graphs.
C, C++, and Go use the eBPF-based native collector to observe on-CPU usage, off-CPU blocking and scheduling delay, virtual memory allocation, physical memory allocation, and physical memory residency. Java uses async-profiler to observe CPU usage, object allocation, and live objects. Python uses py-spy to observe CPU usage. The results can be used to locate hot functions, attribute memory growth, analyze processes inside containers, and preserve performance data for later diagnosis.
The remainder of this section covers standalone use of _output/bin/profiler. For service-based continuous profiling, see the Profiles API section above.
π― Use Cases
1. Locate CPU Hotspots and Call Paths
Sample the call stacks of C, C++, Go, Java, or Python processes at a fixed frequency and use stack width to identify the primary consumers of CPU time. The native collector can also limit sampling to selected CPUs with --cpuid, which is useful for analyzing CPU-pinned workloads or per-CPU hotspots.
2. Attribute Native Process Memory
Observe virtual address-space allocation, physical page allocation, and current physical page residency for C, C++, and Go processes. These modes distinguish between how much address space was requested, how much physical memory was allocated, and how much physical memory remains resident. They help locate call paths responsible for mmap activity, page-fault allocation, and resident memory growth.
3. Analyze JVM Object Allocation and Live Objects
Use async-profiler to collect Java object allocation or live-object call stacks. Object allocation profiles help locate high allocation rates and sources of GC pressure. Live-object profiles help identify objects that remain referenced during the collection window and the paths where they were allocated.
4. Analyze Containers and Multi-process Workloads
Use a container ID to resolve and profile target processes inside Docker or containerd workloads. Java and Python also accept comma-separated PID lists and can limit the number of concurrently running collector subprocesses, which is useful for service replicas and parent-child process groups.
π Usage
1. Build and Runtime Requirements
Build all artifacts from the repository root:
make all
The resulting executable is _output/bin/profiler. Native profiling depends on Linux eBPF, perf events, and the BPF objects built from this repository. It generally requires root privileges and a kernel.perf_event_paranoid setting that permits sampling. Java profiling requires async-profiler; --tool-path must point to a directory containing bin/asprof and lib/libasyncProfiler.so. Python profiling requires py-spy; --tool-path must point to a directory containing the py-spy executable.
Display the complete help for the current version:
_output/bin/profiler --help
The basic command structure is:
sudo _output/bin/profiler \
--type <cpu|memory> \
--language <c|c++|go|java|python> \
--pid <pid> \
--duration 30 \
--aggr-interval 10 \
--output-format flamegraph \
--output-path ./profiles
--type and --language are required. Java, Python, and native memory profiling require exactly one target specified with either --pid or --container-id. Native CPU profiling can sample the entire host when neither target is specified.
2. General CLI Options
| Option | Default | Scope | Description |
|---|---|---|---|
--type, -t |
None | All | Profile type: cpu or memory; required |
--language, -l |
None | All | Target language: c, c++, go, java, or python; required |
--pid, -p |
None | All | Target PID; Java and Python accept comma-separated PIDs, while native profiling accepts at most one PID |
--container-id |
None | All | Target container ID; mutually exclusive with --pid |
--duration, -d |
10 |
All | Total profiling duration in seconds; minimum 1 |
--aggr-interval |
10 |
All | Aggregation interval in seconds; must not exceed the duration |
--freq, -F |
99 |
CPU | Samples collected per second; maximum 1000 for Java |
--output-path |
. |
Local output | Output directory, not an output file name |
--output-format |
collapsed |
All | collapsed, flamegraph, svg, or remote |
--output-storage |
/var/run/huatuo-toolstream.sock |
remote |
Unix socket used for remote upload |
--max-concurrent-procs |
0 |
Java, Python | Maximum concurrent collector subprocesses; 0 means unlimited |
--tool-path |
None | Java, Python | Third-party profiler root directory; required |
--binary-match-path |
None | Java, Python | Executable path used to match target processes |
--huatuo-api-address |
127.0.0.1:19704 |
Container targets | HUATUO API address used to resolve container metadata |
--tracer-id |
Empty; generated internally for local output | All; required for remote |
Stable profiling task ID used by toolstream and remote storage |
--enable-pprof |
false |
Profiler itself | Expose Go pprof endpoints for the profiler process on :6000 |
--version-format |
text |
Version query | Output format for --version: text, json, or short |
--help, -h |
- | All | Display command help |
--version, -v |
- | All | Display version and build information |
Native profiling options:
| Option | Default | Scope | Description |
|---|---|---|---|
--memory-mode |
None | Native memory, Java memory | Memory profiling mode; required with --type memory |
--cpuid |
All CPUs | Native CPU | Comma-separated CPU list or ranges; off-CPU samples use the task’s switch-out CPU |
--cpu-mode |
oncpu |
Native CPU | oncpu for frequency sampling or offcpu for blocked/runqueue time attribution |
--require-hardware-pmu |
false |
Native on-CPU | Require hardware PMU sampling; fail instead of falling back to the software CPU clock |
--offcpu-phase |
all |
Native off-CPU | Accumulate all, blocked, or runqueue time |
--offcpu-min-duration-us |
1000 |
Native off-CPU | Discard phases shorter than this duration in microseconds |
--offcpu-stats |
false |
Native off-CPU | Collect BPF diagnostic statistics; adds overhead to error and cleanup paths |
--thread-group |
false |
Native | Also profile other threads in the target PID’s thread group |
--physical-memory-probability |
100 |
Native physical memory | Physical memory event sampling probability from 1 to 100 |
--log-bpf-debug |
false |
Native | Emit BPF debug events; not recommended for normal profiling |
Logging options:
| Option | Default | Description |
|---|---|---|
--log-level |
error |
trace, debug, info, warn, or error |
--log-file |
stdout |
Log file path, or stdout |
--log-size |
100 |
Log rotation size in MB; 0 disables rotation; applies only to file output |
--verbose |
false |
Equivalent to --log-level debug --log-file stdout and overrides both explicit logging options |
3. Observing C, C++, and Go
C, C++, and Go use the same native eBPF collector; only the --language value changes. CPU mode counts call-stack samples and includes user-space and kernel-space stacks when symbols can be resolved.
sudo _output/bin/profiler \
--type cpu \
--language go \
--pid 12345 \
--duration 30 \
--aggr-interval 10 \
--freq 99 \
--output-format flamegraph \
--output-path ./profiles/go-cpu
Add --thread-group to include worker threads in the same process. Add --cpuid 2,4-7 to limit collection to selected CPUs. Native CPU profiling also supports container-level and host-level collection:
Native on-CPU profiling first uses hardware CPU-cycle events and falls back to
the software CPU clock when the hardware PMU is unavailable. --freq remains
samples per second for either source. Use --require-hardware-pmu when software
clock fallback would hide IRQ-disabled CPU time.
# Profile a specific container
sudo _output/bin/profiler \
--type cpu --language c --container-id <container-id> \
--duration 30 --aggr-interval 10 \
--output-format collapsed --output-path ./profiles/container
# Profile the host without specifying a PID or container
sudo _output/bin/profiler \
--type cpu --language c \
--duration 30 --aggr-interval 10 \
--output-format flamegraph --output-path ./profiles/host
To attribute time spent outside the CPU to the call path that descheduled, select off-CPU mode:
sudo _output/bin/profiler \
--type cpu --language go --pid 12345 --thread-group \
--cpu-mode offcpu --offcpu-phase all \
--cpuid 2,4-7 \
--offcpu-min-duration-us 1000 \
--duration 30 --aggr-interval 10 \
--output-format flamegraph --output-path ./profiles/go-offcpu
Off-CPU output is event-driven, so --freq does not apply. With --cpuid, an interval is collected only when the task switches out from a selected CPU; later wakeup or switch-in on another CPU does not change that attribution. Flame graphs use nanoseconds directly and add roots such as off-CPU blocked, scheduling delay (preempted), and scheduling delay (yielded). The all phase includes blocked and runqueue time but keeps them separated by these roots. A single stable BPF stack map is used so a long sleep cannot be resolved against a later rotating stack-map generation.
Native memory profiling supports these dimensions:
--memory-mode |
Measurement | Suitable for |
|---|---|---|
virtual_alloc |
Virtual address-space allocation and its call stacks | Excessive mmap activity and address-space growth |
physical_alloc |
Physical memory newly allocated during the collection window | Physical page allocation triggered by page faults and allocation-rate analysis |
physical_usage |
Physical memory still resident at collection time | Sources of resident memory and paths retaining physical pages |
sudo _output/bin/profiler \
--type memory \
--language c++ \
--memory-mode physical_usage \
--pid 12345 \
--thread-group \
--physical-memory-probability 100 \
--duration 30 \
--aggr-interval 10 \
--output-format flamegraph \
--output-path ./profiles/native-memory
--physical-memory-probability applies only to physical_alloc and physical_usage. Lowering it reduces processing for high-frequency memory events, but flame-graph values are then estimates based on sampled events rather than counts of every event.
4. Observing Java
Java CPU profiling depends on async-profiler. It supports a single PID, a container, or multiple PIDs:
_output/bin/profiler \
--type cpu \
--language java \
--pid 12345,12346 \
--tool-path /opt/async-profiler \
--max-concurrent-procs 2 \
--duration 30 \
--aggr-interval 10 \
--freq 99 \
--output-format flamegraph \
--output-path ./profiles/java-cpu
Java memory profiling supports two dimensions:
--memory-mode |
Measurement | Suitable for |
|---|---|---|
object_alloc |
Objects allocated during the collection window and their allocation call stacks | High allocation rates, short-lived objects, and sources of GC pressure |
object_usage |
Live objects and their allocation call stacks | Long-lived objects, sources of heap usage, and suspected memory leaks |
_output/bin/profiler \
--type memory \
--language java \
--memory-mode object_usage \
--pid 12345 \
--tool-path /opt/async-profiler \
--duration 30 \
--aggr-interval 10 \
--output-format flamegraph \
--output-path ./profiles/java-memory
To target a container, replace --pid with --container-id <container-id>. If a container has multiple candidate processes, use --binary-match-path to select the target executable path.
5. Observing Python
Python currently supports CPU profiling only. --aggr-interval must equal --duration, so one collection produces one aggregation window. --tool-path must point to the directory containing py-spy.
_output/bin/profiler \
--type cpu \
--language python \
--pid 12345,12346 \
--tool-path /opt/py-spy \
--max-concurrent-procs 2 \
--duration 30 \
--aggr-interval 30 \
--freq 99 \
--output-format flamegraph \
--output-path ./profiles/python-cpu
Python does not support --type memory. Use a separate memory analysis tool for Python memory profiling; the current profiler command does not invoke memray to generate Python memory profiles.
6. Choosing a Flame Graph and Output Format
| Format | Output | When to use it |
|---|---|---|
collapsed |
perf_<Unix timestamp>.folded; each line contains a semicolon-separated call stack followed by a count |
Scripted searches, result comparison, or rendering later with another flame-graph tool |
flamegraph |
flamegraph_<Unix timestamp>.svg; an SVG with embedded interaction scripts |
Default format for manual analysis; supports searching, zooming, and inspecting frame values in a browser |
svg |
The same interactive SVG as flamegraph |
Compatibility with callers that explicitly request SVG; currently equivalent to flamegraph |
remote |
No local flame graph; uploads pprof-compatible data through a Unix socket | Integration with the HUATUO storage pipeline; not suitable for offline viewing |
A flame graph shows the call direction from bottom to top. Rectangle width represents the cumulative value for that call stack in the selected profiling mode. For CPU profiles, width represents the proportion of CPU time derived from sample counts. For memory profiles, it represents virtual allocation, physical allocation, physical residency, Java object allocation, or live-object volume, depending on the selected mode. Horizontal position does not represent chronological order.
Example folded stacks:
main;handleRequest;parsePayload 428
main;handleRequest;writeResponse 172
Choose collapsed when you need to retain raw data and later render it with different colors or filters. Choose flamegraph when you want to inspect hotspots directly. remote depends on the HUATUO toolstream Unix socket, requires a non-empty --tracer-id, and should not be selected for standalone offline use.
7. Reproducing Integration Test Examples
The repository’s integration tests provide executable end-to-end examples. Each test creates a target process, runs profiler, and verifies the expected call stack in the output:
# Native CPU
sudo ./integration/run.sh test_profiler_native_cpu.sh
# Native off-CPU blocking and scheduling delay
sudo ./integration/run.sh test_profiler_native_cpu_offcpu.sh
# Native virtual and physical memory
sudo ./integration/run.sh test_profiler_native_mem_virtual_alloc.sh
sudo ./integration/run.sh test_profiler_native_mem_physical_usage.sh
# Java CPU and memory
sudo ./integration/run.sh test_profiler_java_cpu_multi_pid.sh
sudo ./integration/run.sh test_profiler_java_memory_usage_alloc.sh
# Python multi-process CPU
sudo ./integration/run.sh test_profiler_python_cpu_multi_pid.sh
Container, thread-group, and CPU-selection examples are available in test_profiler_native_cpu_container.sh, test_profiler_native_cpu_thread_group.sh, and test_profiler_native_cpu_cpuid.sh, respectively. Run make all first and configure the Java or Python profiler path in integration/env.sh as needed.
βοΈ How It Works
profiler first selects a collector based on the language and profile type. The native on-CPU collector attaches eBPF programs to perf events; off-CPU mode attaches scheduler switch, wakeup, exit, and task-free tracepoints. Native memory collectors record allocation and release paths through kernel events. The Java and Python collectors start async-profiler and py-spy subprocesses, respectively. Collected records enter a common aggregation pipeline, which merges counts by call stack and then writes a local file or uploads the result to remote storage.
flowchart LR
CLI[profiler CLI options] --> Select{Language and profile type}
Select -->|C/C++/Go| Native[Native eBPF collector]
Select -->|Java| Java[async-profiler]
Select -->|Python| Python[py-spy]
Native --> Queue[Sample record queue]
Java --> Queue
Python --> Queue
Queue --> Aggregate[Aggregate by call stack]
Aggregate --> Folded[Collapsed stacks]
Aggregate --> SVG[Interactive SVG flame graph]
Aggregate --> Remote[Remote upload through Unix socket]
--duration controls the collection lifetime, while --aggr-interval controls the snapshot interval for remote uploads. Local collapsed, flamegraph, and svg modes write the final aggregate when collection ends. remote creates and uploads snapshots at the aggregation interval. The queue decouples collection from symbolization, aggregation, and output so file rendering does not block the sampling path.