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
2525
2626package jdk .jfr .internal .tool ;
2727
28+ import java .io .IOException ;
2829import java .io .PrintWriter ;
30+ import java .nio .file .Path ;
2931import java .time .Duration ;
32+ import java .time .Instant ;
3033import java .time .OffsetDateTime ;
3134import java .time .format .DateTimeFormatter ;
3235import java .time .temporal .ChronoUnit ;
36+ import java .util .ArrayList ;
37+ import java .util .HashMap ;
38+ import java .util .LinkedHashSet ;
3339import java .util .List ;
40+ import java .util .Map ;
41+ import java .util .PriorityQueue ;
42+ import java .util .SequencedSet ;
3443import java .util .StringJoiner ;
3544
45+ import jdk .jfr .Contextual ;
3646import jdk .jfr .DataAmount ;
47+ import jdk .jfr .EventType ;
3748import jdk .jfr .Frequency ;
3849import jdk .jfr .MemoryAddress ;
3950import jdk .jfr .Percentage ;
4657import jdk .jfr .consumer .RecordedObject ;
4758import jdk .jfr .consumer .RecordedStackTrace ;
4859import jdk .jfr .consumer .RecordedThread ;
49- import jdk .jfr .internal . Type ;
60+ import jdk .jfr .consumer . RecordingFile ;
5061import jdk .jfr .internal .util .ValueFormatter ;
5162
5263/**
5566 * This class is also used by {@link RecordedObject#toString()}
5667 */
5768public 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 }
0 commit comments