40
40
import java .util .Arrays ;
41
41
import java .util .Objects ;
42
42
import java .util .Optional ;
43
- import java .util .Properties ;
43
+ import java .util .stream . Stream ;
44
44
45
45
import org .openjdk .jmh .profile .AsyncProfiler ;
46
46
import org .openjdk .jmh .results .format .ResultFormatType ;
49
49
import org .slf4j .Logger ;
50
50
import org .slf4j .LoggerFactory ;
51
51
52
- public abstract class AbstractMain <C extends AbstractConfiguration > {
52
+ import one .convert .Arguments ;
53
+ import one .profiler .AsyncProfilerLoader ;
53
54
54
- private static final Logger STATIC_LOGGER = LoggerFactory . getLogger ( AbstractMain . class );
55
+ public abstract class AbstractMain < C extends AbstractConfiguration > {
55
56
56
57
protected final Logger LOGGER = LoggerFactory .getLogger (getClass ());
58
+
57
59
private final String subpackage ;
58
60
protected final Path resultsDirectory ;
59
61
@@ -81,21 +83,11 @@ protected static String getTimestamp() {
81
83
return year + month + day + "_" + hour + minute + second ;
82
84
}
83
85
84
- protected static Optional <Path > getAsyncProfilerPath () {
86
+ protected Optional <Path > getAsyncProfilerPath () {
85
87
try {
86
- var properties = new Properties ();
87
- properties .load (AbstractMain .class .getResourceAsStream ("/buildtime.properties" ));
88
- var asyncProfilerPath = Path .of (
89
- properties .getProperty ("async.profiler.path" ).trim (),
90
- "lib" ,
91
- "libasyncProfiler.so" )
92
- .toAbsolutePath ();
93
- if (!asyncProfilerPath .toFile ().exists ()) {
94
- return Optional .empty ();
95
- }
96
- return Optional .of (asyncProfilerPath );
97
- } catch (IOException e ) {
98
- STATIC_LOGGER .error ("Failed reading buildtime.properties from the benchmarks JAR." , e );
88
+ return Optional .of (AsyncProfilerLoader .getAsyncProfilerPath ());
89
+ } catch (Exception e ) {
90
+ LOGGER .error ("Failed loading AsyncProfiler." , e );
99
91
return Optional .empty ();
100
92
}
101
93
}
@@ -115,24 +107,34 @@ protected ChainedOptionsBuilder initAsyncProfiler(ChainedOptionsBuilder options)
115
107
});
116
108
}
117
109
118
- protected void convertJfrToFlameGraphs () {
110
+ protected void visualizeJfr () {
119
111
if (getAsyncProfilerPath ().isPresent ()) {
120
112
var combinedJfr = resultsDirectory .resolve ("combined.jfr" );
121
- var jfrAssembleLog = resultsDirectory .resolve ("jfr-assemble.log" );
113
+ // JFR will create the combined file before it finishes combining all of the inputs.
114
+ // Because the command looks at the entire directory, this will make the combined file part of the inputs.
115
+ // And this will cause an unkillable JFR process on MacOS.
116
+ // Therefore we combine the JFR files to a file outside the directory,
117
+ // and only when fully combined we move it over to the results folder.
118
+ var tmpCombinedJfr = resultsDirectory .getParent ().resolve ("combined.jfr" );
122
119
try {
123
120
// Merge all the JFR files that were generated by JMH and copied over by our custom JMH runner.
124
121
var process = new ProcessBuilder ()
125
122
.command ("jfr" , "assemble" , resultsDirectory .toAbsolutePath ().toString (),
126
- combinedJfr .toAbsolutePath ().toString ())
127
- .redirectOutput ( jfrAssembleLog . toFile () )
123
+ tmpCombinedJfr .toAbsolutePath ().toString ())
124
+ .inheritIO ( )
128
125
.start ();
129
126
if (process .waitFor () != 0 ) {
130
- LOGGER .error ("Failed combining JFR files. See '{}' for details." , jfrAssembleLog );
127
+ LOGGER .error ("Failed combining JFR files." );
131
128
return ;
132
129
}
133
- // From the combined JMH file, create flame graphs.
134
- generateFlameGraphsFromJfr (combinedJfr , null );
135
- generateFlameGraphsFromJfr (combinedJfr , "alloc" );
130
+ Files .move (tmpCombinedJfr , combinedJfr );
131
+ LOGGER .error ("Combined JFR files to {}." , combinedJfr );
132
+ // From the combined JMH file, create visualizations.
133
+ for (var visualizationType : VisualizationType .values ()) {
134
+ for (var dataType : DataType .values ()) {
135
+ visualizeJfr (combinedJfr , visualizationType , dataType );
136
+ }
137
+ }
136
138
} catch (Exception e ) {
137
139
LOGGER .error ("Failed converting JFR to flame graphs." , e );
138
140
}
@@ -141,25 +143,31 @@ protected void convertJfrToFlameGraphs() {
141
143
}
142
144
}
143
145
144
- private static void generateFlameGraphsFromJfr (Path jfrFilePath , String type ) {
145
- var args = type == null ? new String [] {
146
- "--simple" ,
147
- jfrFilePath .toString (),
148
- Path .of (jfrFilePath .toAbsolutePath ().getParent ().toString (), "cpu.html" ).toString ()
149
- }
150
- : new String [] {
151
- "--simple" ,
152
- "--" + type ,
153
- jfrFilePath .toString (),
154
- Path .of (jfrFilePath .toAbsolutePath ().getParent ().toString (), type + ".html" ).toString ()
155
- };
156
- try { // Converter is stupidly in the default package.
157
- var fooClass = Class .forName ("jfr2flame" );
158
- var fooMethod = fooClass .getMethod ("main" , String [].class );
159
- fooMethod .invoke (null , (Object ) args );
160
- STATIC_LOGGER .info ("Generating flame graph succeeded: {}." , Arrays .toString (args ));
146
+ private void visualizeJfr (Path jfrFilePath , VisualizationType visualizationType , DataType dataType ) {
147
+ var inputPath = jfrFilePath .toAbsolutePath ();
148
+ var filename = String .format ("%s-%s.html" , dataType .name , visualizationType .name );
149
+ var output = Path .of (inputPath .getParent ().toString (), filename );
150
+ var argStream = Stream .of (
151
+ "--simple" , // Shorter names.
152
+ // "--norm" removes random strings from lambdas;
153
+ // allows to merge different frames which are only different
154
+ // because they use a different instance of the same lambda.
155
+ "--norm" ,
156
+ // "--skip 15" removes bottom frames which come from JMH; they are unnecessary clutter.
157
+ "--skip" , "15" ,
158
+ "--" + dataType .name );
159
+ var args = argStream .toArray (String []::new );
160
+ try {
161
+ if (visualizationType == VisualizationType .FLAME_GRAPH ) {
162
+ one .convert .JfrToFlame .convert (inputPath .toString (), output .toString (), new Arguments (args ));
163
+ } else if (visualizationType == VisualizationType .HEAT_MAP ) {
164
+ one .convert .JfrToHeatmap .convert (inputPath .toString (), output .toString (), new Arguments (args ));
165
+ } else {
166
+ throw new IllegalArgumentException ("Unsupported visualization: " + visualizationType );
167
+ }
168
+ LOGGER .info ("{} Generation succeeded: {}." , visualizationType , Arrays .toString (args ));
161
169
} catch (Exception ex ) {
162
- STATIC_LOGGER .error ("Generating flame graph failed: {}." , Arrays .toString (args ), ex );
170
+ LOGGER .error ("{} Generation failed: {}." , visualizationType , Arrays .toString (args ), ex );
163
171
}
164
172
}
165
173
@@ -193,4 +201,30 @@ public ChainedOptionsBuilder getBaseJmhConfig(C configuration) {
193
201
.shouldDoGC (true );
194
202
}
195
203
204
+ private enum VisualizationType {
205
+
206
+ FLAME_GRAPH ("flamegraph" ),
207
+ HEAT_MAP ("heatmap" );
208
+
209
+ private final String name ;
210
+
211
+ VisualizationType (String name ) {
212
+ this .name = Objects .requireNonNull (name );
213
+ }
214
+
215
+ }
216
+
217
+ private enum DataType {
218
+
219
+ CPU ("cpu" ),
220
+ MEM ("alloc" );
221
+
222
+ private final String name ;
223
+
224
+ DataType (String name ) {
225
+ this .name = Objects .requireNonNull (name );
226
+ }
227
+
228
+ }
229
+
196
230
}
0 commit comments