1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.web.html;
19
20 import java.io.IOException;
21 import java.io.Writer;
22 import java.text.DecimalFormat;
23 import java.text.DecimalFormatSymbols;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29
30 import net.bull.javamelody.JdbcWrapper;
31 import net.bull.javamelody.internal.common.I18N;
32 import net.bull.javamelody.internal.model.Collector;
33 import net.bull.javamelody.internal.model.CollectorServer;
34 import net.bull.javamelody.internal.model.Counter;
35 import net.bull.javamelody.internal.model.CounterRequest;
36 import net.bull.javamelody.internal.model.CounterRequestRumData;
37 import net.bull.javamelody.internal.model.DatabaseInformations;
38 import net.bull.javamelody.internal.model.Range;
39
40 /**
41  * Rapport html pour le détail d'une requête.
42  * @author Emeric Vernat
43  */

44 class HtmlCounterRequestGraphReport extends HtmlAbstractReport {
45     private static final int MAX_REQUEST_NAME_LENGTH = 5000;
46     private static int uniqueByPageAndGraphSequence;
47     private final Range range;
48     private final DecimalFormat systemErrorFormat = I18N.createPercentFormat();
49     private final DecimalFormat nbExecutionsFormat = I18N.createPercentFormat();
50     private final DecimalFormat integerFormat = I18N.createIntegerFormat();
51     private List<Counter> counters;
52     private Map<String, CounterRequest> requestsById;
53
54     HtmlCounterRequestGraphReport(Range range, Writer writer) {
55         super(writer);
56         assert range != null;
57         this.range = range;
58     }
59
60     @Override
61     void toHtml() {
62         throw new UnsupportedOperationException();
63     }
64
65     void writeRequestGraph(String requestId, String requestName) throws IOException {
66         incrementUniqueByPageAndGraphSequence();
67         // la classe tooltip est configurée dans la css de HtmlReport
68         write("<a class='tooltip replaceImage' href='?part=graph&amp;graph=");
69         write(requestId);
70         write("'");
71         final String id = "id" + uniqueByPageAndGraphSequence;
72         write(" data-img-id='" + id + "'");
73         write(" data-img-src='?graph=");
74         write(requestId);
75         write("&amp;width=100&amp;height=50'>");
76         // avant mouseover on prend une image qui sera mise en cache
77         write("<em><img src='?resource=db.png' id='");
78         write(id);
79         write("' alt='graph'/></em>");
80         if (requestName.length() <= MAX_REQUEST_NAME_LENGTH) {
81             // writeDirectly pour ne pas gérer de traductions si le nom contient '#'
82             writeDirectly(htmlEncodeRequestName(requestId, requestName));
83             write("</a>");
84         } else {
85             // si une requête a trop de caractères, alors cela sature le tableau des requêtes
86             // et le rend peu lisible, donc on tronque cette requête en ajoutant une action "Details".
87             writeDirectly(htmlEncodeRequestName(requestId,
88                     requestName.substring(0, MAX_REQUEST_NAME_LENGTH)));
89             write("</a>");
90             write("<br/> ");
91
92             final String idToShow = "request-" + requestId;
93             writeShowHideLink(idToShow, "#Details#");
94             writeln("<div id='request-" + requestId + "' class='displayNone'>");
95             write("<br/> ");
96             writeDirectly(htmlEncodeRequestName(requestId, requestName));
97             writeln("</div> ");
98         }
99     }
100
101     private static void incrementUniqueByPageAndGraphSequence() {
102         uniqueByPageAndGraphSequence++;
103     }
104
105     private static String htmlEncodeRequestName(String requestId, String requestName) {
106         return HtmlCounterReport.htmlEncodeRequestName(requestId, requestName);
107     }
108
109     void writeRequestAndGraphDetail(Collector collector, CollectorServer collectorServer,
110             String graphName) throws IOException {
111         counters = collector.getRangeCounters(range);
112         requestsById = mapAllRequestsById();
113         final CounterRequest request = requestsById.get(graphName);
114         if (request != null) {
115             if (request.getRumData() != null && request.getRumData().getHits() > 0) {
116                 writeRequestRumData(request);
117             }
118             writeRequest(request);
119
120             if (JdbcWrapper.SINGLETON.getSqlCounter().isRequestIdFromThisCounter(graphName)
121                     && !request.getName().toLowerCase(Locale.ENGLISH).startsWith("alter ")) {
122                 // inutile d'essayer d'avoir le plan d'exécution des requêtes sql
123                 // telles que "alter session set ..." (cf issue 152)
124                 writeSqlRequestExplainPlan(collector, collectorServer, request);
125             }
126         }
127         if (isGraphDisplayed(collector, request)) {
128             writeln("<table summary=''><tr><td>");
129             writeln("<div id='track' class='noPrint'>");
130             writeln("<div class='selected' id='handle'>");
131             writeln("<img src='?resource=scaler_slider.gif' alt=''/>");
132             writeln("</div></div>");
133             writeln("</td><td>");
134             writeDirectly("<div class='noPrint gray'>");
135             writeln("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
136             writeln("<label for='cb'><input id='cb' type='checkbox' />&nbsp;#hide_maximum#</label>");
137             writeln("</div> ");
138             writeln("</td></tr></table>");
139
140             writeln("<div align='center'>");
141             writeln("<table summary=''><tr><td>");
142             final String graphNameEncoded = urlEncode(graphName);
143             writeln("<img class='synthèse' id='img' data-graph-name='"
144                     + htmlEncodeButNotSpace(graphName) + "' src='"
145                     + "?width=960&amp;height=400&amp;graph=" + graphNameEncoded + "' alt='zoom'/>");
146             writeDirectly("<br/><div align='right' class='gray'>");
147             writeln("#graph_units#");
148             writeln("</div><div align='right'>");
149             writeln("<a href='?part=lastValue&amp;graph=" + graphNameEncoded
150                     + "' title='#Lien_derniere_valeur#'>#derniere_valeur#</a>");
151             writeln("&nbsp;&nbsp;&nbsp;<a href='?format=xml&amp;period="
152                     + range.getValue().replace("|""%7C") + "&amp;graph=" + graphNameEncoded
153                     + "' title='Dump XML'>XML</a>");
154             writeln("&nbsp;&nbsp;&nbsp;<a href='?format=txt&amp;period="
155                     + range.getValue().replace("|""%7C") + "&amp;graph=" + graphNameEncoded
156                     + "' title='Dump TXT'>TXT</a>");
157             writeln("</div></td></tr></table>");
158             writeln("</div>");
159         }
160         if (request != null && request.getStackTrace() != null) {
161             writeln("<blockquote><blockquote><b>Stack-trace</b><br/><font size='-1'>");
162             writeStackTrace(request);
163             writeln("</font></blockquote></blockquote>");
164         }
165     }
166
167     private void writeStackTrace(CounterRequest request) throws IOException {
168         for (final String element : request.getStackTrace().split("[\n\r]")) {
169             if (!element.isEmpty()) {
170                 // writeDirectly pour ne pas gérer de traductions car les liens contiennent '#'
171                 writeDirectly(HtmlSourceReport.htmlEncodeStackTraceElementAndTabs(element));
172                 writeDirectly("<br/>\n");
173             }
174         }
175     }
176
177     private boolean isGraphDisplayed(Collector collector, CounterRequest request)
178             throws IOException {
179         return request == null || getCounterByRequestId(request) != null
180                 && HtmlCounterReport.isRequestGraphDisplayed(getCounterByRequestId(request))
181                 // on vérifie aussi que l'instance de jrobin existe pour faire le graph,
182                 // notamment si les statistiques ont été réinitialisées, ce qui vide les instances de jrobin
183                 && collector.getJRobin(request.getId()) != null;
184     }
185
186     private void writeSqlRequestExplainPlan(Collector collector, CollectorServer collectorServer,
187             CounterRequest sqlRequest) throws IOException {
188         try {
189             final String explainPlan;
190             if (collectorServer == null) {
191                 explainPlan = DatabaseInformations.explainPlanFor(sqlRequest.getName());
192             } else {
193                 explainPlan = collectorServer.collectSqlRequestExplainPlan(
194                         collector.getApplication(), sqlRequest.getName());
195             }
196             // rq : si explainPlan était un tableau (ex: mysql),
197             // on pourrait utiliser HtmlDatabaseInformationsReport.TableReport
198             if (explainPlan != null) {
199                 writeln("<b>#Plan_d_execution#</b>");
200                 writeln("<div class='explainPlan'>");
201                 writeDirectly(explainPlan.replace(" ""&nbsp;").replace("\n""<br/>"));
202                 writeln("</div><hr/>");
203             }
204         } catch (final Exception e) {
205             writeln("<b>#Plan_d_execution#</b> ");
206             writeln(e.toString());
207             writeln("<br/>");
208         }
209     }
210
211     void writeRequestUsages(Collector collector, String requestId) throws IOException {
212         assert requestId != null;
213         counters = collector.getRangeCounters(range);
214         CounterRequest myRequest = null;
215         final List<CounterRequest> requests = new ArrayList<>();
216         for (final Counter counter : counters) {
217             for (final CounterRequest request : counter.getOrderedRequests()) {
218                 if (myRequest == null && request.getId().equals(requestId)) {
219                     myRequest = request;
220                 }
221                 if (request.containsChildRequest(requestId)) {
222                     requests.add(request);
223                 }
224             }
225         }
226         writeRequestUsages(myRequest, requests);
227     }
228
229     private void writeRequestUsages(CounterRequest myRequest, List<CounterRequest> requests)
230             throws IOException {
231         writeln("<br/><b>#Utilisations_de#</b>");
232         if (myRequest != null) {
233             writeDirectly(htmlEncodeRequestName(myRequest.getId(), myRequest.getName()));
234         }
235         writeln("<br/><br/>");
236         if (requests.isEmpty()) {
237             writeln("#Aucune_requete#");
238             return;
239         }
240         final boolean someUsagesDisplayed = getUsagesDisplayed(requests);
241         final HtmlTable table = new HtmlTable();
242         table.beginTable(getString("Utilisations_de"));
243         write("<th>#Requete#</th>");
244         if (someUsagesDisplayed) {
245             write("<th class='noPrint'>#Chercher_utilisations#</th>");
246         }
247         for (final CounterRequest request : requests) {
248             table.nextRow();
249             writeUsedRequest(request, someUsagesDisplayed);
250         }
251         table.endTable();
252     }
253
254     private void writeUsedRequest(CounterRequest request, boolean someUsageDisplayed)
255             throws IOException {
256         writeln(" <td>");
257         writeCounterIcon(request);
258         writeRequestGraph(request.getId(), request.getName());
259         if (someUsageDisplayed) {
260             writeln("</td><td align='center' class='noPrint'>");
261             if (doesRequestDisplayUsages(request)) {
262                 writeln("<a href='?part=usages&amp;graph=" + request.getId() + "'>");
263                 writeln("<img src='?resource=find.png' alt='#Chercher_utilisations#' title='#Chercher_utilisations#'/></a>");
264             } else {
265                 writeln("&nbsp;");
266             }
267         }
268         writeln("</td>");
269     }
270
271     private boolean getUsagesDisplayed(List<CounterRequest> requests) {
272         for (final CounterRequest request : requests) {
273             if (doesRequestDisplayUsages(request)) {
274                 return true;
275             }
276         }
277         return false;
278     }
279
280     private void writeRequestRumData(CounterRequest request) throws IOException {
281         final CounterRequestRumData rumData = request.getRumData();
282         final DecimalFormat percentUsFormat = new DecimalFormat("0.00",
283                 DecimalFormatSymbols.getInstance(Locale.US));
284         final DecimalFormat percentLocaleFormat = I18N.createPercentFormat();
285         final int networkTimeMean = rumData.getNetworkTimeMean();
286         final int serverMean = request.getMean();
287         final int domProcessingMean = rumData.getDomProcessingMean();
288         final int pageRenderingMean = rumData.getPageRenderingMean();
289         final int total = networkTimeMean + serverMean + domProcessingMean + pageRenderingMean;
290         final double networkPercent = 100d * networkTimeMean / total;
291         final double serverPercent = 100d * serverMean / total;
292         final double domProcessingPercent = 100d * domProcessingMean / total;
293         final double pageRenderingPercent = 100d * pageRenderingMean / total;
294         writeln("<br/><table class='rumData' summary=''><tr>");
295         writeln("<td class='rumDataNetwork tooltip' data-width-percent='"
296                 + percentUsFormat.format(networkPercent) + "'><em>#Network#: "
297                 + integerFormat.format(networkTimeMean) + " ms ("
298                 + percentLocaleFormat.format(networkPercent) + ")</em>#Network#</td>");
299         writeln("<td class='rumDataServer tooltip' data-width-percent='"
300                 + percentUsFormat.format(serverPercent) + "'><em>#Server#: "
301                 + integerFormat.format(serverMean) + " ms ("
302                 + percentLocaleFormat.format(serverPercent) + "%)</em>#Server#</td>");
303         writeln("<td class='rumDataDomProcessing tooltip' data-width-percent='"
304                 + percentUsFormat.format(domProcessingPercent) + "'><em>#DOM_processing#:"
305                 + integerFormat.format(domProcessingMean) + " ms ("
306                 + percentLocaleFormat.format(domProcessingPercent)
307                 + "%)</em>#DOM_processing#</td>");
308         writeln("<td class='rumDataPageRendering tooltip' data-width-percent='"
309                 + percentUsFormat.format(pageRenderingPercent) + "'><em>#Page_rendering#:"
310                 + integerFormat.format(pageRenderingMean) + " ms ("
311                 + percentLocaleFormat.format(pageRenderingPercent)
312                 + "%)</em>#Page_rendering#</td>");
313         writeln("</tr></table>");
314     }
315
316     private void writeRequest(CounterRequest request) throws IOException {
317         final Map<String, Long> childRequests = request.getChildRequestsExecutionsByRequestId();
318         writeln(" <br/>");
319         final HtmlTable table = new HtmlTable();
320         table.beginTable(getString("Drill_down"));
321         writeln("<th>#Requete#</th>");
322         final boolean hasChildren = !childRequests.isEmpty();
323         if (hasChildren) {
324             writeln("<th class='sorttable_numeric'>#Hits_par_requete#</th>");
325         }
326         writeln("<th class='sorttable_numeric'>#Temps_moyen#</th><th class='sorttable_numeric'>#Temps_max#</th>");
327         writeln("<th class='sorttable_numeric'>#Ecart_type#</th><th class='sorttable_numeric'>#Temps_cpu_moyen#</th>");
328         final boolean allocatedKBytesDisplayed = request.getAllocatedKBytesMean() >= 0;
329         if (allocatedKBytesDisplayed) {
330             writeln("<th class='sorttable_numeric'>#Ko_alloues_moyens#</th>");
331         }
332         writeln("<th class='sorttable_numeric'>#erreur_systeme#</th>");
333         final Counter parentCounter = getCounterByRequestId(request);
334         final boolean allChildHitsDisplayed = parentCounter != null
335                 && parentCounter.getChildCounterName() != null && request.hasChildHits();
336         if (allChildHitsDisplayed) {
337             final String childCounterName = parentCounter.getChildCounterName();
338             writeln("<th class='sorttable_numeric'>"
339                     + getFormattedString("hits_fils_moyens", childCounterName));
340             writeln("</th><th class='sorttable_numeric'>"
341                     + getFormattedString("temps_fils_moyen", childCounterName) + "</th>");
342         }
343         table.nextRow();
344         write("<td class='wrappedText'>");
345         writeCounterIcon(request);
346         writeDirectly(htmlEncodeRequestName(request.getId(), request.getName()));
347         if (hasChildren) {
348             writeln("</td><td>&nbsp;");
349         }
350         writeRequestValues(request, allChildHitsDisplayed, allocatedKBytesDisplayed);
351         writeln("</td> ");
352
353         if (hasChildren) {
354             writeChildRequests(request, childRequests, allChildHitsDisplayed,
355                     allocatedKBytesDisplayed, table);
356         }
357         table.endTable();
358         if (doesRequestDisplayUsages(request)) {
359             writeln("<div align='right' class='noPrint'>");
360             writeln("<a href='?part=usages&amp;graph=" + request.getId() + "'>");
361             writeln("<img src='?resource=find.png' alt='#Chercher_utilisations#' ");
362             writeln("title='#Chercher_utilisations#'/> #Chercher_utilisations#</a></div>");
363         } else {
364             writeln("<br/>");
365         }
366     }
367
368     private boolean doesRequestDisplayUsages(CounterRequest request) {
369         final Counter parentCounter = getCounterByRequestId(request);
370         return parentCounter != null && !parentCounter.isErrorCounter()
371                 && !Counter.HTTP_COUNTER_NAME.equals(parentCounter.getName());
372     }
373
374     private void writeChildRequests(CounterRequest request, Map<String, Long> childRequests,
375             boolean allChildHitsDisplayed, boolean allocatedKBytesDisplayed, HtmlTable table)
376             throws IOException {
377         for (final Map.Entry<String, Long> entry : childRequests.entrySet()) {
378             final CounterRequest childRequest = requestsById.get(entry.getKey());
379             if (childRequest != null) {
380                 table.nextRow();
381                 final Long nbExecutions = entry.getValue();
382                 final float executionsByRequest = (float) nbExecutions / request.getHits();
383                 writeChildRequest(childRequest, executionsByRequest, allChildHitsDisplayed,
384                         allocatedKBytesDisplayed);
385             }
386         }
387     }
388
389     private void writeChildRequest(CounterRequest childRequest, float executionsByRequest,
390             boolean allChildHitsDisplayed, boolean allocatedKBytesDisplayed) throws IOException {
391         writeln("<td>");
392         writeln("<div class='wrappedText childRequest'>");
393         writeCounterIcon(childRequest);
394         writeRequestGraph(childRequest.getId(), childRequest.getName());
395         writeln("</div></td><td align='right'>");
396         write(nbExecutionsFormat.format(executionsByRequest));
397         writeRequestValues(childRequest, allChildHitsDisplayed, allocatedKBytesDisplayed);
398         writeln("</td>");
399     }
400
401     private void writeRequestValues(CounterRequest request, boolean allChildHitsDisplayed,
402             boolean allocatedKBytesDisplayed) throws IOException {
403         final String nextColumn = "</td><td align='right'>";
404         writeln(nextColumn);
405         writeln(integerFormat.format(request.getMean()));
406         writeln(nextColumn);
407         writeln(integerFormat.format(request.getMaximum()));
408         writeln(nextColumn);
409         writeln(integerFormat.format(request.getStandardDeviation()));
410         writeln(nextColumn);
411         final String nbsp = "&nbsp;";
412         if (request.getCpuTimeMean() >= 0) {
413             writeln(integerFormat.format(request.getCpuTimeMean()));
414         } else {
415             writeln(nbsp);
416         }
417         if (allocatedKBytesDisplayed) {
418             writeln(nextColumn);
419             if (request.getAllocatedKBytesMean() >= 0) {
420                 writeln(integerFormat.format(request.getAllocatedKBytesMean()));
421             } else {
422                 writeln(nbsp);
423             }
424         }
425         writeln(nextColumn);
426         writeln(systemErrorFormat.format(request.getSystemErrorPercentage()));
427         if (allChildHitsDisplayed) {
428             writeln(nextColumn);
429             final boolean childHitsDisplayed = request.hasChildHits();
430             if (childHitsDisplayed) {
431                 writeln(integerFormat.format(request.getChildHitsMean()));
432             } else {
433                 writeln(nbsp);
434             }
435             writeln(nextColumn);
436             if (childHitsDisplayed) {
437                 writeln(integerFormat.format(request.getChildDurationsMean()));
438             } else {
439                 writeln(nbsp);
440             }
441         }
442     }
443
444     private void writeCounterIcon(CounterRequest request) throws IOException {
445         final Counter parentCounter = getCounterByRequestId(request);
446         if (parentCounter != null && parentCounter.getIconName() != null) {
447             writeln("<img src='?resource=" + parentCounter.getIconName() + "' alt='"
448                     + parentCounter.getName() + "' width='16' height='16' />&nbsp;");
449         }
450     }
451
452     private Map<String, CounterRequest> mapAllRequestsById() {
453         final Map<String, CounterRequest> result = new HashMap<>();
454         for (final Counter counter : counters) {
455             for (final CounterRequest request : counter.getRequests()) {
456                 result.put(request.getId(), request);
457             }
458         }
459         return result;
460     }
461
462     private Counter getCounterByRequestId(CounterRequest request) {
463         final String requestId = request.getId();
464         for (final Counter counter : counters) {
465             if (counter.isRequestIdFromThisCounter(requestId)) {
466                 return counter;
467             }
468         }
469         return null;
470     }
471 }
472