Introduction to Pyroscope
Pyroscope is an open-source continuous profiling platform that helps identify performance bottlenecks in applications. It provides low-overhead profiling with real-time visualization of CPU, memory, and other runtime metrics.
Pyroscope Java Agent Integration
Maven Configuration
<properties>
<pyroscope.version>0.13.0</pyroscope.version>
</properties>
<dependencies>
<!-- Pyroscope Java Client -->
<dependency>
<groupId>io.pyroscope</groupId>
<artifactId>pyroscope-java</artifactId>
<version>${pyroscope.version}</version>
</dependency>
<!-- For async profiling -->
<dependency>
<groupId>io.pyroscope</groupId>
<artifactId>pyroscope-java-async-profiler</artifactId>
<version>${pyroscope.version}</version>
</dependency>
<!-- Logging integration -->
<dependency>
<groupId>io.pyroscope</groupId>
<artifactId>pyroscope-log4j</artifactId>
<version>${pyroscope.version}</version>
</dependency>
</dependencies>
Basic Pyroscope Configuration
public class PyroscopeConfig {
public static void initialize() {
Pyroscope.Options options = new Pyroscope.Options()
.setApplicationName("my-java-app")
.setServerAddress("http://localhost:4040")
.setProfilingEvent(Event.ITIMER)
.setProfilingAlloc("512k")
.setProfilingLock("10ms")
.setProfilingInterval(Duration.ofSeconds(10))
.setUploadInterval(Duration.ofSeconds(10));
Pyroscope.start(options);
}
// Environment-based configuration
public static void initializeFromEnv() {
Pyroscope.Options options = Pyroscope.Options.fromEnv()
.setApplicationName(System.getenv().getOrDefault("PYROSCOPE_APPLICATION_NAME", "my-java-app"))
.setServerAddress(System.getenv().getOrDefault("PYROSCOPE_SERVER_ADDRESS", "http://localhost:4040"));
Pyroscope.start(options);
}
}
Spring Boot Integration
Spring Boot Auto-Configuration
@Configuration
@EnableConfigurationProperties(PyroscopeProperties.class)
public class PyroscopeAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(PyroscopeAutoConfiguration.class);
@Bean
@ConditionalOnProperty(name = "pyroscope.enabled", havingValue = "true")
public PyroscopeInitializer pyroscopeInitializer(PyroscopeProperties properties) {
return new PyroscopeInitializer(properties);
}
}
@Component
public class PyroscopeInitializer {
public PyroscopeInitializer(PyroscopeProperties properties) {
initializePyroscope(properties);
}
private void initializePyroscope(PyroscopeProperties properties) {
try {
Pyroscope.Options options = new Pyroscope.Options()
.setApplicationName(properties.getApplicationName())
.setServerAddress(properties.getServerAddress())
.setProfilingEvent(Event.valueOf(properties.getProfilingEvent()))
.setProfilingInterval(Duration.ofSeconds(properties.getProfilingInterval()))
.setUploadInterval(Duration.ofSeconds(properties.getUploadInterval()))
.setAuthToken(properties.getAuthToken());
// Add labels for better filtering
options.setLabels(Map.of(
"environment", properties.getEnvironment(),
"region", properties.getRegion(),
"version", properties.getAppVersion()
));
Pyroscope.start(options);
logger.info("Pyroscope profiling initialized for application: {}",
properties.getApplicationName());
} catch (Exception e) {
logger.error("Failed to initialize Pyroscope", e);
}
}
}
@ConfigurationProperties(prefix = "pyroscope")
public class PyroscopeProperties {
private boolean enabled = true;
private String applicationName = "spring-boot-app";
private String serverAddress = "http://localhost:4040";
private String profilingEvent = "ITIMER";
private int profilingInterval = 10;
private int uploadInterval = 10;
private String authToken;
private String environment = "development";
private String region = "local";
private String appVersion = "1.0.0";
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getApplicationName() { return applicationName; }
public void setApplicationName(String applicationName) { this.applicationName = applicationName; }
public String getServerAddress() { return serverAddress; }
public void setServerAddress(String serverAddress) { this.serverAddress = serverAddress; }
public String getProfilingEvent() { return profilingEvent; }
public void setProfilingEvent(String profilingEvent) { this.profilingEvent = profilingEvent; }
public int getProfilingInterval() { return profilingInterval; }
public void setProfilingInterval(int profilingInterval) { this.profilingInterval = profilingInterval; }
public int getUploadInterval() { return uploadInterval; }
public void setUploadInterval(int uploadInterval) { this.uploadInterval = uploadInterval; }
public String getAuthToken() { return authToken; }
public void setAuthToken(String authToken) { this.authToken = authToken; }
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public String getRegion() { return region; }
public void setRegion(String region) { this.region = region; }
public String getAppVersion() { return appVersion; }
public void setAppVersion(String appVersion) { this.appVersion = appVersion; }
}
Application Configuration
# application.yml pyroscope: enabled: true application-name: "order-service" server-address: "http://pyroscope:4040" profiling-event: "ITIMER" profiling-interval: 10 upload-interval: 10 environment: "production" region: "us-east-1" app-version: "2.1.0"
Custom Profiling Scenarios
Method-Level Profiling
@Service
public class OrderProcessingService {
private static final Logger logger = LoggerFactory.getLogger(OrderProcessingService.class);
@PyroscopeProfiled("order_processing")
public Order processOrder(OrderRequest request) {
// This method will be automatically profiled if using AOP
return executeOrderProcessing(request);
}
public Order processOrderWithManualProfiling(OrderRequest request) {
// Manual profiling with custom labels
Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("order_type", request.getType(), "customer_tier", request.getCustomerTier()),
() -> {
return executeOrderProcessing(request);
}
);
}
private Order executeOrderProcessing(OrderRequest request) {
validateOrder(request);
checkInventory(request);
processPayment(request);
fulfillOrder(request);
return createOrderConfirmation(request);
}
@PyroscopeProfiled("order_validation")
private void validateOrder(OrderRequest request) {
// Validation logic
try {
Thread.sleep(50); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@PyroscopeProfiled("inventory_check")
private void checkInventory(OrderRequest request) {
// Inventory check logic
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Custom annotation for AOP profiling
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PyroscopeProfiled {
String value() default "";
String[] labels() default {};
}
@Aspect
@Component
public class PyroscopeProfilingAspect {
@Around("@annotation(pyroscopeProfiled)")
public Object profileMethod(ProceedingJoinPoint joinPoint, PyroscopeProfiled pyroscopeProfiled)
throws Throwable {
String profileName = pyroscopeProfiled.value();
if (profileName.isEmpty()) {
profileName = joinPoint.getSignature().toShortString();
}
// Execute with Pyroscope profiling
return Pyroscope.LabelsWrapper.run(
buildLabels(joinPoint, pyroscopeProfiled),
() -> {
try {
return joinPoint.proceed();
} catch (RuntimeException e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
);
}
private Pyroscope.LabelsSet buildLabels(ProceedingJoinPoint joinPoint, PyroscopeProfiled annotation) {
Map<String, String> labels = new HashMap<>();
// Add custom labels from annotation
for (String label : annotation.labels()) {
String[] parts = label.split("=");
if (parts.length == 2) {
labels.put(parts[0], parts[1]);
}
}
// Add method-specific labels
labels.put("class", joinPoint.getTarget().getClass().getSimpleName());
labels.put("method", joinPoint.getSignature().getName());
return Pyroscope.LabelsSet.of(labels);
}
}
Database Query Profiling
@Repository
public class OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@PyroscopeProfiled("database_queries")
public List<Order> findOrdersByCustomer(String customerId, Date startDate, Date endDate) {
String query = """
SELECT o FROM Order o
WHERE o.customerId = :customerId
AND o.createdDate BETWEEN :startDate AND :endDate
ORDER BY o.createdDate DESC
""";
return entityManager.createQuery(query, Order.class)
.setParameter("customerId", customerId)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
}
public List<Order> findOrdersWithProfiling(String customerId, Date startDate, Date endDate) {
return Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("query_type", "customer_orders", "operation", "read"),
() -> findOrdersByCustomer(customerId, startDate, endDate)
);
}
}
// JDBC Connection wrapper for profiling
@Component
public class ProfilingDataSource extends AbstractDataSource {
private final DataSource delegate;
public ProfilingDataSource(DataSource delegate) {
this.delegate = delegate;
}
@Override
public Connection getConnection() throws SQLException {
return new ProfilingConnection(delegate.getConnection());
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return new ProfilingConnection(delegate.getConnection(username, password));
}
private static class ProfilingConnection implements Connection {
private final Connection delegate;
public ProfilingConnection(Connection delegate) {
this.delegate = delegate;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return new ProfilingPreparedStatement(delegate.prepareStatement(sql), sql);
}
// Delegate all other methods to the underlying connection
@Override
public void close() throws SQLException { delegate.close(); }
@Override
public boolean isClosed() throws SQLException { return delegate.isClosed(); }
// ... implement all other Connection methods
}
private static class ProfilingPreparedStatement implements PreparedStatement {
private final PreparedStatement delegate;
private final String sql;
public ProfilingPreparedStatement(PreparedStatement delegate, String sql) {
this.delegate = delegate;
this.sql = sql;
}
@Override
public ResultSet executeQuery() throws SQLException {
return Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("sql_operation", "query", "sql_type", getSqlType(sql)),
delegate::executeQuery
);
}
@Override
public int executeUpdate() throws SQLException {
return Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("sql_operation", "update", "sql_type", getSqlType(sql)),
delegate::executeUpdate
);
}
@Override
public boolean execute() throws SQLException {
return Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("sql_operation", "execute", "sql_type", getSqlType(sql)),
delegate::execute
);
}
private String getSqlType(String sql) {
String lowerSql = sql.toLowerCase().trim();
if (lowerSql.startsWith("select")) return "select";
if (lowerSql.startsWith("insert")) return "insert";
if (lowerSql.startsWith("update")) return "update";
if (lowerSql.startsWith("delete")) return "delete";
return "other";
}
// Delegate all other methods
@Override
public void close() throws SQLException { delegate.close(); }
// ... implement all other PreparedStatement methods
}
}
HTTP Request Profiling
@Component
public class ProfilingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(ProfilingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String path = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
// Profile the request
Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of(
"http_method", method,
"http_path", sanitizePath(path),
"http_status", "in_progress"
),
() -> {
try {
chain.doFilter(request, response);
// Update status code after execution
Pyroscope.setLabel("http_status", String.valueOf(httpResponse.getStatus()));
} catch (Exception e) {
Pyroscope.setLabel("http_status", "error");
throw new RuntimeException(e);
}
return null;
}
);
}
private String sanitizePath(String path) {
// Convert path parameters to placeholders
return path.replaceAll("/\\d+", "/{id}")
.replaceAll("/[a-f0-9-]{36}", "/{uuid}");
}
}
// REST Controller with profiling
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderProcessingService orderService;
@PostMapping
@PyroscopeProfiled(value = "create_order", labels = {"http_method=POST", "endpoint=/api/orders"})
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
Order order = orderService.processOrder(request);
return ResponseEntity.ok(order);
}
@GetMapping("/{orderId}")
public ResponseEntity<Order> getOrder(@PathVariable String orderId) {
return Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("operation", "get_order", "order_id", orderId),
() -> {
Order order = orderService.findOrderById(orderId);
return ResponseEntity.ok(order);
}
);
}
}
Advanced Profiling Scenarios
Memory Allocation Profiling
@Service
public class MemoryIntensiveService {
public void processLargeDataset(List<DataRecord> records) {
// Enable memory profiling for this operation
Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("profiling_type", "memory", "dataset_size", String.valueOf(records.size())),
() -> {
processRecordsWithMemoryProfiling(records);
return null;
}
);
}
private void processRecordsWithMemoryProfiling(List<DataRecord> records) {
List<ProcessedRecord> results = new ArrayList<>();
for (DataRecord record : records) {
// This allocation will be visible in memory profiling
ProcessedRecord processed = transformRecord(record);
results.add(processed);
// Simulate memory-intensive operation
if (results.size() % 1000 == 0) {
System.gc(); // Hint for GC, don't do this in production!
}
}
aggregateResults(results);
}
private ProcessedRecord transformRecord(DataRecord record) {
// Memory-intensive transformation
byte[] largeBuffer = new byte[1024 * 1024]; // 1MB allocation
// ... processing logic
return new ProcessedRecord(record, largeBuffer);
}
}
Async Profiling with CompletableFuture
@Service
public class AsyncOrderProcessor {
@Async
public CompletableFuture<Order> processOrderAsync(OrderRequest request) {
return Pyroscope.LabelsWrapper.runAsync(
Pyroscope.LabelsSet.of("processing_type", "async", "order_type", request.getType()),
() -> CompletableFuture.supplyAsync(() -> {
// This will be profiled in the async context
return processOrder(request);
})
);
}
public CompletableFuture<List<Order>> processBatchAsync(List<OrderRequest> requests) {
List<CompletableFuture<Order>> futures = requests.stream()
.map(this::processOrderAsync)
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
}
Custom Profiling Events
@Component
public class CustomProfilingService {
private final PyroscopeProfiler profiler;
public CustomProfilingService() {
this.profiler = PyroscopeProfiler.getInstance();
}
public void trackBusinessEvent(String eventName, Map<String, String> labels) {
Pyroscope.LabelsSet labelSet = Pyroscope.LabelsSet.of(labels);
// Record custom business event
profiler.recordEvent(eventName, labelSet);
}
public void profileCriticalSection(String sectionName, Runnable criticalCode) {
long startTime = System.nanoTime();
try {
criticalCode.run();
} finally {
long duration = System.nanoTime() - startTime;
// Record custom timing metric
Pyroscope.LabelsWrapper.run(
Pyroscope.LabelsSet.of("critical_section", sectionName),
() -> {
// This will create a profile sample for the critical section
try {
Thread.sleep(0); // Force sample point
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
);
logger.info("Critical section {} took {} ns", sectionName, duration);
}
}
}
Monitoring and Health Checks
@Component
public class PyroscopeHealthIndicator implements HealthIndicator {
private static final Logger logger = LoggerFactory.getLogger(PyroscopeHealthIndicator.class);
@Override
public Health health() {
try {
// Check if Pyroscope is active and connected
boolean isHealthy = checkPyroscopeConnection();
if (isHealthy) {
return Health.up()
.withDetail("service", "pyroscope")
.withDetail("status", "connected")
.build();
} else {
return Health.down()
.withDetail("service", "pyroscope")
.withDetail("status", "disconnected")
.build();
}
} catch (Exception e) {
logger.error("Pyroscope health check failed", e);
return Health.down(e).build();
}
}
private boolean checkPyroscopeConnection() {
// Implement connection check to Pyroscope server
// This could be an HTTP health check or agent status check
return true; // Simplified implementation
}
}
@RestController
@RequestMapping("/admin/profiling")
public class ProfilingManagementController {
@PostMapping("/start")
public ResponseEntity<String> startProfiling() {
// Implement dynamic profiling control
Pyroscope.start();
return ResponseEntity.ok("Profiling started");
}
@PostMapping("/stop")
public ResponseEntity<String> stopProfiling() {
Pyroscope.stop();
return ResponseEntity.ok("Profiling stopped");
}
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getProfilingStatus() {
Map<String, Object> status = new HashMap<>();
status.put("active", Pyroscope.isActive());
status.put("applicationName", Pyroscope.getApplicationName());
status.put("serverAddress", Pyroscope.getServerAddress());
return ResponseEntity.ok(status);
}
}
Docker Configuration
FROM openjdk:17-jdk-slim # Install Pyroscope agent RUN wget -O /tmp/pyroscope.jar https://github.com/pyroscope-io/pyroscope-java/releases/download/v0.13.0/pyroscope.jar COPY target/my-app.jar /app/my-app.jar # Run with Pyroscope agent CMD ["java", \ "-javaagent:/tmp/pyroscope.jar", \ "-Dpyroscope.application.name=my-java-app", \ "-Dpyroscope.server.address=http://pyroscope:4040", \ "-Dpyroscope.profiling.event=itimer", \ "-jar", "/app/my-app.jar"]
Conclusion
Pyroscope provides comprehensive profiling capabilities for Java applications:
- Low-Overhead Profiling - Continuous profiling with minimal performance impact
- Integration Flexibility - Multiple integration approaches (agent, manual, AOP)
- Rich Context - Custom labels for filtering and analysis
- Real-time Visualization - Immediate insights into performance bottlenecks
- Production Ready - Safe for use in production environments
The Java integration allows developers to identify performance issues, optimize resource usage, and understand application behavior under real workloads.