Skip to content

Commit 1373ceb

Browse files
committed
8356698: JFR: @contextual
Reviewed-by: mgronlun
1 parent ec02a87 commit 1373ceb

File tree

6 files changed

+703
-8
lines changed

6 files changed

+703
-8
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package jdk.jfr;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Event field annotation, specifies that the value carries contextual
10+
* information.
11+
* <p>
12+
* Contextual information is data that applies to all events happening in the
13+
* same thread from the beginning to the end of the event with a field annotated
14+
* with {@code Contextual}.
15+
* <p>
16+
* For example, to trace requests or transactions in a system, a trace event can
17+
* be created to provide context.
18+
* {@snippet class = "Snippets" region = "ContextualTrace"}
19+
* <p>
20+
* To track details within an order service, an order event can be created where
21+
* only the order ID provides context.
22+
* {@snippet class = "Snippets" region = "ContextualOrder"}
23+
* <p>
24+
* If an order in the order service stalls due to lock contention, a user
25+
* interface can display contextual information together with the
26+
* JavaMonitorEnter event to simplify troubleshooting, for example:
27+
* {@snippet lang=text :
28+
* $ jfr print --events JavaMonitorEnter recording.jfr
29+
* jdk.JavaMonitorEnter {
30+
* Context: Trace.id = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
31+
* Context: Trace.name = "POST /checkout/place-order"
32+
* Context: Order.id = 314159
33+
* startTime = 17:51:29.038 (2025-02-07)
34+
* duration = 50.56 ms
35+
* monitorClass = java.util.ArrayDeque (classLoader = bootstrap)
36+
* previousOwner = "Order Thread" (javaThreadId = 56209, virtual = true)
37+
* address = 0x60000232ECB0
38+
* eventThread = "Order Thread" (javaThreadId = 52613, virtual = true)
39+
* stackTrace = [
40+
* java.util.zip.ZipFile$CleanableResource.getInflater() line: 685
41+
* java.util.zip.ZipFile$ZipFileInflaterInputStream.<init>(ZipFile) line: 388
42+
* java.util.zip.ZipFile.getInputStream(ZipEntry) line: 355
43+
* java.util.jar.JarFile.getInputStream(ZipEntry) line: 833
44+
* ...
45+
* ]
46+
* }
47+
* }
48+
* <p>
49+
* The difference between {@link Relational} and {@link Contextual} annotations
50+
* is that {@link Relational} ties event data together to form a global data
51+
* structure, similar to a foreign key in a relational database, but
52+
* {@link Contextual} represents a state that applies to all events that happen
53+
* at the same time, in the same thread. A field can be both contextual and
54+
* relational at the same time.
55+
* <p>
56+
* A contextual field may incur overhead on a parser reading a recording file,
57+
* since it must track active context, so it should be used sparingly and only
58+
* where appropriate.
59+
*
60+
* @since 25
61+
*/
62+
@MetadataDefinition
63+
@Label("Context")
64+
@Retention(RetentionPolicy.RUNTIME)
65+
@Target({ElementType.FIELD})
66+
public @interface Contextual {
67+
}

src/jdk.jfr/share/classes/jdk/jfr/events/MethodTraceEvent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package jdk.jfr.events;
2626

2727
import jdk.jfr.Category;
28+
import jdk.jfr.Contextual;
2829
import jdk.jfr.Label;
2930
import jdk.jfr.Name;
3031
import jdk.jfr.StackTrace;
@@ -35,6 +36,7 @@
3536
@StackTrace(true)
3637
public final class MethodTraceEvent extends AbstractJDKEvent {
3738

39+
@Contextual
3840
@Label("Method")
3941
private long method;
4042

src/jdk.jfr/share/classes/jdk/jfr/internal/MetadataRepository.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ private EventConfiguration makeConfiguration(Class<? extends jdk.internal.event.
205205
nativeEventTypes.remove(n);
206206
nativeControls.remove(n);
207207
TypeLibrary.removeType(nativeType.getId());
208-
pEventType.setFields(nativeType.getFields());
208+
PrivateAccess access = PrivateAccess.getInstance();
209+
for (int i = 0; i < nativeFields.size(); i++) {
210+
access.setAnnotations(nativeFields.get(i), eventFields.get(i).getAnnotationElements());
211+
}
212+
pEventType.setFields(nativeFields);
209213
}
210214
}
211215
}

src/jdk.jfr/share/classes/jdk/jfr/internal/tool/PrettyWriter.java

Lines changed: 175 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -25,15 +25,26 @@
2525

2626
package jdk.jfr.internal.tool;
2727

28+
import java.io.IOException;
2829
import java.io.PrintWriter;
30+
import java.nio.file.Path;
2931
import java.time.Duration;
32+
import java.time.Instant;
3033
import java.time.OffsetDateTime;
3134
import java.time.format.DateTimeFormatter;
3235
import java.time.temporal.ChronoUnit;
36+
import java.util.ArrayList;
37+
import java.util.HashMap;
38+
import java.util.LinkedHashSet;
3339
import java.util.List;
40+
import java.util.Map;
41+
import java.util.PriorityQueue;
42+
import java.util.SequencedSet;
3443
import java.util.StringJoiner;
3544

45+
import jdk.jfr.Contextual;
3646
import jdk.jfr.DataAmount;
47+
import jdk.jfr.EventType;
3748
import jdk.jfr.Frequency;
3849
import jdk.jfr.MemoryAddress;
3950
import jdk.jfr.Percentage;
@@ -46,7 +57,7 @@
4657
import jdk.jfr.consumer.RecordedObject;
4758
import jdk.jfr.consumer.RecordedStackTrace;
4859
import jdk.jfr.consumer.RecordedThread;
49-
import jdk.jfr.internal.Type;
60+
import jdk.jfr.consumer.RecordingFile;
5061
import jdk.jfr.internal.util.ValueFormatter;
5162

5263
/**
@@ -55,10 +66,60 @@
5566
* This class is also used by {@link RecordedObject#toString()}
5667
*/
5768
public final class PrettyWriter extends EventPrintWriter {
58-
private static final String TYPE_OLD_OBJECT = Type.TYPES_PREFIX + "OldObject";
69+
private static record Timestamp(RecordedEvent event, long seconds, int nanosCompare, boolean contextual) implements Comparable<Timestamp> {
70+
// If the start timestamp from a contextual event has the same start timestamp
71+
// as an ordinary instant event, the contextual event should be processed first
72+
// One way to ensure this is to multiply the nanos value and add 1 ns to the end
73+
// timestamp so the context event always comes first in a comparison.
74+
// This also prevents a contextual start time to be processed after a contextual
75+
// end time, if the event is instantaneous.
76+
public static Timestamp createStart(RecordedEvent event, boolean contextual) {
77+
Instant time = event.getStartTime(); // Method allocates, so store seconds and nanos
78+
return new Timestamp(event, time.getEpochSecond(), 2 * time.getNano(), contextual);
79+
}
80+
81+
public static Timestamp createEnd(RecordedEvent event, boolean contextual) {
82+
Instant time = event.getEndTime(); // Method allocates, so store seconds and nanos
83+
return new Timestamp(event, time.getEpochSecond(), 2 * time.getNano() + 1, contextual);
84+
}
85+
86+
public boolean start() {
87+
return (nanosCompare & 1L) == 0;
88+
}
89+
90+
@Override
91+
public int compareTo(Timestamp that) {
92+
// This is taken from Instant::compareTo
93+
int cmp = Long.compare(seconds, that.seconds);
94+
if (cmp != 0) {
95+
return cmp;
96+
}
97+
return nanosCompare - that.nanosCompare;
98+
}
99+
}
100+
101+
private static record TypeInformation(Long id, List<ValueDescriptor> contextualFields, boolean contextual, String simpleName) {
102+
}
103+
104+
private static final SequencedSet<RecordedEvent> EMPTY_SET = new LinkedHashSet<>();
105+
private static final String TYPE_OLD_OBJECT = "jdk.types.OldObject";
59106
private static final DateTimeFormatter TIME_FORMAT_EXACT = DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS (yyyy-MM-dd)");
60107
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS (yyyy-MM-dd)");
61108
private static final Long ZERO = 0L;
109+
// Rationale for using one million events in the window.
110+
// Events in JFR arrive in batches. The commit time (end time) of an
111+
// event in batch N typically doesn't come before any events in batch N - 1,
112+
// but it can't be ruled out completely. Data is also partitioned into chunks,
113+
// typically 16 MB each. Within a chunk, there must be at least one batch.
114+
// The size of an event is typically more than 16 bytes, so an
115+
// EVENT_WINDOW_SIZE of 1 000 000 events will likely cover more than one batch.
116+
// Having at least two batches in a window avoids boundary issues.
117+
// At the same time, a too large window, means it will take more time
118+
// before the first event is printed and the tool will feel unresponsive.
119+
private static final int EVENT_WINDOW_SIZE = 1_000_000;
120+
private final PriorityQueue<Timestamp> timeline = new PriorityQueue<>(EVENT_WINDOW_SIZE + 4);
121+
private final Map<Long, TypeInformation> typeInformation = new HashMap<>();
122+
private final Map<Long, SequencedSet<RecordedEvent>> contexts = new HashMap<>();
62123
private final boolean showExact;
63124
private RecordedEvent currentEvent;
64125

@@ -71,19 +132,110 @@ public PrettyWriter(PrintWriter destination) {
71132
this(destination, false);
72133
}
73134

135+
void print(Path source) throws IOException {
136+
printBegin();
137+
int counter = 0;
138+
try (RecordingFile file = new RecordingFile(source)) {
139+
while (file.hasMoreEvents()) {
140+
RecordedEvent event = file.readEvent();
141+
if (typeInformation(event).contextual()) {
142+
timeline.add(Timestamp.createStart(event, true));
143+
timeline.add(Timestamp.createEnd(event, true));
144+
}
145+
if (acceptEvent(event)) {
146+
timeline.add(Timestamp.createEnd(event, false));
147+
}
148+
// There should not be a limit on the size of the recording files that
149+
// the 'jfr' tool can process. To avoid OutOfMemoryError and time complexity
150+
// issues on large recordings, a window size must be set when sorting
151+
// and processing contextual events.
152+
while (timeline.size() > EVENT_WINDOW_SIZE) {
153+
print(timeline.remove());
154+
flush(false);
155+
}
156+
if ((++counter % EVENT_WINDOW_SIZE) == 0) {
157+
contexts.entrySet().removeIf(c -> c.getValue().isEmpty());
158+
}
159+
}
160+
while (!timeline.isEmpty()) {
161+
print(timeline.remove());
162+
}
163+
}
164+
printEnd();
165+
flush(true);
166+
}
167+
168+
private TypeInformation typeInformation(RecordedEvent event) {
169+
long id = event.getEventType().getId();
170+
TypeInformation ti = typeInformation.get(id);
171+
if (ti == null) {
172+
ti = createTypeInformation(event.getEventType());
173+
typeInformation.put(ti.id(), ti);
174+
}
175+
return ti;
176+
}
177+
178+
private TypeInformation createTypeInformation(EventType eventType) {
179+
ArrayList<ValueDescriptor> contextualFields = new ArrayList<>();
180+
for (ValueDescriptor v : eventType.getFields()) {
181+
if (v.getAnnotation(Contextual.class) != null) {
182+
contextualFields.add(v);
183+
}
184+
}
185+
contextualFields.trimToSize();
186+
String name = eventType.getName();
187+
String simpleName = name.substring(name.lastIndexOf(".") + 1);
188+
boolean contextual = contextualFields.size() > 0;
189+
return new TypeInformation(eventType.getId(), contextualFields, contextual, simpleName);
190+
}
191+
192+
private void print(Timestamp t) {
193+
RecordedEvent event = t.event();
194+
RecordedThread rt = event.getThread();
195+
if (rt != null) {
196+
processThreadedTimestamp(rt, t);
197+
} else {
198+
if (!t.contextual()) {
199+
print(event);
200+
}
201+
}
202+
}
203+
204+
public void processThreadedTimestamp(RecordedThread thread, Timestamp t) {
205+
RecordedEvent event = t.event();
206+
var contextEvents = contexts.computeIfAbsent(thread.getId(), k -> new LinkedHashSet<>(1));
207+
if (t.contextual) {
208+
if (t.start()) {
209+
contextEvents.add(event);
210+
} else {
211+
contextEvents.remove(event);
212+
}
213+
return;
214+
}
215+
if (typeInformation(event).contextual()) {
216+
print(event);
217+
} else {
218+
print(event, contextEvents);
219+
}
220+
}
221+
74222
@Override
75223
protected void print(List<RecordedEvent> events) {
76-
for (RecordedEvent e : events) {
77-
print(e);
78-
flush(false);
79-
}
224+
throw new InternalError("Should not reach here!");
80225
}
81226

82227
public void print(RecordedEvent event) {
228+
print(event, EMPTY_SET);
229+
}
230+
231+
public void print(RecordedEvent event, SequencedSet<RecordedEvent> context) {
83232
currentEvent = event;
84233
print(event.getEventType().getName(), " ");
85234
println("{");
86235
indent();
236+
if (!context.isEmpty()) {
237+
printContexts(context);
238+
}
87239
for (ValueDescriptor v : event.getFields()) {
88240
String name = v.getName();
89241
if (!isZeroDuration(event, name) && !isLateField(name)) {
@@ -106,6 +258,22 @@ public void print(RecordedEvent event) {
106258
println();
107259
}
108260

261+
private void printContexts(SequencedSet<RecordedEvent> contextEvents) {
262+
for (RecordedEvent e : contextEvents) {
263+
printContextFields(e);
264+
}
265+
}
266+
267+
private void printContextFields(RecordedEvent contextEvent) {
268+
TypeInformation ti = typeInformation(contextEvent);
269+
for (ValueDescriptor v : ti.contextualFields()) {
270+
printIndent();
271+
String name = "Context: " + ti.simpleName() + "." + v.getName();
272+
print(name, " = ");
273+
printValue(getValue(contextEvent, v), v, "");
274+
}
275+
}
276+
109277
private boolean isZeroDuration(RecordedEvent event, String name) {
110278
return name.equals("duration") && ZERO.equals(event.getValue("duration"));
111279
}

src/jdk.jfr/share/classes/jdk/jfr/snippet-files/Snippets.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import jdk.jfr.AnnotationElement;
2828
import jdk.jfr.BooleanFlag;
29+
import jdk.jfr.Contextual;
2930
import jdk.jfr.ValueDescriptor;
3031
import jdk.jfr.EventFactory;
3132
import jdk.jfr.EventType;
@@ -251,6 +252,39 @@ void printTemperaturesInCelsius(Path file) throws IOException {
251252
}
252253
// @end
253254

255+
// @start region="ContextualTrace"
256+
@Label("Trace")
257+
@Name("com.example.Trace")
258+
class TraceEvent extends Event {
259+
@Label("ID")
260+
@Contextual
261+
String id;
262+
263+
@Label("Name")
264+
@Contextual
265+
String name;
266+
}
267+
// @end
268+
269+
void hello() {
270+
// @start region="ContextualOrder"
271+
@Label("Order")
272+
@Name("com.example.Order")
273+
class OrderEvent extends Event {
274+
@Label("Order ID")
275+
@Contextual
276+
long id;
277+
278+
@Label("Order Date")
279+
@Timestamp(Timestamp.MILLISECONDS_SINCE_EPOCH)
280+
long date;
281+
282+
@Label("Payment Method")
283+
String paymentMethod;
284+
}
285+
// @end
286+
}
287+
254288
// @start region="DataAmountOverview"
255289
@Name("com.example.ImageRender")
256290
@Label("Image Render")

0 commit comments

Comments
 (0)