Verwenden Sie Java 11? Was mir an dieser Version am besten gefällt, ist, dass die JFR-API geöffnet / standardisiert wurde. Also "[Mikro-Benchmark mit JFR-API](https://qiita.com/koduki/items/6e8fbcbafdcc2f743f56#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0% Benutzerdefiniertes Ereignis wie im Artikel "E3% 82% A4% E3% 83% 99% E3% 83% B3% E3% 83% 88% E3% 81% AE% E4% BD% 9C% E6% 88% 90)" Sie können auch leicht machen.
Hast du übrigens jemals darüber nachgedacht? "JFR ist praktisch, unterscheidet sich jedoch von den Informationen, die Sie anzeigen möchten." "Da es sich um eine Binärdatei handelt, kann sie nicht mit Kibana oder vorhandenen Tools verknüpft werden." "Usw."
Ich habe mich gewundert! Es wäre praktisch, wenn ich den Inhalt von JFR überprüfen könnte, ohne JMC selbst zu verwenden. Also lass es uns tatsächlich tun. Basierend auf der Stichprobe der Methode, die von JFR erhalten werden kann, möchte ich den Ausführungsstatus der Methode in HTML wie APM umwandeln, was üblich ist.
Der diesmal verwendete Quellcode ist unten. https://github.com/koduki/jfr-viewer
Laden wir zunächst den vorhandenen JFR. Hier ist test.jfr
die vorhandene JFR.
Als Einschränkung scheint es, dass JFR, das in Java 9 oder höher aufgezeichnet wurde, zum Lesen mit der JFR-API erforderlich ist.
Es kann nicht geholfen werden, da sich die API geändert hat, aber es ist ein wenig problematisch. Ich muss die Produktionsumgebung so schnell wie möglich auf Java 11 ändern!
Verwenden Sie zum Lesen "RecordingFile # readAllEvents" zum Lesen. Es gibt andere Methoden, aber diese Methode wird empfohlen, da der Rückgabewert List ist und mit der Stream-API einfach zu handhaben ist.
RecordingFile.readAllEvents(path).stream()
Als nächstes filtern wir nach Ereignisnamen usw. Diesmal entspricht .ExecutionSample
dem.
Mit dem JMC-Ereignisbrowser können Sie den Namen leicht herausfinden.
Path path = Paths.get("test.jfr");
RecordingFile.readAllEvents(path).stream()
.filter((e) -> e.getEventType().getName().endsWith(".ExecutionSample"))
Unten erhalten wir den Stack-Trace aus dem ExecutionSample-Ereignis und konvertieren ihn in die folgende Datenstruktur.
+Methodenname 1
-Erfassungszeit 1
-Erfassungszeit 2
+Methodenname 2
-Erfassungszeit 1
-Erfassungszeit 2
- ...
Eigentlich sollte die Aufrufreihenfolge der Methoden korrekt grafisch dargestellt werden, aber dieses Mal habe ich die Reihenfolge der Stapelverfolgung übersprungen und beibehalten, sodass es sich anfühlt, als ob die übergeordnete Methode ganz oben steht. Es mag also so aussehen, als ob es einen Fehler gibt, der es seltsam macht, wenn dieselbe Methode in einer anderen Hierarchie aufgerufen wird, aber es ist eine Spezifikation. Wenn es eine Spezifikation ist, ist es eine Spezifikation!
Sie können auch die Art der Methode ändern, die Sie erhalten möchten, indem Sie mit addMthods
spielen.
Dieses Mal versuche ich, die JFR-API abzuspielen, aber es ist beispielsweise möglich, anhand des Paketnamens zu überprüfen und nur Ihren eigenen Code zu überprüfen.
static Tuple2<LinkedHashMap<String, Set<Instant>>, List<Instant>> readMethod(Path path) throws IOException {
var times = new ArrayList<Instant>();
var methods = new LinkedHashMap<String, Set<Instant>>();
RecordingFile.readAllEvents(path).stream()
.filter((e) -> e.getEventType().getName().endsWith(".ExecutionSample"))
.forEach((event) -> {
RecordedStackTrace stacktrace = event.getStackTrace();
if (stacktrace != null) {
times.add(event.getStartTime());
addMthods(methods, stacktrace, event);
}
});
return $(methods, times);
}
static void addMthods(LinkedHashMap<String, Set<Instant>> methods, RecordedStackTrace stacktrace, RecordedEvent event) {
stacktrace.getFrames().stream()
.filter(x -> x.isJavaFrame())
.filter(x -> !x.getMethod().getType().getName().startsWith("jdk.jfr."))
.collect(Collectors.toCollection(ArrayDeque::new))
.descendingIterator()
.forEachRemaining(x -> {
RecordedMethod method = x.getMethod();
String key = method.getType().getName() + "#" + method.getName();
Set<Instant> span = methods.getOrDefault(key, new HashSet<>());
span.add(event.getStartTime());
methods.put(key, span);
});
}
Nachdem wir JFR analysiert haben, ist es in Ordnung, wenn wir es in HTML konvertieren. Von diesem Zeitpunkt an ist JFR völlig irrelevant geworden, und ich denke, die optimale Lösung besteht darin, ein Tool zu verwenden, das sich gut anfühlt, wie z. B. CSV. Diesmal ist es jedoch schwierig, eine Umgebung zu erstellen. Deshalb habe ich es als Testversion geschrieben.
Konvertieren Sie zunächst die Datenstruktur wie folgt.
- [Methodenname 1, [Startzeit,Endzeit]]
- [Methodenname 2, [Startzeit,Endzeit]]
- [Methodenname 3, [Startzeit,Endzeit]]
- [Methodenname 4, [Startzeit,Endzeit]]
Im Standardprofil zeigt das Methodenbeispiel jedoch nicht die Endzeit der Methode. Es ist nur Zeit, um die Stapelverfolgung abzurufen. Daher wird angenommen, dass "Methoden, die in aufeinanderfolgenden Erfassungszeiten existieren, noch ausgeführt werden". Diese Definition ist nicht immer korrekt, aber ich denke, sie ist ein ziemlich brauchbarer Index für die Stapelverzögerungsanalyse.
Konvertieren Sie mit der folgenden Methode.
static List<Tuple2<String, List<Tuple2<Instant, Instant>>>> parseSpan(LinkedHashMap<String, Set<Instant>> methods, List<Instant> times) {
var span = new ArrayList<Tuple2<String, List<Tuple2<Instant, Instant>>>>();
for (Entry<String, Set<Instant>> m : methods.entrySet()) {
var key = m.getKey();
var value = m.getValue();
var xs = new ArrayList<Tuple2<Instant, Instant>>();
Instant start = null;
Instant last = null;
for (Instant t : times) {
if (value.contains(t)) {
if (start == null) {
start = t;
}
last = t;
} else {
if (last != null) {
xs.add($(start, last));
}
start = null;
last = null;
}
}
span.add($(key, xs));
}
return span;
}
Lassen Sie uns nun den analysierten Wert in HTML auf der Timeline anzeigen. Ich habe versucht, mit vis.js eine Zeitleiste zu erstellen.
static void report(List<Tuple2<String, List<Tuple2<Instant, Instant>>>> spans, Path path) throws IOException {
var names = spans.stream().map(xs -> xs._1()).distinct().collect(Collectors.toList());
var htmlNames = names.stream().map(x -> String.format("\"%s\"", x)).collect(Collectors.toList());
var htmlItems = new ArrayList<String>();
int index = 0;
for (int i = 0; i < spans.size(); i++) {
var s = spans.get(i);
String msg = "{ id: %d, group: %d, content: \"\", start: \"%s\", end: \"%s\" }";
for (int j = 0; j < s._2().size(); j++) {
var x = s._2().get(j);
htmlItems.add(String.format(msg, index++, names.indexOf(s._1()), x._1(), x._2()));
}
}
var html = "<!DOCTYPE html>\n"
+ "<html>\n"
+ " <head>\n"
+ " <title>Timeline</title>\n"
+ "\n"
+ " <style type=\"text/css\">\n"
+ " body,\n"
+ " html {\n"
+ " font-family: sans-serif;\n"
+ " }\n"
+ " </style>\n"
+ "\n"
+ " <script src=\"http://visjs.org/dist/vis.js\"></script>\n"
+ " <link\n"
+ " href=\"http://visjs.org/dist/vis-timeline-graph2d.min.css\"\n"
+ " rel=\"stylesheet\"\n"
+ " type=\"text/css\"\n"
+ " />\n"
+ " </head>\n"
+ " <body>\n"
+ " <p>\n"
+ " A Simple Timeline\n"
+ " </p>\n"
+ "\n"
+ " <div id=\"visualization\"></div>\n"
+ "\n"
+ " <script type=\"text/javascript\">\n"
+ " // DOM element where the Timeline will be attached\n"
+ " var container = document.getElementById(\"visualization\");\n"
+ "\n"
+ " // create a data set with groups\n"
+ " var names = [" + String.join(",", htmlNames) + "];\n"
+ " var groups = new vis.DataSet();\n"
+ " for (var g = 0; g < names.length; g++) {\n"
+ " groups.add({ id: g, content: names[g] });\n"
+ " }\n"
+ "\n"
+ " \n"
+ " // Create a DataSet (allows two way data-binding)\n"
+ " var items = new vis.DataSet([" + String.join(",", htmlItems) + "]);\n"
+ "\n"
+ " // Configuration for the Timeline\n"
+ " function customOrder(a, b) {\n"
+ " // order by id\n"
+ " return a.id - b.id;\n"
+ " }\n"
+ "\n"
+ " // Configuration for the Timeline\n"
+ " var options = {\n"
+ " order: customOrder,\n"
+ " editable: true,\n"
+ " margin: { item: 0 }\n"
+ " };\n"
+ "\n"
+ " // Create a Timeline\n"
+ " var timeline = new vis.Timeline(container);\n"
+ " timeline.setOptions(options);\n"
+ " timeline.setGroups(groups);\n"
+ " timeline.setItems(items);\n"
+ " </script>\n"
+ " </body>\n"
+ "</html>";
Files.writeString(path, html);
}
Führen Sie zum Abschluss den Vorgang wie unten gezeigt aus, um den Vorgang abzuschließen.
public static void main(String[] args) throws IOException {
var methods = readMethod(Paths.get("test.jfr"));
report(parseSpan(methods._1(), methods._2()), Paths.get("target/test.html"));
}
Klicken Sie hier für den generierten HTML-Code. Es fühlt sich so an!
Ich habe einen Zeitleistenbericht mit der JFR-API erstellt. Ich bin der Meinung, dass Sie etwas Nützliches machen können, wenn Sie eine weitere Benutzeroberfläche entwickeln und einen Drilldown durchführen. Alternativ kann es jederzeit möglich sein, mit fluentd oder logstash zu analysieren und Profilerinformationen an Kibana usw. zu senden oder eine benutzerdefinierte API zur Unterstützung von OpenTracing zu entwickeln.
Es hat einen eingebauten Low-Load-Profiler und es ist ein Traum zu sagen, dass Sie frei damit spielen können!
Da ich den Beispielcode ziemlich grob geschrieben habe, habe ich außerdem das Gefühl, ineffiziente Dinge zu tun, aber das interessiert mich nicht. Auch lokale Inferenz ist schließlich praktisch. Es ist eine große Hilfe für Fälle, in denen diesmal Taples verwendet werden.
Dann wird nächstes Jahr Happy Hacking!
Recommended Posts