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
 ------------------------------