LUCENE-9193: heap allocations for tests.profile

Can be a bit noisier than cpu sampling, due to how threads are allocated
in tests... maybe we can improve that in the future.
This commit is contained in:
Robert Muir 2020-01-30 08:26:26 -05:00
parent 3a8ed5e8ed
commit 4b5105e167
No known key found for this signature in database
GPG Key ID: 817AE1DD322D7ECA
4 changed files with 129 additions and 37 deletions

View File

@ -73,6 +73,8 @@ public class ProfileResults {
/*
* 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";
@ -89,67 +91,125 @@ public class ProfileResults {
*/
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, int stacksize, int count, boolean lineNumbers) throws IOException {
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, Integer>> histogram = new HashMap<>();
int total = 0;
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();
RecordedThread thread = event.getThread("sampledThread");
// ignore gradle's epoll loop in the worker thread
if (thread != null && thread.getJavaName().startsWith("/127.0.0.1")) {
if (!isInteresting(mode, event)) {
continue;
}
// process java and native method samples
if (event.getEventType().getName().equals("jdk.ExecutionSample") ||
event.getEventType().getName().equals("jdk.NativeMethodSample")) {
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\t\t\t at ");
}
stack.append(frameToString(trace.getFrames().get(i), lineNumbers));
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 ");
}
String line = stack.toString();
SimpleEntry<String,Integer> entry = histogram.computeIfAbsent(line, u -> new SimpleEntry<String, Integer>(line, 0));
entry.setValue(entry.getValue() + 1);
total++;
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 samples\n", total);
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, "PERCENT\tSAMPLES\tSTACK\n", total);
List<SimpleEntry<String, Integer>> entries = new ArrayList<>(histogram.values());
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, Integer> c : entries) {
for (SimpleEntry<String, Long> c : entries) {
if (seen++ == count) {
break;
}
System.out.printf(Locale.ROOT, "%2.2f%%\t%d\t%s\n", 100 * (float) c.getValue() / total, c.getValue(), c.getKey());
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());
}
}
}

View File

@ -38,6 +38,7 @@ if (Boolean.parseBoolean(propertyOrDefault("tests.profile", "false"))) {
gradle.buildFinished {
if (!recordings.isEmpty()) {
ProfileResults.printReport(recordings.getFiles().collect { it.toString() },
propertyOrDefault(ProfileResults.MODE_KEY, ProfileResults.MODE_DEFAULT) as String,
Integer.parseInt(propertyOrDefault(ProfileResults.STACKSIZE_KEY, ProfileResults.STACKSIZE_DEFAULT)),
Integer.parseInt(propertyOrDefault(ProfileResults.COUNT_KEY, ProfileResults.COUNT_DEFAULT)),
Boolean.parseBoolean(propertyOrDefault(ProfileResults.LINENUMBERS_KEY, ProfileResults.LINENUMBERS_DEFAULT)))

View File

@ -19,13 +19,23 @@
Collects only execution and method samples at a low interval
-->
<configuration version="2.0" label="TestProfiling" description="Sampling for unit tests" provider="Apache">
<event name="jdk.ExecutionSample">
<setting name="enabled" control="method-sampling-enabled">true</setting>
<setting name="period" control="method-sampling-java-interval">1 ms</setting>
</event>
<event name="jdk.ExecutionSample">
<setting name="enabled">true</setting>
<setting name="period">1 ms</setting>
</event>
<event name="jdk.NativeMethodSample">
<setting name="enabled" control="method-sampling-enabled">true</setting>
<setting name="period" control="method-sampling-native-interval">1 ms</setting>
</event>
<event name="jdk.NativeMethodSample">
<setting name="enabled">true</setting>
<setting name="period">1 ms</setting>
</event>
<event name="jdk.ObjectAllocationInNewTLAB">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
<event name="jdk.ObjectAllocationOutsideTLAB">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
</configuration>

View File

@ -116,10 +116,31 @@ Profiling slow tests
--------------------
The "tests.profile" mode switch turns on a sampling profiler during test execution,
and prints a simple summary at the end. Example:
and prints a simple summary at the end.
For example, top-10 histogram of methods (cpu samples):
gradlew -p lucene/core test -Ptests.profile=true
Alternatively, you can profile heap allocations instead:
gradlew -p lucene/core test -Ptests.profile=true -Ptests.profile.mode=heap
By default, results are computed (deduplicated) on just the method name, folding
together all events from the same method. To drill down further, you can increase the
stack size from the default of 1, to get a histogram of stacktraces instead:
gradlew -p lucene/core test -Ptests.profile=true -Ptests.profile.stacksize=8
For big methods, it can also be helpful to include line numbers for more granularity:
gradlew -p lucene/core test -Ptests.profile=true -Ptests.profile.linenumbers=true
Using these additional options will make the results more sparse, so it may be useful
to increase the top-N count:
gradlew -p lucene/core test -Ptests.profile=true -Ptests.count=100
Testing against different JVMs
------------------------------