Perfetto is Google's open-source system profiling, app tracing, and trace analysis platform. This guide covers comprehensive integration of Perfetto tracing into Java applications for performance monitoring and analysis.
Understanding Perfetto and Java Integration
Step 1: Core Concepts and Setup
package com.example.perfetto;
import java.io.*;
import java.nio.file.*;
import java.util.concurrent.*;
public class PerfettoIntegration {
// Perfetto trace configuration
public static class PerfettoConfig {
private final String tracePath;
private final long maxFileSize;
private final int bufferSize;
private final boolean systemTracing;
public PerfettoConfig(String tracePath, long maxFileSize,
int bufferSize, boolean systemTracing) {
this.tracePath = tracePath;
this.maxFileSize = maxFileSize;
this.bufferSize = bufferSize;
this.systemTracing = systemTracing;
}
// Getters
public String getTracePath() { return tracePath; }
public long getMaxFileSize() { return maxFileSize; }
public int getBufferSize() { return bufferSize; }
public boolean isSystemTracing() { return systemTracing; }
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String tracePath = "/data/misc/perfetto-traces/trace.pftrace";
private long maxFileSize = 100 * 1024 * 1024; // 100MB
private int bufferSize = 8192; // 8KB
private boolean systemTracing = false;
public Builder tracePath(String path) {
this.tracePath = path;
return this;
}
public Builder maxFileSize(long size) {
this.maxFileSize = size;
return this;
}
public Builder bufferSize(int size) {
this.bufferSize = size;
return this;
}
public Builder systemTracing(boolean enabled) {
this.systemTracing = enabled;
return this;
}
public PerfettoConfig build() {
return new PerfettoConfig(tracePath, maxFileSize, bufferSize, systemTracing);
}
}
}
// Native interface for Perfetto
public static class NativePerfetto {
static {
System.loadLibrary("perfetto_jni");
}
// Native methods
public static native void initialize(String config);
public static native void startTracing();
public static native void stopTracing();
public static native void flushTracing();
public static native void traceEventBegin(String category, String name);
public static native void traceEventEnd(String category, String name);
public static native void traceCounter(String category, String name, long value);
public static native void traceInstant(String category, String name);
}
}
Java-based Perfetto Client
Step 2: Pure Java Perfetto Implementation
package com.example.perfetto.java;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class JavaPerfettoClient {
private final String perfettoSocketPath = "/dev/socket/traced_producer";
private final AtomicBoolean tracingActive = new AtomicBoolean(false);
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
private final BlockingQueue<TraceEvent> eventQueue = new LinkedBlockingQueue<>(10000);
private Socket perfettoSocket;
private OutputStream outputStream;
private InputStream inputStream;
public static class TraceEvent {
public enum Type { BEGIN, END, COUNTER, INSTANT }
private final Type type;
private final String category;
private final String name;
private final long timestamp;
private final long value;
private final Map<String, String> args;
public TraceEvent(Type type, String category, String name,
long timestamp, long value, Map<String, String> args) {
this.type = type;
this.category = category;
this.name = name;
this.timestamp = timestamp;
this.value = value;
this.args = args != null ? new HashMap<>(args) : new HashMap<>();
}
// Getters
public Type getType() { return type; }
public String getCategory() { return category; }
public String getName() { return name; }
public long getTimestamp() { return timestamp; }
public long getValue() { return value; }
public Map<String, String> getArgs() { return args; }
}
public boolean connect() {
try {
// Try to connect to Perfetto socket
perfettoSocket = new Socket();
perfettoSocket.connect(new InetSocketAddress("localhost", 9000)); // Example
outputStream = perfettoSocket.getOutputStream();
inputStream = perfettoSocket.getInputStream();
return true;
} catch (IOException e) {
System.err.println("Failed to connect to Perfetto: " + e.getMessage());
return false;
}
}
public void startTracing(PerfettoConfig config) {
if (!tracingActive.compareAndSet(false, true)) {
System.out.println("Tracing already active");
return;
}
// Start event processing thread
scheduler.submit(this::processEvents);
// Start periodic flush
scheduler.scheduleAtFixedRate(this::flushEvents, 1, 1, TimeUnit.SECONDS);
System.out.println("Perfetto tracing started");
}
public void stopTracing() {
if (!tracingActive.compareAndSet(true, false)) {
System.out.println("Tracing not active");
return;
}
// Flush remaining events
flushEvents();
// Close connection
try {
if (perfettoSocket != null) {
perfettoSocket.close();
}
} catch (IOException e) {
System.err.println("Error closing Perfetto connection: " + e.getMessage());
}
System.out.println("Perfetto tracing stopped");
}
public void traceEventBegin(String category, String name, Map<String, String> args) {
if (!tracingActive.get()) return;
TraceEvent event = new TraceEvent(
TraceEvent.Type.BEGIN, category, name,
System.nanoTime(), 0, args
);
if (!eventQueue.offer(event)) {
System.err.println("Trace event queue full, dropping event");
}
}
public void traceEventEnd(String category, String name) {
if (!tracingActive.get()) return;
TraceEvent event = new TraceEvent(
TraceEvent.Type.END, category, name,
System.nanoTime(), 0, null
);
if (!eventQueue.offer(event)) {
System.err.println("Trace event queue full, dropping event");
}
}
public void traceCounter(String category, String name, long value) {
if (!tracingActive.get()) return;
TraceEvent event = new TraceEvent(
TraceEvent.Type.COUNTER, category, name,
System.nanoTime(), value, null
);
if (!eventQueue.offer(event)) {
System.err.println("Trace event queue full, dropping event");
}
}
private void processEvents() {
while (tracingActive.get() || !eventQueue.isEmpty()) {
try {
TraceEvent event = eventQueue.poll(100, TimeUnit.MILLISECONDS);
if (event != null) {
sendEventToPerfetto(event);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (IOException e) {
System.err.println("Failed to send event to Perfetto: " + e.getMessage());
}
}
}
private void sendEventToPerfetto(TraceEvent event) throws IOException {
if (outputStream == null) return;
// Convert event to Perfetto protobuf format (simplified)
String eventData = formatEventAsProtobuf(event);
byte[] data = eventData.getBytes(StandardCharsets.UTF_8);
outputStream.write(data);
outputStream.flush();
}
private String formatEventAsProtobuf(TraceEvent event) {
// Simplified protobuf-like format
StringBuilder pb = new StringBuilder();
pb.append("category: \"").append(event.getCategory()).append("\"\n");
pb.append("name: \"").append(event.getName()).append("\"\n");
pb.append("timestamp: ").append(event.getTimestamp()).append("\n");
switch (event.getType()) {
case BEGIN:
pb.append("type: BEGIN_EVENT\n");
break;
case END:
pb.append("type: END_EVENT\n");
break;
case COUNTER:
pb.append("type: COUNTER_EVENT\n");
pb.append("value: ").append(event.getValue()).append("\n");
break;
case INSTANT:
pb.append("type: INSTANT_EVENT\n");
break;
}
if (!event.getArgs().isEmpty()) {
pb.append("args: {\n");
for (Map.Entry<String, String> arg : event.getArgs().entrySet()) {
pb.append(" \"").append(arg.getKey()).append("\": \"")
.append(arg.getValue()).append("\"\n");
}
pb.append("}\n");
}
return pb.toString();
}
private void flushEvents() {
// Force flush any buffered events
try {
if (outputStream != null) {
outputStream.flush();
}
} catch (IOException e) {
System.err.println("Failed to flush events: " + e.getMessage());
}
}
}
Annotation-Based Tracing
Step 3: Declarative Tracing with Annotations
package com.example.perfetto.annotations;
import java.lang.annotation.*;
import java.util.concurrent.ConcurrentHashMap;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TraceMethod {
String category() default "java";
String name() default "";
boolean trackArgs() default false;
boolean trackReturn() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TraceClass {
String category() default "java";
boolean allMethods() default false;
}
// Method interceptor for tracing
public class TracingInterceptor {
private static final ConcurrentHashMap<String, JavaPerfettoClient> clients =
new ConcurrentHashMap<>();
public static Object traceMethodExecution(Method method, Object[] args, Object target) {
String category = "java";
String methodName = method.getName();
// Check for method-level annotation
TraceMethod methodAnnotation = method.getAnnotation(TraceMethod.class);
if (methodAnnotation != null) {
category = methodAnnotation.category();
if (!methodAnnotation.name().isEmpty()) {
methodName = methodAnnotation.name();
}
}
// Check for class-level annotation
TraceClass classAnnotation = method.getDeclaringClass().getAnnotation(TraceClass.class);
if (classAnnotation != null && methodAnnotation == null) {
category = classAnnotation.category();
}
JavaPerfettoClient client = getOrCreateClient(category);
// Prepare arguments for tracing
Map<String, String> traceArgs = new HashMap<>();
if (methodAnnotation != null && methodAnnotation.trackArgs()) {
for (int i = 0; i < args.length; i++) {
traceArgs.put("arg" + i, String.valueOf(args[i]));
}
}
// Start tracing
client.traceEventBegin(category, methodName, traceArgs);
long startTime = System.nanoTime();
try {
Object result = method.invoke(target, args);
if (methodAnnotation != null && methodAnnotation.trackReturn()) {
traceArgs.put("return", String.valueOf(result));
}
return result;
} catch (Exception e) {
traceArgs.put("exception", e.getClass().getSimpleName());
throw new RuntimeException("Method invocation failed", e);
} finally {
// Calculate duration
long duration = System.nanoTime() - startTime;
traceArgs.put("duration_ns", String.valueOf(duration));
// End tracing
client.traceEventEnd(category, methodName);
// Trace duration as counter
client.traceCounter(category, methodName + ".duration", duration);
}
}
private static JavaPerfettoClient getOrCreateClient(String category) {
return clients.computeIfAbsent(category, k -> {
JavaPerfettoClient client = new JavaPerfettoClient();
client.connect();
return client;
});
}
}
// AspectJ-style tracing aspect
public class TracingAspect {
private final JavaPerfettoClient perfettoClient;
public TracingAspect() {
this.perfettoClient = new JavaPerfettoClient();
this.perfettoClient.connect();
}
public Object traceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
String category = "aspect." + className;
Map<String, String> args = new HashMap<>();
Object[] methodArgs = joinPoint.getArgs();
for (int i = 0; i < methodArgs.length; i++) {
args.put("arg" + i, String.valueOf(methodArgs[i]));
}
perfettoClient.traceEventBegin(category, methodName, args);
long startTime = System.nanoTime();
try {
Object result = joinPoint.proceed();
args.put("return", String.valueOf(result));
return result;
} catch (Exception e) {
args.put("exception", e.getClass().getSimpleName());
throw e;
} finally {
long duration = System.nanoTime() - startTime;
perfettoClient.traceEventEnd(category, methodName);
perfettoClient.traceCounter(category, methodName + ".duration", duration);
}
}
}
Application Integration Examples
Step 4: Spring Boot Integration
package com.example.perfetto.spring;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.scheduling.annotation.Async;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@SpringBootApplication
@RestController
@TraceClass(category = "spring.boot")
public class SpringPerfettoApplication {
private final JavaPerfettoClient perfettoClient;
private final UserService userService;
private final OrderService orderService;
public SpringPerfettoApplication(UserService userService, OrderService orderService) {
this.userService = userService;
this.orderService = orderService;
this.perfettoClient = new JavaPerfettoClient();
}
@Bean
public CommandLineRunner initPerfetto() {
return args -> {
perfettoClient.connect();
perfettoClient.startTracing(PerfettoConfig.builder()
.tracePath("/tmp/spring-trace.pftrace")
.maxFileSize(50 * 1024 * 1024)
.build());
System.out.println("Perfetto tracing initialized for Spring Boot");
};
}
@PreDestroy
public void cleanup() {
perfettoClient.stopTracing();
}
@GetMapping("/users/{id}")
@TraceMethod(category = "http.requests", trackArgs = true, trackReturn = true)
public User getUser(@PathVariable String id) {
perfettoClient.traceEventBegin("http.requests", "getUser",
Map.of("userId", id, "thread", Thread.currentThread().getName()));
try {
User user = userService.findUserById(id);
perfettoClient.traceCounter("business.metrics", "user.queries", 1);
return user;
} finally {
perfettoClient.traceEventEnd("http.requests", "getUser");
}
}
@PostMapping("/orders")
@TraceMethod(category = "http.requests", trackArgs = true)
public Order createOrder(@RequestBody OrderRequest request) {
perfettoClient.traceEventBegin("http.requests", "createOrder",
Map.of("items", String.valueOf(request.getItems().size())));
try {
Order order = orderService.createOrder(request);
perfettoClient.traceCounter("business.metrics", "orders.created", 1);
return order;
} finally {
perfettoClient.traceEventEnd("http.requests", "createOrder");
}
}
@GetMapping("/metrics")
public Map<String, Object> getMetrics() {
perfettoClient.traceInstant("diagnostics", "metrics_endpoint_called");
Map<String, Object> metrics = new HashMap<>();
metrics.put("timestamp", System.currentTimeMillis());
metrics.put("activeThreads", Thread.activeCount());
metrics.put("memoryUsed", Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory());
// Trace memory usage
perfettoClient.traceCounter("system.metrics", "memory.used",
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
perfettoClient.traceCounter("system.metrics", "threads.active",
Thread.activeCount());
return metrics;
}
public static void main(String[] args) {
SpringApplication.run(SpringPerfettoApplication.class, args);
}
}
@Service
@TraceClass(category = "business.services")
class UserService {
private final Map<String, User> userStore = new ConcurrentHashMap<>();
@TraceMethod(name = "findUserById", trackArgs = true, trackReturn = true)
public User findUserById(String id) {
// Simulate database lookup
try {
Thread.sleep(new Random().nextInt(100)); // Simulate latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return userStore.get(id);
}
@TraceMethod(name = "createUser", trackArgs = true)
public User createUser(User user) {
userStore.put(user.getId(), user);
return user;
}
}
@Service
class OrderService {
private final JavaPerfettoClient perfettoClient;
public OrderService() {
this.perfettoClient = new JavaPerfettoClient();
this.perfettoClient.connect();
}
@Async
@TraceMethod(category = "async.operations", trackArgs = true)
public CompletableFuture<Order> createOrderAsync(OrderRequest request) {
perfettoClient.traceEventBegin("async.operations", "createOrderAsync",
Map.of("thread", Thread.currentThread().getName()));
try {
// Simulate async processing
Thread.sleep(200);
Order order = createOrder(request);
return CompletableFuture.completedFuture(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return CompletableFuture.failedFuture(e);
} finally {
perfettoClient.traceEventEnd("async.operations", "createOrderAsync");
}
}
public Order createOrder(OrderRequest request) {
// Order creation logic
return new Order(UUID.randomUUID().toString(), request.getItems());
}
}
// Data classes
class User {
private String id;
private String name;
private String email;
// Constructors, getters, setters
public User(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public String getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
class Order {
private String id;
private List<String> items;
public Order(String id, List<String> items) {
this.id = id;
this.items = items;
}
public String getId() { return id; }
public List<String> getItems() { return items; }
}
class OrderRequest {
private List<String> items;
public List<String> getItems() { return items; }
public void setItems(List<String> items) { this.items = items; }
}
Database and External Service Tracing
Step 5: Database Query Tracing
package com.example.perfetto.database;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class TracedDataSource implements javax.sql.DataSource {
private final javax.sql.DataSource delegate;
private final JavaPerfettoClient perfettoClient;
private final Map<String, QueryStats> queryStats = new ConcurrentHashMap<>();
public TracedDataSource(javax.sql.DataSource delegate, JavaPerfettoClient perfettoClient) {
this.delegate = delegate;
this.perfettoClient = perfettoClient;
}
@Override
public Connection getConnection() throws SQLException {
Connection connection = delegate.getConnection();
return new TracedConnection(connection, perfettoClient, queryStats);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
Connection connection = delegate.getConnection(username, password);
return new TracedConnection(connection, perfettoClient, queryStats);
}
// Other DataSource methods delegated to underlying datasource
@Override public PrintWriter getLogWriter() throws SQLException { return delegate.getLogWriter(); }
@Override public void setLogWriter(PrintWriter out) throws SQLException { delegate.setLogWriter(out); }
@Override public void setLoginTimeout(int seconds) throws SQLException { delegate.setLoginTimeout(seconds); }
@Override public int getLoginTimeout() throws SQLException { return delegate.getLoginTimeout(); }
@Override public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
return delegate.getParentLogger();
}
@Override public <T> T unwrap(Class<T> iface) throws SQLException { return delegate.unwrap(iface); }
@Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return delegate.isWrapperFor(iface); }
}
class TracedConnection implements Connection {
private final Connection delegate;
private final JavaPerfettoClient perfettoClient;
private final Map<String, QueryStats> queryStats;
public TracedConnection(Connection delegate, JavaPerfettoClient perfettoClient,
Map<String, QueryStats> queryStats) {
this.delegate = delegate;
this.perfettoClient = perfettoClient;
this.queryStats = queryStats;
}
@Override
public Statement createStatement() throws SQLException {
Statement statement = delegate.createStatement();
return new TracedStatement(statement, perfettoClient, queryStats);
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
PreparedStatement statement = delegate.prepareStatement(sql);
return new TracedPreparedStatement(statement, sql, perfettoClient, queryStats);
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
CallableStatement statement = delegate.prepareCall(sql);
return new TracedCallableStatement(statement, sql, perfettoClient, queryStats);
}
// Delegate all other Connection methods
@Override public void close() throws SQLException { delegate.close(); }
@Override public boolean isClosed() throws SQLException { return delegate.isClosed(); }
@Override public DatabaseMetaData getMetaData() throws SQLException { return delegate.getMetaData(); }
// ... implement all other Connection methods by delegating to delegate
}
class TracedStatement implements Statement {
private final Statement delegate;
private final JavaPerfettoClient perfettoClient;
private final Map<String, QueryStats> queryStats;
public TracedStatement(Statement delegate, JavaPerfettoClient perfettoClient,
Map<String, QueryStats> queryStats) {
this.delegate = delegate;
this.perfettoClient = perfettoClient;
this.queryStats = queryStats;
}
@Override
public ResultSet executeQuery(String sql) throws SQLException {
long startTime = System.nanoTime();
perfettoClient.traceEventBegin("database.queries", "executeQuery",
Map.of("sql", shortenSql(sql), "type", "SELECT"));
try {
ResultSet resultSet = delegate.executeQuery(sql);
return new TracedResultSet(resultSet, perfettoClient);
} finally {
long duration = System.nanoTime() - startTime;
perfettoClient.traceEventEnd("database.queries", "executeQuery");
// Update query statistics
updateQueryStats(sql, duration, false);
// Trace query duration
perfettoClient.traceCounter("database.metrics", "query.duration", duration);
perfettoClient.traceCounter("database.metrics", "queries.executed", 1);
}
}
@Override
public int executeUpdate(String sql) throws SQLException {
long startTime = System.nanoTime();
perfettoClient.traceEventBegin("database.queries", "executeUpdate",
Map.of("sql", shortenSql(sql), "type", "UPDATE"));
try {
return delegate.executeUpdate(sql);
} finally {
long duration = System.nanoTime() - startTime;
perfettoClient.traceEventEnd("database.queries", "executeUpdate");
updateQueryStats(sql, duration, false);
perfettoClient.traceCounter("database.metrics", "update.duration", duration);
}
}
@Override
public boolean execute(String sql) throws SQLException {
long startTime = System.nanoTime();
perfettoClient.traceEventBegin("database.queries", "execute",
Map.of("sql", shortenSql(sql), "type", "EXECUTE"));
try {
return delegate.execute(sql);
} finally {
long duration = System.nanoTime() - startTime;
perfettoClient.traceEventEnd("database.queries", "execute");
updateQueryStats(sql, duration, false);
}
}
private String shortenSql(String sql) {
if (sql == null) return "null";
if (sql.length() > 100) {
return sql.substring(0, 100) + "...";
}
return sql;
}
private void updateQueryStats(String sql, long duration, boolean fromCache) {
String queryKey = sql.split("\\s+")[0].toUpperCase(); // First word (SELECT, INSERT, etc.)
queryStats.compute(queryKey, (k, stats) -> {
if (stats == null) {
stats = new QueryStats();
}
stats.recordExecution(duration, fromCache);
return stats;
});
}
// Delegate all other Statement methods
@Override public void close() throws SQLException { delegate.close(); }
@Override public int getMaxFieldSize() throws SQLException { return delegate.getMaxFieldSize(); }
@Override public void setMaxFieldSize(int max) throws SQLException { delegate.setMaxFieldSize(max); }
// ... implement all other Statement methods
}
class TracedPreparedStatement extends TracedStatement implements PreparedStatement {
private final PreparedStatement delegate;
private final String sql;
public TracedPreparedStatement(PreparedStatement delegate, String sql,
JavaPerfettoClient perfettoClient,
Map<String, QueryStats> queryStats) {
super(delegate, perfettoClient, queryStats);
this.delegate = delegate;
this.sql = sql;
}
@Override
public ResultSet executeQuery() throws SQLException {
long startTime = System.nanoTime();
perfettoClient.traceEventBegin("database.queries", "preparedExecuteQuery",
Map.of("sql", shortenSql(sql), "type", "PREPARED_SELECT"));
try {
return delegate.executeQuery();
} finally {
long duration = System.nanoTime() - startTime;
perfettoClient.traceEventEnd("database.queries", "preparedExecuteQuery");
updateQueryStats(sql, duration, false);
}
}
@Override
public int executeUpdate() throws SQLException {
long startTime = System.nanoTime();
perfettoClient.traceEventBegin("database.queries", "preparedExecuteUpdate",
Map.of("sql", shortenSql(sql), "type", "PREPARED_UPDATE"));
try {
return delegate.executeUpdate();
} finally {
long duration = System.nanoTime() - startTime;
perfettoClient.traceEventEnd("database.queries", "preparedExecuteUpdate");
updateQueryStats(sql, duration, false);
}
}
// Delegate PreparedStatement methods
@Override public void setInt(int parameterIndex, int x) throws SQLException { delegate.setInt(parameterIndex, x); }
@Override public void setString(int parameterIndex, String x) throws SQLException { delegate.setString(parameterIndex, x); }
// ... implement all PreparedStatement methods
}
class TracedResultSet implements ResultSet {
private final ResultSet delegate;
private final JavaPerfettoClient perfettoClient;
private int rowCount = 0;
public TracedResultSet(ResultSet delegate, JavaPerfettoClient perfettoClient) {
this.delegate = delegate;
this.perfettoClient = perfettoClient;
}
@Override
public boolean next() throws SQLException {
boolean hasNext = delegate.next();
if (hasNext) {
rowCount++;
// Trace every 100 rows to avoid overhead
if (rowCount % 100 == 0) {
perfettoClient.traceCounter("database.metrics", "resultset.rows", rowCount);
}
}
return hasNext;
}
@Override
public void close() throws SQLException {
// Trace total rows when resultset is closed
perfettoClient.traceCounter("database.metrics", "resultset.total_rows", rowCount);
delegate.close();
}
// Delegate all ResultSet methods
@Override public String getString(int columnIndex) throws SQLException { return delegate.getString(columnIndex); }
@Override public boolean wasNull() throws SQLException { return delegate.wasNull(); }
// ... implement all ResultSet methods
}
class QueryStats {
private long executionCount = 0;
private long totalDuration = 0;
private long cacheHits = 0;
private long maxDuration = 0;
private long minDuration = Long.MAX_VALUE;
public void recordExecution(long duration, boolean fromCache) {
executionCount++;
totalDuration += duration;
maxDuration = Math.max(maxDuration, duration);
minDuration = Math.min(minDuration, duration);
if (fromCache) {
cacheHits++;
}
}
public double getAverageDuration() {
return executionCount > 0 ? (double) totalDuration / executionCount : 0;
}
// Getters
public long getExecutionCount() { return executionCount; }
public long getTotalDuration() { return totalDuration; }
public long getCacheHits() { return cacheHits; }
public long getMaxDuration() { return maxDuration; }
public long getMinDuration() { return minDuration == Long.MAX_VALUE ? 0 : minDuration; }
}
HTTP Client Tracing
Step 6: HTTP Request/Response Tracing
package com.example.perfetto.http;
import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
public class TracedHttpClient {
private final HttpClient delegate;
private final JavaPerfettoClient perfettoClient;
private final String clientName;
public TracedHttpClient(String clientName, JavaPerfettoClient perfettoClient) {
this.clientName = clientName;
this.perfettoClient = perfettoClient;
this.delegate = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}
public <T> CompletableFuture<HttpResponse<T>> sendAsync(
HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
String requestId = UUID.randomUUID().toString();
String method = request.method();
String url = request.uri().toString();
Map<String, String> traceArgs = new HashMap<>();
traceArgs.put("requestId", requestId);
traceArgs.put("method", method);
traceArgs.put("url", url);
traceArgs.put("client", clientName);
perfettoClient.traceEventBegin("http.client", "request", traceArgs);
long startTime = System.nanoTime();
return delegate.sendAsync(request, responseBodyHandler)
.whenComplete((response, throwable) -> {
long duration = System.nanoTime() - startTime;
Map<String, String> endArgs = new HashMap<>();
endArgs.put("requestId", requestId);
if (throwable != null) {
endArgs.put("error", throwable.getClass().getSimpleName());
perfettoClient.traceCounter("http.metrics", "requests.failed", 1);
} else {
endArgs.put("statusCode", String.valueOf(response.statusCode()));
endArgs.put("duration_ns", String.valueOf(duration));
perfettoClient.traceCounter("http.metrics", "requests.successful", 1);
}
perfettoClient.traceEventEnd("http.client", "request");
perfettoClient.traceCounter("http.metrics", "request.duration", duration);
perfettoClient.traceCounter("http.metrics", "requests.total", 1);
});
}
public HttpResponse<String> send(HttpRequest request) throws IOException, InterruptedException {
String requestId = UUID.randomUUID().toString();
String method = request.method();
String url = request.uri().toString();
Map<String, String> traceArgs = new HashMap<>();
traceArgs.put("requestId", requestId);
traceArgs.put("method", method);
traceArgs.put("url", url);
traceArgs.put("client", clientName);
perfettoClient.traceEventBegin("http.client", "request", traceArgs);
long startTime = System.nanoTime();
try {
HttpResponse<String> response = delegate.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, String> endArgs = new HashMap<>();
endArgs.put("requestId", requestId);
endArgs.put("statusCode", String.valueOf(response.statusCode()));
endArgs.put("duration_ns", String.valueOf(System.nanoTime() - startTime));
// Trace response size
int responseSize = response.body().length();
perfettoClient.traceCounter("http.metrics", "response.size", responseSize);
return response;
} catch (Exception e) {
Map<String, String> endArgs = new HashMap<>();
endArgs.put("requestId", requestId);
endArgs.put("error", e.getClass().getSimpleName());
throw e;
} finally {
perfettoClient.traceEventEnd("http.client", "request");
}
}
}
// Spring RestTemplate interceptor
public class TracingClientHttpRequestInterceptor implements org.springframework.http.client.ClientHttpRequestInterceptor {
private final JavaPerfettoClient perfettoClient;
public TracingClientHttpRequestInterceptor(JavaPerfettoClient perfettoClient) {
this.perfettoClient = perfettoClient;
}
@Override
public org.springframework.http.client.ClientHttpResponse intercept(
org.springframework.http.HttpRequest request, byte[] body,
org.springframework.http.client.ClientHttpRequestExecution execution) throws IOException {
String requestId = UUID.randomUUID().toString();
String method = request.getMethod().name();
String url = request.getURI().toString();
Map<String, String> traceArgs = new HashMap<>();
traceArgs.put("requestId", requestId);
traceArgs.put("method", method);
traceArgs.put("url", url);
traceArgs.put("bodySize", String.valueOf(body.length));
perfettoClient.traceEventBegin("http.client", "restTemplateRequest", traceArgs);
long startTime = System.nanoTime();
try {
org.springframework.http.client.ClientHttpResponse response = execution.execute(request, body);
Map<String, String> endArgs = new HashMap<>();
endArgs.put("requestId", requestId);
endArgs.put("statusCode", String.valueOf(response.getStatusCode().value()));
endArgs.put("duration_ns", String.valueOf(System.nanoTime() - startTime));
return response;
} catch (Exception e) {
Map<String, String> endArgs = new HashMap<>();
endArgs.put("requestId", requestId);
endArgs.put("error", e.getClass().getSimpleName());
throw e;
} finally {
perfettoClient.traceEventEnd("http.client", "restTemplateRequest");
}
}
}
Trace Analysis and Visualization
Step 7: Trace Processing and Analysis
package com.example.perfetto.analysis;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
import javax.json.*;
public class PerfettoTraceAnalyzer {
private final String traceFilePath;
public PerfettoTraceAnalyzer(String traceFilePath) {
this.traceFilePath = traceFilePath;
}
public TraceAnalysisResult analyzeTrace() throws IOException {
Path tracePath = Paths.get(traceFilePath);
if (!Files.exists(tracePath)) {
throw new FileNotFoundException("Trace file not found: " + traceFilePath);
}
byte[] traceData = Files.readAllBytes(tracePath);
return analyzeTraceData(traceData);
}
private TraceAnalysisResult analyzeTraceData(byte[] traceData) {
TraceAnalysisResult result = new TraceAnalysisResult();
// Parse trace data (simplified - real implementation would use Perfetto protobuf)
String traceContent = new String(traceData);
String[] lines = traceContent.split("\n");
for (String line : lines) {
if (line.contains("category:")) {
parseTraceEvent(line, result);
}
}
return result;
}
private void parseTraceEvent(String eventLine, TraceAnalysisResult result) {
// Simplified parsing - real implementation would use proper protobuf parsing
String category = extractValue(eventLine, "category");
String name = extractValue(eventLine, "name");
String type = extractValue(eventLine, "type");
String timestampStr = extractValue(eventLine, "timestamp");
if (category != null && name != null && timestampStr != null) {
long timestamp = Long.parseLong(timestampStr);
TraceEvent event = new TraceEvent(category, name, type, timestamp);
result.addEvent(event);
// Update statistics
result.updateCategoryStats(category);
result.updateMethodStats(name);
}
}
private String extractValue(String line, String key) {
int startIdx = line.indexOf(key + ": ");
if (startIdx == -1) return null;
startIdx += key.length() + 2; // Move past ": "
int endIdx = line.indexOf('\n', startIdx);
if (endIdx == -1) endIdx = line.length();
String value = line.substring(startIdx, endIdx).trim();
if (value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length() - 1);
}
return value;
}
public void generateReport(TraceAnalysisResult result, String outputPath) throws IOException {
StringBuilder report = new StringBuilder();
report.append("Perfetto Trace Analysis Report\n");
report.append("==============================\n\n");
report.append("Summary:\n");
report.append(String.format("Total Events: %d\n", result.getTotalEvents()));
report.append(String.format("Trace Duration: %.2f seconds\n",
result.getTraceDurationSeconds()));
report.append(String.format("Events per Second: %.2f\n",
result.getEventsPerSecond()));
report.append("\nTop Categories:\n");
result.getCategoryStats().entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
.limit(10)
.forEach(entry -> {
report.append(String.format(" %s: %d events\n", entry.getKey(), entry.getValue()));
});
report.append("\nTop Methods:\n");
result.getMethodStats().entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
.limit(10)
.forEach(entry -> {
report.append(String.format(" %s: %d calls\n", entry.getKey(), entry.getValue()));
});
// Write report to file
Files.write(Paths.get(outputPath), report.toString().getBytes());
}
public static class TraceAnalysisResult {
private final List<TraceEvent> events = new ArrayList<>();
private final Map<String, Long> categoryStats = new HashMap<>();
private final Map<String, Long> methodStats = new HashMap<>();
private long startTime = Long.MAX_VALUE;
private long endTime = Long.MIN_VALUE;
public void addEvent(TraceEvent event) {
events.add(event);
startTime = Math.min(startTime, event.getTimestamp());
endTime = Math.max(endTime, event.getTimestamp());
}
public void updateCategoryStats(String category) {
categoryStats.merge(category, 1L, Long::sum);
}
public void updateMethodStats(String method) {
methodStats.merge(method, 1L, Long::sum);
}
// Getters
public List<TraceEvent> getEvents() { return events; }
public Map<String, Long> getCategoryStats() { return categoryStats; }
public Map<String, Long> getMethodStats() { return methodStats; }
public int getTotalEvents() { return events.size(); }
public double getTraceDurationSeconds() {
return (endTime - startTime) / 1_000_000_000.0;
}
public double getEventsPerSecond() {
double duration = getTraceDurationSeconds();
return duration > 0 ? events.size() / duration : 0;
}
}
public static class TraceEvent {
private final String category;
private final String name;
private final String type;
private final long timestamp;
public TraceEvent(String category, String name, String type, long timestamp) {
this.category = category;
this.name = name;
this.type = type;
this.timestamp = timestamp;
}
// Getters
public String getCategory() { return category; }
public String getName() { return name; }
public String getType() { return type; }
public long getTimestamp() { return timestamp; }
}
}
Best Practices and Configuration
Step 8: Production Configuration
package com.example.perfetto.config;
import java.util.*;
public class PerfettoProductionConfig {
public static class ProductionTracingConfig {
private final boolean enabled;
private final String tracePath;
private final long maxFileSize;
private final double samplingRate;
private final Set<String> excludedCategories;
private final Map<String, TracingLevel> categoryLevels;
public ProductionTracingConfig(boolean enabled, String tracePath, long maxFileSize,
double samplingRate, Set<String> excludedCategories,
Map<String, TracingLevel> categoryLevels) {
this.enabled = enabled;
this.tracePath = tracePath;
this.maxFileSize = maxFileSize;
this.samplingRate = samplingRate;
this.excludedCategories = excludedCategories;
this.categoryLevels = categoryLevels;
}
public boolean isCategoryEnabled(String category) {
return enabled &&
!excludedCategories.contains(category) &&
(samplingRate == 1.0 || Math.random() < samplingRate);
}
public TracingLevel getTracingLevel(String category) {
return categoryLevels.getOrDefault(category, TracingLevel.BASIC);
}
public static Builder builder() {
return new Builder();
}
}
public enum TracingLevel {
NONE, // No tracing
BASIC, // Method entry/exit only
DETAILED, // Includes arguments
VERBOSE // Includes arguments and return values
}
public static class Builder {
private boolean enabled = false;
private String tracePath = "/var/log/perfetto/trace.pftrace";
private long maxFileSize = 100 * 1024 * 1024; // 100MB
private double samplingRate = 0.1; // 10% sampling
private Set<String> excludedCategories = new HashSet<>();
private Map<String, TracingLevel> categoryLevels = new HashMap<>();
public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
public Builder tracePath(String path) {
this.tracePath = path;
return this;
}
public Builder maxFileSize(long size) {
this.maxFileSize = size;
return this;
}
public Builder samplingRate(double rate) {
this.samplingRate = Math.max(0.0, Math.min(1.0, rate));
return this;
}
public Builder excludeCategory(String category) {
this.excludedCategories.add(category);
return this;
}
public Builder setCategoryLevel(String category, TracingLevel level) {
this.categoryLevels.put(category, level);
return this;
}
public ProductionTracingConfig build() {
// Set default levels for common categories
categoryLevels.putIfAbsent("database.queries", TracingLevel.BASIC);
categoryLevels.putIfAbsent("http.requests", TracingLevel.DETAILED);
categoryLevels.putIfAbsent("business.services", TracingLevel.BASIC);
categoryLevels.putIfAbsent("system.metrics", TracingLevel.BASIC);
return new ProductionTracingConfig(enabled, tracePath, maxFileSize,
samplingRate, excludedCategories, categoryLevels);
}
}
// Runtime configuration management
public static class DynamicTracingConfig {
private ProductionTracingConfig currentConfig;
private final JavaPerfettoClient perfettoClient;
public DynamicTracingConfig(JavaPerfettoClient perfettoClient) {
this.perfettoClient = perfettoClient;
this.currentConfig = loadInitialConfig();
}
public void updateConfig(ProductionTracingConfig newConfig) {
this.currentConfig = newConfig;
// Apply configuration changes
if (newConfig.enabled) {
perfettoClient.startTracing(/* appropriate config */);
} else {
perfettoClient.stopTracing();
}
// Trace the configuration change
Map<String, String> args = new HashMap<>();
args.put("samplingRate", String.valueOf(newConfig.samplingRate));
args.put("enabled", String.valueOf(newConfig.enabled));
perfettoClient.traceInstant("tracing.config", "config_updated", args);
}
public ProductionTracingConfig getCurrentConfig() {
return currentConfig;
}
private ProductionTracingConfig loadInitialConfig() {
return ProductionTracingConfig.builder()
.enabled(Boolean.parseBoolean(System.getProperty("perfetto.enabled", "false")))
.samplingRate(Double.parseDouble(System.getProperty("perfetto.samplingRate", "0.1")))
.tracePath(System.getProperty("perfetto.tracePath", "/tmp/trace.pftrace"))
.build();
}
}
}
Key Benefits and Integration Summary
Benefits of Perfetto Integration:
- Comprehensive Tracing: End-to-end visibility across application layers
- Low Overhead: Efficient tracing with configurable sampling
- Standardized Format: Compatible with Perfetto UI and analysis tools
- Production Ready: Configurable for different environments
Integration Points:
- Method-level tracing with annotations
- Database query tracing with wrapped DataSource
- HTTP client/server tracing for external calls
- Async operation tracing for CompletableFuture and threads
- Custom metrics for business and system monitoring
Best Practices:
- Use sampling in production to minimize overhead
- Configure appropriate trace buffer sizes
- Implement proper cleanup and resource management
- Use meaningful category names for better analysis
- Combine with application metrics for comprehensive monitoring
This comprehensive Perfetto integration provides deep visibility into Java application performance, enabling effective troubleshooting and optimization in production environments.