You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by rm...@apache.org on 2020/01/28 16:28:59 UTC
[lucene-solr] branch master updated: LUCENE-9185: add
"tests.profile" to gradle build to aid fixing slow tests
This is an automated email from the ASF dual-hosted git repository.
rmuir pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git
The following commit(s) were added to refs/heads/master by this push:
new e504798 LUCENE-9185: add "tests.profile" to gradle build to aid fixing slow tests
e504798 is described below
commit e504798a44e5f1577d87ef3a43d9d1e3a859d68a
Author: Robert Muir <rm...@apache.org>
AuthorDate: Tue Jan 28 11:27:18 2020 -0500
LUCENE-9185: add "tests.profile" to gradle build to aid fixing slow tests
Run test(s) with -Ptests.profile=true to print a histogram at the end of
the build.
---
build.gradle | 1 +
.../org/apache/lucene/gradle/ProfileResults.java | 155 +++++++++++++++++++++
gradle/testing/profiling.gradle | 46 ++++++
gradle/testing/profiling.jfc | 31 +++++
help/tests.txt | 8 ++
5 files changed, 241 insertions(+)
diff --git a/build.gradle b/build.gradle
index eb25449..fc5d965 100644
--- a/build.gradle
+++ b/build.gradle
@@ -87,6 +87,7 @@ apply from: file('gradle/maven/maven-local.gradle')
apply from: file('gradle/testing/per-project-summary.gradle')
apply from: file('gradle/testing/slowest-tests-at-end.gradle')
apply from: file('gradle/testing/failed-tests-at-end.gradle')
+apply from: file('gradle/testing/profiling.gradle')
apply from: file('gradle/help.gradle')
// Ant-compatibility layer. ALL OF THESE SHOULD BE GONE at some point. They are
diff --git a/buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java b/buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java
new file mode 100644
index 0000000..9b9d66b
--- /dev/null
+++ b/buildSrc/src/main/java/org/apache/lucene/gradle/ProfileResults.java
@@ -0,0 +1,155 @@
+/*
+ * 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 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),
+ Integer.parseInt(System.getProperty(STACKSIZE_KEY, STACKSIZE_DEFAULT)),
+ Integer.parseInt(System.getProperty(COUNT_KEY, COUNT_DEFAULT)),
+ Boolean.parseBoolean(System.getProperty(LINENUMBERS_KEY, LINENUMBERS_DEFAULT)));
+ }
+
+ /**
+ * 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 {
+ 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;
+ 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")) {
+ 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));
+ }
+ String line = stack.toString();
+ SimpleEntry<String,Integer> entry = histogram.computeIfAbsent(line, u -> new SimpleEntry<String, Integer>(line, 0));
+ entry.setValue(entry.getValue() + 1);
+ total++;
+ }
+ }
+ }
+ }
+ }
+ // print summary from histogram
+ System.out.printf(Locale.ROOT, "PROFILE SUMMARY from %d samples\n", total);
+ 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());
+ entries.sort((u, v) -> v.getValue().compareTo(u.getValue()));
+ int seen = 0;
+ for (SimpleEntry<String, Integer> 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());
+ }
+ }
+}
diff --git a/gradle/testing/profiling.gradle b/gradle/testing/profiling.gradle
new file mode 100644
index 0000000..a6eb7ed
--- /dev/null
+++ b/gradle/testing/profiling.gradle
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+import org.apache.lucene.gradle.ProfileResults;
+
+def recordings = files()
+
+if (Boolean.parseBoolean(propertyOrDefault("tests.profile", "false"))) {
+ allprojects {
+ tasks.withType(Test) {
+ jvmArgs("-XX:StartFlightRecording=dumponexit=true,maxsize=250M,settings=" + rootProject.file("gradle/testing/profiling.jfc"),
+ "-XX:+UnlockDiagnosticVMOptions",
+ "-XX:+DebugNonSafepoints")
+ // delete any previous profile results
+ doFirst {
+ project.delete fileTree(dir: workingDir, include: '*.jfr')
+ }
+ doLast {
+ recordings = recordings.plus fileTree(dir: workingDir, include: '*.jfr')
+ }
+ }
+ }
+
+ gradle.buildFinished {
+ if (!recordings.isEmpty()) {
+ ProfileResults.printReport(recordings.getFiles().collect { it.toString() },
+ 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)))
+ }
+ }
+}
diff --git a/gradle/testing/profiling.jfc b/gradle/testing/profiling.jfc
new file mode 100644
index 0000000..106b495
--- /dev/null
+++ b/gradle/testing/profiling.jfc
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<!--
+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.NativeMethodSample">
+ <setting name="enabled" control="method-sampling-enabled">true</setting>
+ <setting name="period" control="method-sampling-native-interval">1 ms</setting>
+ </event>
+</configuration>
diff --git a/help/tests.txt b/help/tests.txt
index 1d1de07..7799b1d 100644
--- a/help/tests.txt
+++ b/help/tests.txt
@@ -112,6 +112,14 @@ specifying the project and test task or a fully qualified task path. Example:
gradlew -p lucene/core test -Ptests.verbose=true --tests "TestDemo"
+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:
+
+gradlew -p lucene/core test -Ptests.profile=true
+
Testing against different JVMs
------------------------------