Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtual threads metrics #5067

Merged
merged 20 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ subprojects {

check.dependsOn("testModules")

if (!(project.name in ['micrometer-test-aspectj-ltw', 'micrometer-test-aspectj-ctw'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp
if (!(project.name in ['micrometer-test-aspectj-ltw', 'micrometer-test-aspectj-ctw', 'micrometer-java21'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp
apply plugin: 'me.champeau.gradle.japicmp'
apply plugin: 'de.undercouch.download'

Expand Down
23 changes: 23 additions & 0 deletions micrometer-java21/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
description 'Micrometer core classes that require Java 21'

// skip this module when building with jdk <21
if (!javaLanguageVersion.canCompileOrRun(21)) {
project.tasks.configureEach { task -> task.enabled = false }
}

dependencies {
api project(":micrometer-core")
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved

testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.awaitility:awaitility'
}

java {
targetCompatibility = 21
}

tasks.withType(JavaCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
options.release = 21
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2024 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.java21.instrument.binder.jfr;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.MeterBinder;
import jdk.jfr.consumer.RecordingStream;

import java.io.Closeable;
import java.io.IOException;
import java.time.Duration;
import java.util.Objects;

import static java.util.Collections.emptyList;

public class JfrVirtualThreadEventMetrics implements MeterBinder, Closeable {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved

private static final String PINNED_EVENT = "jdk.VirtualThreadPinned";

private static final String SUBMIT_FAILED_EVENT = "jdk.VirtualThreadSubmitFailed";

private final RecordingSettings settings;

private final Iterable<Tag> tags;

private boolean started = false;

private RecordingStream recordingStream;

public JfrVirtualThreadEventMetrics() {
this(new RecordingSettings(), emptyList());
}

public JfrVirtualThreadEventMetrics(Iterable<Tag> tags) {
this(new RecordingSettings(), tags);
}

public JfrVirtualThreadEventMetrics(RecordingSettings settings, Iterable<Tag> tags) {
this.settings = settings;
this.tags = tags;
}

@Override
public void bindTo(MeterRegistry registry) {
if (started) {
return;
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
}

started = true;
recordingStream = createRecordingStream(settings);
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved

final Timer pinnedTimer = Timer.builder("jvm.virtual.thread.pinned")
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
.tags(tags)
.description("The duration of virtual threads that were pinned to a physical thread")
.register(registry);
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved

final Counter submitFailedCounter = Counter.builder("jvm.virtual.thread.submit.failed")
.tags(tags)
.description("The number of virtual thread submissions that failed")
.register(registry);
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved

recordingStream.onEvent(PINNED_EVENT, event -> pinnedTimer.record(event.getDuration()));
recordingStream.onEvent(SUBMIT_FAILED_EVENT, event -> submitFailedCounter.increment());
}

protected RecordingStream createRecordingStream(RecordingSettings settings) {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
final RecordingStream recordingStream = new RecordingStream();
recordingStream.enable(PINNED_EVENT).withThreshold(settings.pinnedThreshold);
recordingStream.enable(SUBMIT_FAILED_EVENT);
recordingStream.setMaxAge(settings.maxAge);
recordingStream.setMaxSize(settings.maxSizeBytes);
recordingStream.startAsync();
return recordingStream;
}

@Override
public void close() throws IOException {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
if (started) {
recordingStream.close();
}
}

public record RecordingSettings(Duration maxAge, long maxSizeBytes, Duration pinnedThreshold) {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
public RecordingSettings {
Objects.requireNonNull(maxAge, "maxAge parameter must not be null");
Objects.requireNonNull(pinnedThreshold, "pinnedThreshold must not be null");
if (maxSizeBytes < 0) {
throw new IllegalArgumentException("maxSizeBytes must be positive");
}
}

public RecordingSettings() {
this(Duration.ofSeconds(5), 10L * 1024 * 1024, Duration.ofMillis(20));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Instrumentation of JDK classes.
*/
@NonNullApi
@NonNullFields
package io.micrometer.java21.instrument.binder.jfr;

import io.micrometer.common.lang.NonNullApi;
import io.micrometer.common.lang.NonNullFields;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2022 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.java21.instrument.binder.jfr;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.time.Duration;

import static org.awaitility.Awaitility.await;

class JfrVirtualThreadEventMetricsTests {

private static final Tags USER_TAGS = Tags.of("k", "v");

private MeterRegistry registry;

private JfrVirtualThreadEventMetrics jfrMetrics;

@BeforeEach
void setup() {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
registry = new SimpleMeterRegistry();
jfrMetrics = new JfrVirtualThreadEventMetrics(USER_TAGS);
jfrMetrics.bindTo(registry);
}

@Test
void registerPinnedEvent() throws Exception {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
Thread.ofVirtual().name("vt-test").start(() -> {
synchronized (this) {
sleep(Duration.ofMillis(100));
}
}).join();

await().atMost(Duration.ofSeconds(2))
.until(() -> registry.get("jvm.virtual.thread.pinned").tags(USER_TAGS).timer().count() == 1);
}

private void sleep(Duration duration) {
try {
Thread.sleep(duration);
}
catch (InterruptedException ignored) {
}
}

@AfterEach
void cleanup() throws IOException {
jonatan-ivanov marked this conversation as resolved.
Show resolved Hide resolved
jfrMetrics.close();
}

}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ include 'concurrency-tests'
include 'micrometer-bom'
include 'micrometer-jakarta9'
include 'micrometer-java11'
include 'micrometer-java21'
include 'micrometer-jetty11'
include 'micrometer-jetty12'
include 'micrometer-osgi-test'
Expand Down