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

Implement memory metrics #652

Merged
merged 11 commits into from
Jan 14, 2023
48 changes: 48 additions & 0 deletions jfr-streaming/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,51 @@ tasks {
useJUnitPlatform()
}
}

testing {
suites {

val serialGcTest by registering(JvmTestSuite::class) {
dependencies {
implementation("io.opentelemetry:opentelemetry-sdk-testing")
}
targets {
all {
testTask {
jvmArgs = listOf("-XX:+UseSerialGC")
}
}
}
}
val parallelGcTest by registering(JvmTestSuite::class) {
dependencies {
implementation("io.opentelemetry:opentelemetry-sdk-testing")
}
targets {
all {
testTask {
jvmArgs = listOf("-XX:+UseParallelGC")
}
}
}
}
val g1GcTest by registering(JvmTestSuite::class) {
dependencies {
implementation("io.opentelemetry:opentelemetry-sdk-testing")
}
targets {
all {
testTask {
jvmArgs = listOf("-XX:+UseG1GC")
}
}
}
}
}
}

tasks {
check {
dependsOn(testing.suites)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jfr.metrics;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.awaitility.Awaitility.await;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.testing.assertj.MetricAssert;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import java.util.Collection;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeAll;

public class AbstractMetricsTest {

static SdkMeterProvider meterProvider;
static InMemoryMetricReader metricReader;
static boolean isInitialized = false;

@BeforeAll
static void initializeOpenTelemetry() {
if (isInitialized) {
return;
}
isInitialized = true;
metricReader = InMemoryMetricReader.create();
meterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build();
GlobalOpenTelemetry.set(OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build());
JfrMetrics.enable(meterProvider);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: This abstract base class needs some work:

  • Should be renamed to something more descriptive, maybe AbstractJfrTest
  • Should not have state which is shared across all tests. Right now that's required because JfrMetrics can't be cancelled once started. We should change that.
  • Should be put in a common place where it can be used by each of the different testing suites, rather than repeated in each test suite

Can do all this in a future PR.

Copy link
Contributor Author

@roberttoyonaga roberttoyonaga Jan 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup I agree with all those points.

Right now that's required because JfrMetrics can't be cancelled once started

Maybe @BeforeEach test I can reinitialize JFR streaming and all openTelemetry variables, and @AfterEach test the JFR streaming can be ended. I think that should be enough for tests to fully clean up after themselves.

Should be put in a common place where it can be used by each of the different testing suites

I was trying to figure out a good way of doing this today. Maybe something related to test fixtures. I'll have to investigate more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update:
I renamed the base test class to AbstractJfrTest. I was able to get the testFixtures working. So now the class resides in a single place, instead of being duplicated for each GC type.

}

@SafeVarargs
protected final void waitAndAssertMetrics(Consumer<MetricAssert>... assertions) {
await()
.untilAsserted(
() -> {
Collection<MetricData> metrics = metricReader.collectAllMetrics();

assertThat(metrics).isNotEmpty();

for (Consumer<MetricAssert> assertion : assertions) {
assertThat(metrics).anySatisfy(metric -> assertion.accept(assertThat(metric)));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jfr.metrics;

import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_ACTION;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_G1_EDEN_SPACE;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_G1_SURVIVOR_SPACE;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_GC;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.BYTES;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.END_OF_MAJOR_GC;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.END_OF_MINOR_GC;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_DESCRIPTION_COMMITTED;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_DESCRIPTION_GC_DURATION;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_DESCRIPTION_MEMORY;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_DESCRIPTION_MEMORY_AFTER;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_NAME_MEMORY;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_NAME_MEMORY_AFTER;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.metrics.data.HistogramData;
import io.opentelemetry.sdk.metrics.data.HistogramPointData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.metrics.data.SumData;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;

class G1GcMemoryMetricTest extends AbstractMetricsTest {
private void usageCheck(ThrowingConsumer<MetricData> attributeCheck) {
waitAndAssertMetrics(
metric ->
metric
.hasName(METRIC_NAME_MEMORY)
.hasUnit(BYTES)
.hasDescription(METRIC_DESCRIPTION_MEMORY)
.satisfies(attributeCheck),
metric ->
metric
.hasName(METRIC_NAME_MEMORY_AFTER)
.hasUnit(BYTES)
.hasDescription(METRIC_DESCRIPTION_MEMORY_AFTER)
.satisfies(attributeCheck));
}

/**
* This is a basic test for process.runtime.jvm.memory.usage and
* process.runtime.jvm.memory.usage_after_last_gc metrics.
*/
@Test
void shouldHaveMemoryUsageMetrics() {
System.gc();
// Test to make sure there's metric data for both eden and survivor spaces.
// TODO: once G1 old gen usage added to jdk.G1HeapSummary (in JDK 21), test for it here too.
usageCheck(
metricData -> {
SumData<?> sumData = metricData.getLongSumData();
assertThat(sumData.getPoints())
.anyMatch(p -> p.getAttributes().equals(ATTR_G1_EDEN_SPACE))
.anyMatch(p -> p.getAttributes().equals(ATTR_G1_SURVIVOR_SPACE));
});
}

@Test
void shouldHaveMemoryLimitMetrics() {
// TODO: needs JFR support. Placeholder.
}

@Test
void shouldHaveMemoryCommittedMetrics() {
System.gc();
// TODO: need JFR support for the other G1 pools
waitAndAssertMetrics(
metric ->
metric
.hasName("process.runtime.jvm.memory.committed")
.hasUnit(BYTES)
.hasDescription(METRIC_DESCRIPTION_COMMITTED)
.satisfies(
metricData -> {
SumData<?> sumData = metricData.getLongSumData();
assertThat(sumData.getPoints())
.anyMatch(p -> p.getAttributes().equals(ATTR_G1_EDEN_SPACE));
}));
}

@Test
void shouldHaveGCDurationMetrics() {
// TODO: Need a reliable way to test old and young gen GC in isolation.
System.gc();
waitAndAssertMetrics(
metric ->
metric
.hasName("process.runtime.jvm.gc.duration")
.hasUnit(MILLISECONDS)
.hasDescription(METRIC_DESCRIPTION_GC_DURATION)
.satisfies(
metricData -> {
HistogramData data = metricData.getHistogramData();
assertThat(data.getPoints())
.map(HistogramPointData.class::cast)
.anyMatch(
p ->
p.getSum() > 0
&& (p.getAttributes()
.equals(
Attributes.of(
ATTR_GC,
"G1 Young Generation",
ATTR_ACTION,
END_OF_MINOR_GC))
|| p.getAttributes()
.equals(
Attributes.of(
ATTR_GC,
"G1 Old Generation",
ATTR_ACTION,
END_OF_MAJOR_GC))));
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
import io.opentelemetry.contrib.jfr.metrics.internal.cpu.ContextSwitchRateHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.cpu.LongLockHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.cpu.OverallCPULoadHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.CodeCacheConfigurationHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.G1HeapSummaryHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.GCHeapSummaryHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.MetaspaceSummaryHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.ObjectAllocationInNewTLABHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.ObjectAllocationOutsideTLABHandler;
import io.opentelemetry.contrib.jfr.metrics.internal.memory.ParallelHeapSummaryHandler;
Expand All @@ -41,7 +42,6 @@ private HandlerRegistry(List<? extends RecordedEventHandler> mappers) {

static HandlerRegistry createDefault(MeterProvider meterProvider) {
var handlers = new ArrayList<RecordedEventHandler>();

for (var bean : ManagementFactory.getGarbageCollectorMXBeans()) {
var name = bean.getName();
switch (name) {
Expand Down Expand Up @@ -83,13 +83,14 @@ static HandlerRegistry createDefault(MeterProvider meterProvider) {
new ObjectAllocationOutsideTLABHandler(grouper),
new NetworkReadHandler(grouper),
new NetworkWriteHandler(grouper),
new GCHeapSummaryHandler(),
new ContextSwitchRateHandler(),
new OverallCPULoadHandler(),
new ContainerConfigurationHandler(),
new LongLockHandler(grouper),
new ThreadCountHandler(),
new ClassesLoadedHandler(),
new MetaspaceSummaryHandler(),
new CodeCacheConfigurationHandler(),
new DirectBufferStatisticsHandler());
handlers.addAll(basicHandlers);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package io.opentelemetry.contrib.jfr.metrics.internal;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;

public final class Constants {
private Constants() {}
Expand Down Expand Up @@ -71,6 +72,23 @@ private Constants() {}
public static final AttributeKey<String> ATTR_GC = AttributeKey.stringKey("pool");
public static final AttributeKey<String> ATTR_ACTION = AttributeKey.stringKey("action");
public static final AttributeKey<Boolean> ATTR_DAEMON = AttributeKey.booleanKey(DAEMON);
public static final Attributes ATTR_PS_EDEN_SPACE =
Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "PS Eden Space");
public static final Attributes ATTR_PS_SURVIVOR_SPACE =
Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "PS Survivor Space");
public static final Attributes ATTR_PS_OLD_GEN =
Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "PS Old Gen");
public static final Attributes ATTR_G1_SURVIVOR_SPACE =
Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "G1 Survivor Space");
public static final Attributes ATTR_G1_EDEN_SPACE =
Attributes.of(ATTR_TYPE, HEAP, ATTR_POOL, "G1 Eden Space");
public static final Attributes ATTR_METASPACE =
Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "Metaspace");
public static final Attributes ATTR_COMPRESSED_CLASS_SPACE =
Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "Compressed Class Space");
public static final Attributes ATTR_CODE_CACHE =
Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "CodeCache");

public static final String UNIT_CLASSES = "{classes}";
public static final String UNIT_THREADS = "{threads}";
public static final String UNIT_BUFFERS = "{buffers}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.jfr.metrics.internal.memory;

import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_POOL;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_TYPE;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.BYTES;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.INITIAL_SIZE;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_DESCRIPTION_MEMORY_INIT;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.METRIC_NAME_MEMORY_INIT;
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.NON_HEAP;
import static io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler.defaultMeter;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler;
import java.time.Duration;
import java.util.Optional;
import jdk.jfr.consumer.RecordedEvent;

/** Handles attributes with pool value CodeCache */
public final class CodeCacheConfigurationHandler implements RecordedEventHandler {
private static final String EVENT_NAME = "jdk.CodeCacheConfiguration";

private static final Attributes ATTR = Attributes.of(ATTR_TYPE, NON_HEAP, ATTR_POOL, "CodeCache");

private volatile long initialSize = 0;

public CodeCacheConfigurationHandler() {
initializeMeter(defaultMeter());
}

@Override
public void initializeMeter(Meter meter) {
meter
.upDownCounterBuilder(METRIC_NAME_MEMORY_INIT)
.setDescription(METRIC_DESCRIPTION_MEMORY_INIT)
.setUnit(BYTES)
.buildWithCallback(measurement -> measurement.record(initialSize, ATTR));
}

@Override
public String getEventName() {
return EVENT_NAME;
}

@Override
public void accept(RecordedEvent event) {
if (event.hasField(INITIAL_SIZE)) {
initialSize = event.getLong(INITIAL_SIZE);
}
}

@Override
public Optional<Duration> getPollingDuration() {
return Optional.of(Duration.ofSeconds(1));
}
}
Loading