[JAVA] Essayez de faire un rapport chronologique du temps d'exécution d'une méthode à l'aide de l'API JFR

introduction

Utilisez-vous Java 11? Ce que j'aime le plus dans cette version, c'est que l'API JFR a été ouverte / normalisée. Donc, "[Micro benchmark réalisé avec JFR API](https://qiita.com/koduki/items/6e8fbcbafdcc2f743f56#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%" Événement personnalisé comme dans l'article "E3% 82% A4% E3% 83% 99% E3% 83% B3% E3% 83% 88% E3% 81% AE% E4% BD% 9C% E6% 88% 90)" Vous pouvez également faire facilement.

Au fait, y avez-vous déjà pensé? "JFR est pratique mais différent des informations que vous voulez voir" "Comme il s'agit d'un fichier binaire, il ne peut pas être lié à Kibana ou à des outils existants" "etc"

Je me demandais! Ce serait pratique si je pouvais vérifier le contenu de JFR sans utiliser JMC par moi-même. Alors faisons-le. Sur la base de l'échantillonnage de la méthode qui peut être obtenue par JFR, je voudrais rendre l'état d'exécution de la méthode en HTML comme APM qui est courant.

Le code source utilisé cette fois est ci-dessous. https://github.com/koduki/jfr-viewer

Essayez de charger JFR

Commençons par charger le JFR existant. Ici, test.jfr est le JFR existant. En guise de mise en garde, il semble que JFR enregistré en Java 9 ou version ultérieure soit nécessaire pour lire avec l'API JFR. Cela ne peut pas être aidé car l'API a changé, mais c'est un peu gênant. Je dois changer l'environnement de production en Java 11 dès que possible!

Pour revenir en arrière, utilisez RecordingFile # readAllEvents pour lire. Il existe d'autres méthodes, mais cette méthode est recommandée car la valeur de retour est List, elle est donc facile à gérer avec l'API Stream.

RecordingFile.readAllEvents(path).stream()

Ensuite, nous filtrerons par nom d'événement, etc. Cette fois, «.ExecutionSample» lui correspond. Vous pouvez facilement trouver quel est le nom en utilisant le navigateur d'événements de JMC. 001.png

Path path = Paths.get("test.jfr");
RecordingFile.readAllEvents(path).stream()
        .filter((e) -> e.getEventType().getName().endsWith(".ExecutionSample"))

Agréger les méthodes à partir de la trace de pile

Ci-dessous, nous obtenons la trace de la pile de l'événement ExecutionSample et la convertissons dans la structure de données suivante.

+Nom de la méthode 1
      -Temps d'acquisition 1
      -Temps d'acquisition 2
+Nom de la méthode 2
      -Temps d'acquisition 1
      -Temps d'acquisition 2
      - ...
 

En fait, l'ordre d'appel des méthodes devrait être représenté graphiquement correctement, mais cette fois j'ai ignoré et conservé l'ordre de la trace de la pile, donnant l'impression que la méthode parente est en haut. Il peut donc sembler qu'il y ait un bogue qui rend étrange si la même méthode est appelée dans différentes hiérarchies, mais c'est par conception. Si c'est une spécification, c'est une spécification!

Vous pouvez également changer le type de méthode que vous souhaitez obtenir en jouant avec ʻaddMthods`. Cette fois, j'essaie de jouer à l'API JFR, mais par exemple, il peut être possible de vérifier par le nom du package et de vérifier uniquement votre propre code.

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);
            });
}

Convertir en chronologie

Maintenant que nous avons analysé JFR, c'est OK si nous le convertissons en HTML. À partir de ce moment, JFR est devenu complètement hors de propos, et je pense que la solution optimale est d'utiliser un outil qui fait du bien, comme CSV, mais cette fois, c'est pénible de créer un environnement, alors je l'ai écrit comme un essai.

Commencez par convertir la structure de données comme suit.

- [Nom de la méthode 1, [Heure de début,heure de fin]]
- [Nom de la méthode 2, [Heure de début,heure de fin]]
- [Nom de la méthode 3, [Heure de début,heure de fin]]
- [Nom de la méthode 4, [Heure de début,heure de fin]]

Cependant, dans le profil standard, l'exemple de méthode n'affiche pas l'heure de fin de la méthode. Il n'y a que le temps d'obtenir la trace de la pile. Par conséquent, on suppose que "les méthodes qui existent à des moments d'acquisition consécutifs sont toujours en cours d'exécution". Cette définition n'est pas toujours correcte, mais je pense que c'est un index assez utilisable pour l'analyse des délais par lots.

Convertissez avec la méthode suivante.

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

Rapport au HTML

Maintenant, affichons la valeur analysée en HTML sur la chronologie. J'ai essayé d'utiliser vis.js pour en faire une chronologie.

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);
}

Enfin, exécutez à partir du principal comme indiqué ci-dessous pour terminer.

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"));
}

Cliquez ici pour le HTML généré. C'est comme ça! 002.png

Résumé

J'ai créé un rapport chronologique à l'aide de l'API JFR. Je pense que si vous concevez une interface utilisateur plus détaillée et explorez en avant, vous pouvez créer quelque chose d'utile. Alternativement, il peut être possible d'analyser avec fluentd ou logstash à tout moment et d'envoyer des informations de profileur à Kibana etc., ou de concevoir une API personnalisée pour prendre en charge OpenTracing.

Il a un profileur à faible charge intégré, et c'est un rêve de jouer librement!

De plus, depuis que j'ai écrit l'exemple de code assez grossièrement, je sens que je fais des choses inefficaces, mais je m'en fiche de cela. En outre, l'inférence locale est pratique après tout. C'est une grande aide pour les cas qui utilisent des taples comme cette fois.

Alors l'année prochaine sera Happy Hacking!

Recommended Posts

Essayez de faire un rapport chronologique du temps d'exécution d'une méthode à l'aide de l'API JFR
Essayez de faire un simple rappel
Essayez de créer un itérateur qui puisse être vu
Essayez d'émettre ou d'obtenir une carte de Jave à Trello à l'aide de l'API
Définir l'heure de LocalDateTime à une heure spécifique
Méthode d'acquisition de la date et de l'heure à l'aide de l'API DateAndTime
Je veux appeler une méthode d'une autre classe
Initialisation de for Essayez de changer le problème Java en TypeScript 5-4
Essayez de mettre en œuvre à l'aide de l'API de recherche de produits Rakuten (facile)
Essayez de créer un environnement de développement Java à l'aide de Docker
Commande pour essayer d'utiliser Docker pour le moment
Essayez d'utiliser la méthode each_with_index
[Débutant] Essayez de créer un jeu RPG simple avec Java ①
Je souhaite créer un modèle spécifique d'ActiveRecord ReadOnly
[swift5] Essayez de créer un client API avec différentes méthodes
J'ai créé un client RESAS-API en Java
[Rails] Implémentation de la fonction de catégorie multicouche en utilisant l'ascendance "J'ai essayé de créer une fenêtre avec Bootstrap 3"