blob: 8e6ce1f2d8db406c2f61e9bf9a3da1046a08cfc2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.gradle;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.AbstractMap.SimpleEntry;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import jdk.jfr.consumer.RecordedClass;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordedFrame;
import jdk.jfr.consumer.RecordedMethod;
import jdk.jfr.consumer.RecordedStackTrace;
import jdk.jfr.consumer.RecordedThread;
import jdk.jfr.consumer.RecordingFile;
/**
* Processes an array of recording files (from tests), and prints a simple histogram.
* Inspired by the JFR example code.
* Whole stacks are deduplicated (with the default stacksize being 1): you can drill deeper
* by adjusting the parameters.
*/
public class ProfileResults {
/**
* Formats a frame to a formatted line. This is deduplicated on!
*/
static String frameToString(RecordedFrame frame, boolean lineNumbers) {
StringBuilder builder = new StringBuilder();
RecordedMethod method = frame.getMethod();
RecordedClass clazz = method.getType();
if (clazz == null) {
builder.append("<<");
builder.append(frame.getType());
builder.append(">>");
} else {
builder.append(clazz.getName());
}
builder.append("#");
builder.append(method.getName());
builder.append("()");
if (lineNumbers) {
builder.append(":");
if (frame.getLineNumber() == -1) {
builder.append("(" + frame.getType() + " code)");
} else {
builder.append(frame.getLineNumber());
}
}
return builder.toString();
}
/*
* Making the java code wordy to work around gradle's broken property design, ugh!
*/
public static final String MODE_KEY = "tests.profile.mode";
public static final String MODE_DEFAULT = "cpu";
public static final String STACKSIZE_KEY = "tests.profile.stacksize";
public static final String STACKSIZE_DEFAULT = "1";
public static final String COUNT_KEY = "tests.profile.count";
public static final String COUNT_DEFAULT = "10";
public static final String LINENUMBERS_KEY = "tests.profile.linenumbers";
public static final String LINENUMBERS_DEFAULT = "false";
/**
* Driver method, for testing standalone.
* <pre>
* java -Dtests.profile.count=5 buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java \
* ./lucene/core/build/tmp/tests-cwd/somefile.jfr ...
* </pre>
*/
public static void main(String[] args) throws IOException {
printReport(Arrays.asList(args),
System.getProperty(MODE_KEY, MODE_DEFAULT),
Integer.parseInt(System.getProperty(STACKSIZE_KEY, STACKSIZE_DEFAULT)),
Integer.parseInt(System.getProperty(COUNT_KEY, COUNT_DEFAULT)),
Boolean.parseBoolean(System.getProperty(LINENUMBERS_KEY, LINENUMBERS_DEFAULT)));
}
/** true if we care about this event */
static boolean isInteresting(String mode, RecordedEvent event) {
String name = event.getEventType().getName();
switch(mode) {
case "cpu":
return (name.equals("jdk.ExecutionSample") || name.equals("jdk.NativeMethodSample")) &&
!isGradlePollThread(event.getThread("sampledThread"));
case "heap":
return (name.equals("jdk.ObjectAllocationInNewTLAB") || name.equals("jdk.ObjectAllocationOutsideTLAB")) &&
!isGradlePollThread(event.getThread("eventThread"));
default:
throw new UnsupportedOperationException(event.toString());
}
}
/** true if the thread is gradle epoll thread (which we don't care about) */
static boolean isGradlePollThread(RecordedThread thread) {
return (thread != null && thread.getJavaName().startsWith("/127.0.0.1"));
}
/** value we accumulate for this event */
static long getValue(RecordedEvent event) {
switch(event.getEventType().getName()) {
case "jdk.ObjectAllocationInNewTLAB":
return event.getLong("tlabSize");
case "jdk.ObjectAllocationOutsideTLAB":
return event.getLong("allocationSize");
case "jdk.ExecutionSample":
return 1L;
case "jdk.NativeMethodSample":
return 1L;
default:
throw new UnsupportedOperationException(event.toString());
}
}
/** format a value, if its huge, we show millions */
static String formatValue(long value) {
if (value > 1_000_000) {
return String.format("%dM", value / 1_000_000);
} else {
return Long.toString(value);
}
}
/** fixed width used for printing the different columns */
private static final int COLUMN_SIZE = 14;
private static final String COLUMN_PAD = "%-" + COLUMN_SIZE + "s";
private static String pad(String input) {
return String.format(Locale.ROOT, COLUMN_PAD, input);
}
/**
* Process all the JFR files passed in args and print a merged summary.
*/
public static void printReport(List<String> files, String mode, int stacksize, int count, boolean lineNumbers) throws IOException {
if (!"cpu".equals(mode) && !"heap".equals(mode)) {
throw new IllegalArgumentException("tests.profile.mode must be one of (cpu,heap)");
}
if (stacksize < 1) {
throw new IllegalArgumentException("tests.profile.stacksize must be positive");
}
if (count < 1) {
throw new IllegalArgumentException("tests.profile.count must be positive");
}
Map<String, SimpleEntry<String, Long>> histogram = new HashMap<>();
int totalEvents = 0;
long sumValues = 0;
String framePadding = " ".repeat(COLUMN_SIZE * 2);
for (String file : files) {
try (RecordingFile recording = new RecordingFile(Paths.get(file))) {
while (recording.hasMoreEvents()) {
RecordedEvent event = recording.readEvent();
if (!isInteresting(mode, event)) {
continue;
}
RecordedStackTrace trace = event.getStackTrace();
if (trace != null) {
StringBuilder stack = new StringBuilder();
for (int i = 0; i < Math.min(stacksize, trace.getFrames().size()); i++) {
if (stack.length() > 0) {
stack.append("\n")
.append(framePadding)
.append(" at ");
}
stack.append(frameToString(trace.getFrames().get(i), lineNumbers));
}
String line = stack.toString();
SimpleEntry<String,Long> entry = histogram.computeIfAbsent(line, u -> new SimpleEntry<String, Long>(line, 0L));
long value = getValue(event);
entry.setValue(entry.getValue() + value);
totalEvents++;
sumValues += value;
}
}
}
}
// print summary from histogram
System.out.printf(Locale.ROOT, "PROFILE SUMMARY from %d events (total: %s)\n", totalEvents, formatValue(sumValues));
System.out.printf(Locale.ROOT, " tests.profile.mode=%s\n", mode);
System.out.printf(Locale.ROOT, " tests.profile.count=%d\n", count);
System.out.printf(Locale.ROOT, " tests.profile.stacksize=%d\n", stacksize);
System.out.printf(Locale.ROOT, " tests.profile.linenumbers=%b\n", lineNumbers);
System.out.printf(Locale.ROOT, "%s%sSTACK\n", pad("PERCENT"), pad(mode.toUpperCase(Locale.ROOT) + " SAMPLES"));
List<SimpleEntry<String, Long>> entries = new ArrayList<>(histogram.values());
entries.sort((u, v) -> v.getValue().compareTo(u.getValue()));
int seen = 0;
for (SimpleEntry<String, Long> c : entries) {
if (seen++ == count) {
break;
}
String percent = String.format("%2.2f%%", 100 * (c.getValue() / (float) sumValues));
System.out.printf(Locale.ROOT, "%s%s%s\n", pad(percent), pad(formatValue(c.getValue())), c.getKey());
}
}
}