Pyroscope¶
Pyroscope is PerfShop's continuous profiling service. It receives, in push mode, the CPU, memory allocation, and lock contention profiles produced by the Pyroscope Java agent embedded in the backend, stores them locally, and feeds the Flamegraph panels of the APM dashboards.
Source of truth
This page is taken from pyroscope/pyroscope-config.yml, the backend JAVA_OPTS in the compose files, and the Pyroscope datasource in grafana/provisioning/datasources/pyroscope.yml.
Why Pyroscope?¶
Continuous profiling is the lesser-known pillar of the "four pillars of observability" (metrics, logs, traces, profiles). Where metrics say "how much", traces say "where in the code", profiles say "which precise line consumes".
Concretely, in PerfShop, Pyroscope answers questions such as:
- "When CPU chaos is active, which Java method is consuming the CPU exactly?"
- "Which class allocates the most objects when the OutOfMemoryError chaos is in progress?"
- "On which
synchronizedare threads blocking during the Race Condition (A8) simulation?"
Answers are visual: flamegraphs in Grafana, where the width of each rectangle represents the CPU time consumed by the method (or the memory allocated, or the time blocked on a lock).
Architecture¶
flowchart LR
subgraph BE["perfshop-app (JVM)"]
direction TB
APP["Spring Boot 3.2"]
PA["Pyroscope agent<br/>pyroscope.jar"]
APP -.injected.-> PA
end
PYRO["perfshop-pyroscope<br/>(grafana/pyroscope:latest)<br/>filesystem backend"]
GRAF["Grafana<br/>(Pyroscope datasource)"]
PA -->|"JFR HTTP push :4040<br/>(CPU, alloc, lock)"| PYRO
PYRO -->|"Pyroscope datasource<br/>(query flamegraphs)"| GRAF
Pyroscope works in push only mode: the agent periodically pushes the profiles, Pyroscope does not scrape. This is different from Prometheus, and it is consistent with the JFR model (one file per time window).
Pyroscope configuration¶
server:
http_listen_port: 4040
log_level: warn
storage:
backend: filesystem
filesystem:
dir: /var/pyroscope/data
scrape_configs: []
| Parameter | Value | Effect |
|---|---|---|
http_listen_port |
4040 | Single HTTP port — used both for push (POST /ingest) and for the query API (Grafana) |
log_level |
warn | Log level for Pyroscope itself |
storage.backend |
filesystem |
Local storage on the named volume pyroscope-data |
storage.filesystem.dir |
/var/pyroscope/data |
Internal path, mounted on the named volume |
scrape_configs: [] |
(empty) | No target scraped — push-only mode |
The default host port is 4040 (variable PYROSCOPE_HTTP_PORT).
No retention configured
PerfShop does not have an explicit retention policy on the Pyroscope side — this is intentional for the pedagogical context (profiles remain available for deferred analysis throughout the duration of a lab). In a real production setup, compactor.retention_period would be set to bound disk consumption.
Pyroscope agent on the backend side¶
The Pyroscope Java agent is embedded in the perfshop-backend image (path /agents/pyroscope.jar) and activated via JAVA_OPTS:
-javaagent:/agents/pyroscope.jar
-Dpyroscope.server.address=http://perfshop-pyroscope:4040
-Dpyroscope.application.name=perfshop
-Dpyroscope.format=jfr
-Dpyroscope.profiler.event=cpu
-Dpyroscope.profiler.alloc=512k
-Dpyroscope.profiler.lock=10ms
-Dpyroscope.profilingInterval=PT0.02S
Property breakdown¶
| Property | Effect |
|---|---|
pyroscope.server.address |
Pyroscope endpoint via internal Docker DNS |
pyroscope.application.name=perfshop |
Identifies the application in Pyroscope — appears as service: perfshop in Grafana |
pyroscope.format=jfr |
Java Flight Recorder format — the agent uses the JVM's JFR APIs to collect without significant overhead |
pyroscope.profiler.event=cpu |
CPU profile enabled — this is the main profile |
pyroscope.profiler.alloc=512k |
Heap allocation profile — a sample is taken every 512 KB allocated (approximately) |
pyroscope.profiler.lock=10ms |
Lock contention profile — any wait on a lock ≥ 10 ms is sampled |
pyroscope.profilingInterval=PT0.02S |
CPU sampling every 20 ms (ISO 8601 Duration notation) |
Three profiles in parallel¶
The Pyroscope agent therefore produces three profile streams simultaneously, without interfering with each other:
flowchart TB
AGENT["pyroscope.jar"]
AGENT --> CPU["CPU profile<br/>20ms sampling<br/>perf_event or itimer"]
AGENT --> ALLOC["Allocation profile<br/>1 sample / 512 KB allocated"]
AGENT --> LOCK["Lock contention profile<br/>blocks ≥ 10 ms"]
CPU --> P1[("Pyroscope<br/>cpu profile")]
ALLOC --> P2[("Pyroscope<br/>alloc profile")]
LOCK --> P3[("Pyroscope<br/>lock profile")]
CPU profile — perf_event vs itimer¶
Pyroscope automatically chooses the sampling method depending on the environment:
| Environment | Method | Notes |
|---|---|---|
| Native Linux (VPS, physical server) | perf_event |
Samples the hardware CPU, more accurate, but requires specific Linux capabilities |
| Docker Desktop (Windows / macOS / virtualized Linux) | itimer |
Samples via POSIX software timers, works in all containerized environments |
This is why the Student and Instructor APM dashboards expose two CPU Flamegraph panels side by side:
CPU Flamegraph — Linux (perf_event)— empty under Docker DesktopCPU Flamegraph — Docker Desktop / Windows / macOS (itimer)— empty on native Linux
One of the two is always empty depending on the environment, but the student can understand the difference and read the one that contains data.
Heap allocation profile¶
The alloc profile captures the origin of memory allocations — which methods allocate the most objects, at what frequency. This is what the "Heap Flamegraph — memory allocations (F3-OOM)" panel of the Instructor APM dashboard shows.
When the memory backend chaos is active and the heap fills up, the student can:
- Open the Heap Flamegraph panel,
- Identify the method at the top of the graph (the widest one),
- Correlate with the source code to understand which class is responsible for the growth.
Lock contention profile¶
The lock profile captures threads that wait on a synchronized or a ReentrantLock. The "Lock Contention Flamegraph — JVM locks (A8 Race Condition)" panel highlights contention hot-spots.
It is the main tool for the A8 — Race Condition pedagogical chaos scenario: a thread holds a lock for too long, the others pile up waiting, the lock profile shows exactly where.
Grafana datasource¶
- name: Pyroscope
type: grafana-pyroscope-datasource
uid: pyroscope
access: proxy
url: http://perfshop-pyroscope:4040
isDefault: false
editable: true
Minimal configuration — the native Grafana plugin grafana-pyroscope-datasource handles everything: profile selection (cpu, alloc, lock), label filtering, comparison between two time windows.
Reading a flamegraph — quick guide¶
flowchart TB
R["Root<br/>= 100% of CPU time<br/>over the observed window"]
R --> M1["Tomcat thread loop<br/>~80%"]
R --> M2["GC threads<br/>~12%"]
R --> M3["Others<br/>~8%"]
M1 --> M11["doFilter (LicenseInterceptor)<br/>~5%"]
M1 --> M12["doFilter (ChaosInterceptor)<br/>~30%"]
M1 --> M13["ProductController.list()<br/>~45%"]
M12 --> M121["CpuChaosScheduler.busyLoop()<br/>~28%"]
| Concept | Reading |
|---|---|
| Width of a rectangle | Proportion of CPU time consumed by the method and all its descendants |
| Height (vertical axis) | Depth of the call stack |
| Stacking | A method called by the method above |
| Color | Decorative (random palette to visually distinguish) |
| Hot path | The widest path from root to a leaf — that is what to focus on |
The classic beginner mistake is to look at the height: it is not an indicator. Only the width matters.
Volumes¶
| Volume | Mount | Content |
|---|---|---|
pyroscope-data (named volume) |
/var/pyroscope/data |
Profiles stored on filesystem, organized by time series |
./pyroscope/pyroscope-config.yml (bind mount) |
/etc/pyroscope/pyroscope-config.yml |
Pyroscope configuration (read-only) |
Ports¶
| Service | Host port | Container port | Env variable |
|---|---|---|---|
perfshop-pyroscope |
4040 | 4040 | PYROSCOPE_HTTP_PORT |
To go further¶
- Overview — four observability signals
- Grafana — Pyroscope datasource
- Shipped dashboards — Flamegraph panels of the Student and Instructor APM dashboards
- Tempo — the other agent embedded in the backend image
- Chaos engineering section — scenarios that exploit continuous profiling (A8 Race Condition, F3 OutOfMemoryError, CPU chaos)