Skip to content

Commit

Permalink
Add Java Flight Recorder support
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielThomas committed Aug 29, 2024
1 parent c0f3e5f commit f060c09
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 2 deletions.
50 changes: 48 additions & 2 deletions spectator-ext-jvm/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,58 @@
sourceSets {
java17 {
java {
srcDirs = ['src/main/java17']
compileClasspath = configurations.compileClasspath
runtimeClasspath = configurations.runtimeClasspath
}
}
java17Test {
java {
srcDirs = ['src/test/java17']
compileClasspath = jar.outputs.files + configurations.testCompileClasspath
runtimeClasspath = jar.outputs.files + runtimeClasspath + configurations.testRuntimeClasspath
}
}
}

dependencies {
api project(':spectator-api')
implementation 'com.typesafe:config'
}

jar {
def java17Compiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(17)
}

tasks.named('compileJava17Java', JavaCompile).configure {
javaCompiler = java17Compiler
}

tasks.named('compileJava17TestJava', JavaCompile).configure {
javaCompiler = java17Compiler
}

tasks.named('jar').configure {
into('META-INF/versions/17') {
from sourceSets.java17.output
}
manifest {
attributes(
"Automatic-Module-Name": "com.netflix.spectator.jvm"
'Automatic-Module-Name': 'com.netflix.spectator.jvm',
'Multi-Release': 'true'
)
}
}

def testJava17 = tasks.register('testJava17', Test) {
description = "Runs tests for java17Test sourceset."
group = 'verification'

testClassesDirs = sourceSets.java17Test.output.classesDirs
classpath = sourceSets.java17Test.runtimeClasspath

javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(17)
}
}
check.dependsOn testJava17
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.netflix.spectator.jvm;

import com.netflix.spectator.api.Registry;
import jdk.jfr.EventSettings;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingStream;

import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;

/**
* Helpers supporting continuous monitoring with Java Flight Recorder.
*/
public class JavaFlightRecorder {

private static final String PREFIX = "jdk.";
private static final String CPULoad = PREFIX + "CPULoad";
private static final String VirtualThreadPinned = PREFIX + "VirtualThreadPinned";
private static final String VirtualThreadSubmitFailed = PREFIX + "VirtualThreadSubmitFailed";
private static final String ZAllocationStall = PREFIX + "ZAllocationStall";
private static final String ZYoungGarbageCollection = PREFIX + "ZYoungGarbageCollection";
private static final String ZOldGarbageCollection = PREFIX + "ZOldGarbageCollection";

private JavaFlightRecorder() {
}

/**
* Collect low-overhead Java Flight Recorder events, using the provided
* {@link Executor} to execute a single task to collect events.
*
* @param registry the registry
* @param executor the executor to execute the task for streaming events
* @return an {@link AutoCloseable} allowing the underlying
* {@link jdk.jfr.consumer.EventStream} to be closed
*/
public static AutoCloseable monitorDefaultEvents(Registry registry, Executor executor) {
Objects.requireNonNull(registry);
Objects.requireNonNull(executor);
RecordingStream rs = new RecordingStream();
collectCpuEvents(registry, rs);
collectVirtualThreadEvents(registry, rs);
collectZgcEvents(registry, rs);
executor.execute(rs::start);
return rs::close;
}

private static void collectCpuEvents(Registry registry, RecordingStream rs) {
consume(CPULoad, rs, event -> {
String pid = "unknown";
try {
pid = String.valueOf(ProcessHandle.current().pid());
} catch (UnsupportedOperationException ignore) {
}
registry.gauge("jvm.cpuLoad", "type", "jvmUser", "pid", pid)
.set(event.getFloat("jvmUser"));
registry.gauge("jvm.cpuLoad", "type", "jvmSystem", "pid", pid)
.set(event.getFloat("jvmSystem"));
registry.gauge("jvm.cpuLoad", "type", "machineTotal", "pid", pid)
.set(event.getFloat("machineTotal"));
}).withPeriod(Duration.ofSeconds(1));
}

private static void collectVirtualThreadEvents(Registry registry, RecordingStream rs) {
consume(VirtualThreadPinned, rs, event ->
registry.timer("jvm.vt.pinned").record(event.getDuration())
).withThreshold(Duration.ofMillis(20));
consume(VirtualThreadSubmitFailed, rs, event ->
registry.counter("jvm.vt.submitFailed").increment()
);
}

private static void collectZgcEvents(Registry registry, RecordingStream rs) {
consume(ZYoungGarbageCollection, rs, event ->
registry.timer("jvm.zgc.youngCollection", "tenuringThreshold", event.getString("tenuringThreshold"))
.record(event.getDuration()));

consume(ZOldGarbageCollection, rs, event ->
registry.timer("jvm.zgc.oldCollection")
.record(event.getDuration()));

consume(ZAllocationStall, rs, event ->
registry.timer("jvm.zgc.allocationStall", "type", event.getString("type"))
.record(event.getDuration()));
}

/**
* Consume a given JFR event. For full event details see the event definitions and default/profiling configuration:
* <p>
* - <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/jfr/metadata/metadata.xml">metadata.xml</a>
* - <a href="https://github.com/openjdk/jdk/blob/master/src/jdk.jfr/share/conf/jfr/default.jfc">default.jfc</a>
* - <a href="https://github.com/openjdk/jdk/blob/master/src/jdk.jfr/share/conf/jfr/profile.jfc">profile.jfc</a>
* <p>
* We avoid the default event configurations because despite their claims of "low-overhead" there are
* situtations where they can impose significant overhead to the application.
*/
private static EventSettings consume(String name, RecordingStream rs, Consumer<RecordedEvent> consumer) {
EventSettings settings = rs.enable(name)
.withoutStackTrace() // ensure no events have stacktraces accidentally enabled
.withThreshold(Duration.ofMillis(0));
rs.onEvent(name, consumer);
return settings;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.netflix.spectator.jvm;

import com.netflix.spectator.api.DefaultRegistry;
import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.Measurement;
import com.netflix.spectator.api.Registry;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class JavaFlightRecorderTest {

@Test
public void test() throws Exception {
Registry registry = new DefaultRegistry();
ExecutorService executor = Executors.newSingleThreadExecutor();
try (var closable = JavaFlightRecorder.monitorDefaultEvents(registry, executor)) {
Thread.sleep(2000);
}
executor.shutdownNow();

Assertions.assertEquals(3, registry.measurements().count());
List<String> distinctMeasures = registry.measurements()
.map(Measurement::id)
.map(Id::name)
.distinct()
.toList();
Assertions.assertEquals(1, distinctMeasures.size());
Assertions.assertEquals("jvm.cpuLoad", distinctMeasures.get(0));
}

}

0 comments on commit f060c09

Please sign in to comment.