Skip to content

Commit 941aa40

Browse files
authored
Merge pull request #83 from avaje/feature/gc-pause
Add JvmGCPause
2 parents 8040dc9 + 613ebe7 commit 941aa40

File tree

6 files changed

+137
-17
lines changed

6 files changed

+137
-17
lines changed

metrics/src/main/java/io/avaje/metrics/MTags.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,30 @@ final class MTags implements Tags {
77

88
static Tags EMPTY = new MTags(new String[]{});
99

10-
private final String[] keyValuePairs;
10+
private final String[] rawTags;
1111

12-
MTags(String[] keyValuePairs) {
13-
if (keyValuePairs.length % 2 != 0) {
14-
throw new IllegalArgumentException("Incorrect length, must be pairs of key values");
15-
}
16-
this.keyValuePairs = keyValuePairs;
12+
MTags(String[] rawTags) {
13+
this.rawTags = rawTags;
1714
}
1815

1916
@Override
2017
public boolean isEmpty() {
21-
return keyValuePairs.length == 0;
18+
return rawTags.length == 0;
2219
}
2320

2421
@Override
2522
public String[] array() {
26-
return keyValuePairs;
23+
return rawTags;
2724
}
2825

2926
@Override
3027
public String[] append(String... moreTags) {
31-
if (keyValuePairs.length == 0) {
28+
if (rawTags.length == 0) {
3229
return moreTags;
3330
}
34-
String[] merged = new String[keyValuePairs.length + moreTags.length];
35-
System.arraycopy(keyValuePairs, 0, merged, 0, keyValuePairs.length);
36-
System.arraycopy(moreTags, 0, merged, keyValuePairs.length, moreTags.length);
31+
String[] merged = new String[rawTags.length + moreTags.length];
32+
System.arraycopy(rawTags, 0, merged, 0, rawTags.length);
33+
System.arraycopy(moreTags, 0, merged, rawTags.length, moreTags.length);
3734
return merged;
3835
}
3936

@@ -42,16 +39,16 @@ public boolean equals(Object object) {
4239
if (this == object) return true;
4340
if (!(object instanceof MTags)) return false;
4441
MTags dTags = (MTags) object;
45-
return Objects.deepEquals(keyValuePairs, dTags.keyValuePairs);
42+
return Objects.deepEquals(rawTags, dTags.rawTags);
4643
}
4744

4845
@Override
4946
public int hashCode() {
50-
return Arrays.hashCode(keyValuePairs);
47+
return Arrays.hashCode(rawTags);
5148
}
5249

5350
@Override
5451
public String toString() {
55-
return keyValuePairs.length == 0 ? "" : "tags:" + Arrays.toString(keyValuePairs);
52+
return rawTags.length == 0 ? "" : "tags:" + Arrays.toString(rawTags);
5653
}
5754
}

metrics/src/main/java/io/avaje/metrics/core/DefaultMetricProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public JvmMetrics registerJvmThreadMetrics() {
153153
@Override
154154
public JvmMetrics registerJvmGCMetrics() {
155155
JvmGarbageCollection.createGauges(this, withDetails);
156+
JvmGCPause.createMeters(this);
156157
return this;
157158
}
158159

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.avaje.metrics.core;
2+
3+
import com.sun.management.GarbageCollectionNotificationInfo;
4+
import io.avaje.applog.AppLog;
5+
import io.avaje.metrics.Meter;
6+
import io.avaje.metrics.MetricRegistry;
7+
8+
import javax.management.Notification;
9+
import javax.management.NotificationEmitter;
10+
import javax.management.NotificationFilter;
11+
import javax.management.NotificationListener;
12+
import javax.management.openmbean.CompositeData;
13+
import java.lang.management.GarbageCollectorMXBean;
14+
import java.lang.management.ManagementFactory;
15+
import java.lang.management.MemoryPoolMXBean;
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
import java.util.List;
19+
20+
import static com.sun.management.GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION;
21+
import static java.lang.System.Logger.Level.INFO;
22+
23+
final class JvmGCPause {
24+
25+
private static final System.Logger log = AppLog.getLogger("io.avaje.metrics");
26+
27+
static void createMeters(MetricRegistry registry) {
28+
if (!extensionsPresent() || !hasNotifications()) {
29+
return;
30+
}
31+
32+
final var listener = new Listener(registry);
33+
final var filter = new Filter();
34+
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
35+
if (gcBean instanceof NotificationEmitter) {
36+
final var emitter = (NotificationEmitter) gcBean;
37+
emitter.addNotificationListener(listener, filter, null);
38+
}
39+
}
40+
}
41+
42+
static final class Filter implements NotificationFilter {
43+
@Override
44+
public boolean isNotificationEnabled(Notification notification) {
45+
return GARBAGE_COLLECTION_NOTIFICATION.equals(notification.getType());
46+
}
47+
}
48+
49+
static final class Listener implements NotificationListener {
50+
51+
private final Meter concurrent;
52+
private final Meter pause;
53+
54+
Listener(MetricRegistry registry) {
55+
this.concurrent = registry.meter("jvm.gc.concurrent");
56+
this.pause = registry.meter("jvm.gc.pause");
57+
}
58+
59+
@Override
60+
public void handleNotification(Notification notification, Object ref) {
61+
CompositeData cd = (CompositeData) notification.getUserData();
62+
GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd);
63+
64+
String gcName = notificationInfo.getGcName();
65+
String gcCause = notificationInfo.getGcCause();
66+
long duration = notificationInfo.getGcInfo().getDuration();
67+
68+
if (isConcurrentPhase(gcCause, gcName)) {
69+
concurrent.addEvent(duration);
70+
} else {
71+
pause.addEvent(duration);
72+
}
73+
}
74+
}
75+
76+
private static boolean isConcurrentPhase(String cause, String name) {
77+
return "No GC".equals(cause)
78+
|| "Shenandoah Cycles".equals(name)
79+
|| (name.startsWith("ZGC") && name.endsWith("Cycles")) // ZGC
80+
|| (name.startsWith("GPGC") && !name.endsWith("Pauses")) // Zing
81+
;
82+
}
83+
84+
private static boolean extensionsPresent() {
85+
if (ManagementFactory.getMemoryPoolMXBeans().isEmpty()) {
86+
// native-image Substrate VM
87+
log.log(INFO, "GC notifications not available, empty MemoryPoolMXBeans");
88+
return false;
89+
}
90+
91+
try {
92+
Class.forName("com.sun.management.GarbageCollectionNotificationInfo", false,
93+
MemoryPoolMXBean.class.getClassLoader());
94+
return true;
95+
} catch (Throwable e) {
96+
// We are operating in a JVM without access to this level of detail
97+
log.log(INFO, "GC notifications not available - no com.sun.management.GarbageCollectionNotificationInfo");
98+
return false;
99+
}
100+
}
101+
102+
private static boolean hasNotifications() {
103+
List<String> gcsWithoutNotification = new ArrayList<>();
104+
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
105+
if (!(gcBean instanceof NotificationEmitter)) {
106+
continue;
107+
}
108+
final var emitter = (NotificationEmitter) gcBean;
109+
boolean notificationAvailable = Arrays.stream(emitter.getNotificationInfo())
110+
.anyMatch(notificationInfo -> Arrays.asList(notificationInfo.getNotifTypes())
111+
.contains(GARBAGE_COLLECTION_NOTIFICATION));
112+
if (notificationAvailable) {
113+
return true;
114+
}
115+
gcsWithoutNotification.add(gcBean.getName());
116+
}
117+
log.log(INFO, "GC notifications not available GCs=" + gcsWithoutNotification);
118+
return false;
119+
}
120+
}

metrics/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
requires transitive io.avaje.applog;
1111
requires transitive org.jspecify;
1212
requires static java.management;
13+
requires static jdk.management;
1314

1415
uses io.avaje.metrics.spi.SpiMetricBuilder;
1516
uses io.avaje.metrics.spi.SpiMetricProvider;

metrics/src/test/java/io/avaje/metrics/MetricIDTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ void equalToWithTag() {
2828

2929
@Test
3030
void suffix() {
31-
var one = Metric.ID.of("one", Tags.of("a", "b"));
31+
var one = Metric.ID.of("one", Tags.of("a:b"));
3232
Metric.ID suffix = one.suffix(".error");
3333
assertThat(suffix.name()).isEqualTo("one.error");
3434
assertThat(suffix.tags()).isEqualTo(one.tags());

metrics/src/test/java/io/avaje/metrics/core/JvmGCLoadTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ class JvmGCLoadTest {
1212
@Test
1313
void test() throws InterruptedException {
1414
DefaultMetricProvider registry = new DefaultMetricProvider();
15-
JvmGarbageCollection.createGauges(registry, true);
15+
new JvmGCPause().createMeters(registry);
16+
JvmGarbageCollection.createGauges(registry, false);
1617
for (int i = 0; i < 3; i++) {
1718
doSomething(registry);
1819
}

0 commit comments

Comments
 (0)