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.model;
19
20 import java.io.Serializable;
21 import java.security.MessageDigest;
22 import java.security.NoSuchAlgorithmException;
23 import java.util.Collections;
24 import java.util.LinkedHashMap;
25 import java.util.Map;
26
27 /**
28  * Données statistiques d'une requête identifiée, hors paramètres dynamiques comme un identifiant,
29  * et sur la période considérée selon le pilotage du {@link Collector} par l'intermédiaire d'un {@link Counter}.
30  *
31  * Les méthodes d'une instance de cette classe ne sont pas thread-safe.
32  * L'état d'une instance doit être accédé ou modifié par l'intermédiaire d'une instance de {@link Counter},
33  * qui gérera les accès concurrents sur les instances de cette classe.
34  * @author Emeric Vernat
35  */

36 public class CounterRequest implements Cloneable, Serializable {
37     private static final long serialVersionUID = -4301825473892026959L;
38     private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
39
40     private final String name;
41     private final String id;
42     // tous ces champs de type long sont initialisés à 0,
43     // il peut être supposé que le type long est suffisant
44     // sans dépassement de capacité (max : 2^63-1 soit un peu moins de 10^19)
45     // et le type long est préféré au type BigInteger pour raison de performances
46     private long hits;
47     private long durationsSum;
48     private long durationsSquareSum;
49     private long maximum;
50     private long cpuTimeSum;
51     private long allocatedKBytesSum;
52     private long systemErrors;
53     private long responseSizesSum;
54     private long childHits;
55     private long childDurationsSum;
56     private String stackTrace;
57     @SuppressWarnings("all")
58     private Map<String, Long> childRequestsExecutionsByRequestId;
59
60     private CounterRequestRumData rumData;
61
62     /**
63      * Interface du contexte d'une requête en cours.
64      */

65     interface ICounterRequestContext {
66         /**
67          * @return Nombre de hits du compteur fils pour ce contexte.
68          */

69         int getChildHits();
70
71         /**
72          * @return Temps total du compteur fils pour ce contexte.
73          */

74         int getChildDurationsSum();
75     }
76
77     /**
78      * Constructeur.
79      * @param name Nom de la requête
80      * @param counterName Nom du counter
81      */

82     public CounterRequest(String name, String counterName) {
83         super();
84         assert name != null;
85         assert counterName != null;
86         this.name = name;
87         this.id = buildId(name, counterName);
88     }
89
90     /**
91      * @return Nom de la requête
92      */

93     public String getName() {
94         return name;
95     }
96
97     /**
98      * @return Identifiant de la requête, construit à partir de son nom et du nom du counter
99      */

100     public String getId() {
101         return id;
102     }
103
104     /**
105      * @return Nombre d'exécution de cette requête
106      */

107     public long getHits() {
108         return hits;
109     }
110
111     /**
112      * @return Number of system errors
113      */

114     public long getSystemErrors() {
115         return systemErrors;
116     }
117
118     /**
119      * @return Somme des temps d'exécution de cette requête
120      */

121     public long getDurationsSum() {
122         return durationsSum;
123     }
124
125     /**
126      * @return Moyenne des temps d'exécution
127      */

128     public int getMean() {
129         if (hits > 0) {
130             return (int) (durationsSum / hits);
131         }
132         return -1;
133     }
134
135     /**
136      * @return écart type (ou sigma, dit "standard deviation" en anglais)
137      */

138     public int getStandardDeviation() {
139         //    soit un ensemble de valeurs Xi
140         //    la moyenne est m = somme(Xi) / n,
141         //    la déviation de chaque valeur par rapport à la moyenne est di = Xi - m
142         //    la variance des valeurs est V = somme(di^2)/(n-1) = somme( (Xi-m)^2 ) / (n-1)
143         //            dont on peut dériver V = (somme (Xi^2) - (somme(Xi)^2) / n) / (n-1)
144         //            (dont une approximation est V = somme (Xi^2) / n - m^2 mais seulement quand n est élevé).
145         //            Cela ne nécessite que de retenir la somme des Xi et la somme des Xi^2
146         //            car on ne souhaite pas conserver toutes les valeurs des Xi pour ne pas saturer la mémoire,
147         //    et l'écart type (ou sigma) est la racine carrée de la variance : s = sqrt(V)
148         //
149         //    on dit alors la moyenne est m +/- s
150
151         // Références :
152         //      http://web.archive.org/web/20070710000323/http://www.med.umkc.edu/tlwbiostats/variability.html
153         //      http://web.archive.org/web/20050512031826/http://helios.bto.ed.ac.uk/bto/statistics/tress3.html
154         //        http://www.bmj.com/collections/statsbk/2.html
155
156         // Some computes the standard deviation differently, for a new value:
157         // first moment:
158         //        final double dev = value - m1;
159         //        final double nDev = dev / n;
160         //        m1 += nDev;
161         // second moment:
162         //        m2 += dev * nDev * (n - 1);
163         // then
164         //        s = sqrt(m2 / (n - 1))
165         // References:
166         //      https://github.com/apache/sirona/blob/trunk/api/src/main/java/org/apache/sirona/counters/OptimizedStatistics.java
167         //      https://en.wikipedia.org/wiki/Central_moment
168
169         if (hits > 0) {
170             return (int) Math
171                     .sqrt((durationsSquareSum - (double) durationsSum * durationsSum / hits)
172                             / (hits - 1));
173         }
174         return -1;
175     }
176
177     /**
178      * @return Maximum des temps d'exécution de cette requête
179      */

180     public long getMaximum() {
181         return maximum;
182     }
183
184     /**
185      * @return Somme temps cpu pour l'exécution de cette requête
186      */

187     public long getCpuTimeSum() {
188         return cpuTimeSum;
189     }
190
191     /**
192      * @return Moyenne des temps cpu pour l'exécution de cette requête
193      */

194     public int getCpuTimeMean() {
195         if (hits > 0) {
196             return (int) (cpuTimeSum / hits);
197         }
198         return -1;
199     }
200
201     /**
202      * @return Moyenne des Ko alloués pour l'exécution de cette requête
203      */

204     public int getAllocatedKBytesMean() {
205         if (hits > 0 && allocatedKBytesSum >= 0) {
206             return (int) (allocatedKBytesSum / hits);
207         }
208         return -1;
209     }
210
211     /**
212      * @return Pourcentage des erreurs systèmes dans l'exécution de cette requête
213      */

214     public float getSystemErrorPercentage() {
215         // pourcentage d'erreurs systèmes entre 0 et 100,
216         // le type de retour est float pour être mesurable
217         // car il est probable que le pourcentage soit inférieur à 1%
218         if (hits > 0) {
219             return Math.min(100f * systemErrors / hits, 100f);
220         }
221         return 0;
222     }
223
224     /**
225      * @return Moyenne des tailles des réponses (http en particulier)
226      */

227     public long getResponseSizeMean() {
228         if (hits > 0) {
229             return responseSizesSum / hits;
230         }
231         return -1L;
232     }
233
234     /**
235      * @return Booléen selon qu'il existe des requêtes filles (sql en particulier)
236      */

237     public boolean hasChildHits() {
238         return childHits > 0;
239     }
240
241     /**
242      * @return Nombre moyen d'exécutions des requêtes filles (sql en particulier)
243      */

244     public int getChildHitsMean() {
245         if (hits > 0) {
246             return (int) (childHits / hits);
247         }
248         return -1;
249     }
250
251     /**
252      * @return Moyenne des temps d'exécutions des requêtes filles (sql en particulier)
253      */

254     public int getChildDurationsMean() {
255         if (hits > 0) {
256             return (int) (childDurationsSum / hits);
257         }
258         return -1;
259     }
260
261     /**
262      * @return Map des nombres d'exécutions par requêtes filles
263      */

264     public Map<String, Long> getChildRequestsExecutionsByRequestId() {
265         if (childRequestsExecutionsByRequestId == null) {
266             return Collections.emptyMap();
267         }
268         synchronized (this) {
269             return new LinkedHashMap<>(childRequestsExecutionsByRequestId);
270         }
271     }
272
273     public boolean containsChildRequest(String requestId) {
274         if (childRequestsExecutionsByRequestId == null) {
275             return false;
276         }
277         synchronized (this) {
278             return childRequestsExecutionsByRequestId.containsKey(requestId);
279         }
280     }
281
282     /**
283      * @return Dernière stack trace
284      */

285     public String getStackTrace() {
286         return stackTrace;
287     }
288
289     public CounterRequestRumData getRumData() {
290         return rumData;
291     }
292
293     void addHit(long duration, int cpuTime, int allocatedKBytes, boolean systemError,
294             String systemErrorStackTrace, long responseSize) {
295         hits++;
296         durationsSum += duration;
297         durationsSquareSum += duration * duration;
298         if (duration > maximum) {
299             maximum = duration;
300         }
301         cpuTimeSum += cpuTime;
302         allocatedKBytesSum += allocatedKBytes;
303         if (systemError) {
304             systemErrors++;
305         }
306         if (systemErrorStackTrace != null) {
307             stackTrace = systemErrorStackTrace;
308         }
309         responseSizesSum += responseSize;
310     }
311
312     void addChildHits(ICounterRequestContext context) {
313         childHits += context.getChildHits();
314         childDurationsSum += context.getChildDurationsSum();
315     }
316
317     void addChildRequests(Map<String, Long> childRequests) {
318         if (childRequests != null && !childRequests.isEmpty()) {
319             if (childRequestsExecutionsByRequestId == null) {
320                 childRequestsExecutionsByRequestId = new LinkedHashMap<>(childRequests);
321             } else {
322                 for (final Map.Entry<String, Long> entry : childRequests.entrySet()) {
323                     final String requestId = entry.getKey();
324                     Long nbExecutions = childRequestsExecutionsByRequestId.get(requestId);
325                     if (nbExecutions == null) {
326                         if (childRequestsExecutionsByRequestId
327                                 .size() >= Counter.MAX_REQUESTS_COUNT) {
328                             // Si le nombre de requêtes est supérieur à 10000 (sql non bindé par ex.),
329                             // on essaye ici d'éviter de saturer la mémoire (et le disque dur)
330                             // avec toutes ces requêtes différentes, donc on ignore cette nouvelle requête.
331                             // (utile pour une agrégation par année dans PeriodCounterFactory par ex., issue #496)
332                             continue;
333                         }
334                         nbExecutions = entry.getValue();
335                     } else {
336                         nbExecutions += entry.getValue();
337                     }
338                     childRequestsExecutionsByRequestId.put(requestId, nbExecutions);
339                 }
340             }
341         }
342     }
343
344     void addHits(CounterRequest request) {
345         assert request != null;
346         if (request.hits != 0) {
347             hits += request.hits;
348             durationsSum += request.durationsSum;
349             durationsSquareSum += request.durationsSquareSum;
350             if (request.maximum > maximum) {
351                 maximum = request.maximum;
352             }
353             cpuTimeSum += request.cpuTimeSum;
354             allocatedKBytesSum += request.allocatedKBytesSum;
355             systemErrors += request.systemErrors;
356             responseSizesSum += request.responseSizesSum;
357             childHits += request.childHits;
358             childDurationsSum += request.childDurationsSum;
359             if (request.stackTrace != null) {
360                 stackTrace = request.stackTrace;
361             }
362             addChildRequests(request.childRequestsExecutionsByRequestId);
363         }
364         if (request.rumData != null) {
365             if (rumData != null) {
366                 rumData.addHits(request.rumData);
367             } else {
368                 rumData = request.rumData.clone();
369             }
370         }
371     }
372
373     void removeHits(CounterRequest request) {
374         assert request != null;
375         if (request.hits != 0) {
376             hits -= request.hits;
377             durationsSum -= request.durationsSum;
378             durationsSquareSum -= request.durationsSquareSum;
379             // on doit enlever le maximum même si on ne connaît pas le précédent maximum car sinon
380             // le maximum des périodes jour, semaine, mois, année est celui de la période tout
381             if (request.maximum >= maximum) {
382                 if (hits > 0) {
383                     maximum = durationsSum / hits;
384                 } else {
385                     maximum = -1;
386                 }
387             }
388             cpuTimeSum -= request.cpuTimeSum;
389             allocatedKBytesSum -= request.allocatedKBytesSum;
390             systemErrors -= request.systemErrors;
391             responseSizesSum -= request.responseSizesSum;
392             childHits -= request.childHits;
393             childDurationsSum -= request.childDurationsSum;
394
395             removeChildHits(request);
396         }
397         if (rumData != null && request.rumData != null) {
398             rumData.removeHits(request.rumData);
399         }
400     }
401
402     private void removeChildHits(CounterRequest request) {
403         if (request.childRequestsExecutionsByRequestId != null
404                 && childRequestsExecutionsByRequestId != null) {
405             for (final Map.Entry<String, Long> entry : request.childRequestsExecutionsByRequestId
406                     .entrySet()) {
407                 final String requestId = entry.getKey();
408                 Long nbExecutions = childRequestsExecutionsByRequestId.get(requestId);
409                 if (nbExecutions != null) {
410                     nbExecutions = Math.max(nbExecutions - entry.getValue(), 0);
411                     if (nbExecutions == 0) {
412                         childRequestsExecutionsByRequestId.remove(requestId);
413                         if (childRequestsExecutionsByRequestId.isEmpty()) {
414                             childRequestsExecutionsByRequestId = null;
415                             break;
416                         }
417                     } else {
418                         childRequestsExecutionsByRequestId.put(requestId, nbExecutions);
419                     }
420                 }
421             }
422         }
423     }
424
425     void addRumHit(long networkTime, long domProcessing, long pageRendering) {
426         if (rumData == null) {
427             rumData = new CounterRequestRumData();
428         }
429         rumData.addHit(networkTime, domProcessing, pageRendering);
430     }
431
432     /** {@inheritDoc} */
433     @Override
434     public CounterRequest clone() { // NOPMD
435         try {
436             final CounterRequest clone = (CounterRequest) super.clone();
437             if (childRequestsExecutionsByRequestId != null) {
438                 // getChildRequestsExecutionsByRequestId fait déjà un clone de la map
439                 clone.childRequestsExecutionsByRequestId = getChildRequestsExecutionsByRequestId();
440             }
441             if (rumData != null) {
442                 clone.rumData = rumData.clone();
443             }
444             return clone;
445         } catch (final CloneNotSupportedException e) {
446             // ne peut arriver puisque CounterRequest implémente Cloneable
447             throw new IllegalStateException(e);
448         }
449     }
450
451     // retourne l'id supposé unique de la requête pour le stockage
452     private static String buildId(String name, String counterName) {
453         final MessageDigest messageDigest = getMessageDigestInstance();
454         messageDigest.update(name.getBytes());
455         final byte[] digest = messageDigest.digest();
456
457         final int l = counterName.length();
458         final char[] chars = new char[l + digest.length * 2];
459         // copie du counterName au début de chars
460         counterName.getChars(0, l, chars, 0);
461         // encodage en chaîne hexadécimale du digest,
462         // puisque les caractères bizarres ne peuvent être utilisés sur un système de fichiers
463         for (int j = 0; j < digest.length; j++) {
464             final int v = digest[j] & 0xFF;
465             chars[j * 2 + l] = HEX_ARRAY[v >>> 4];
466             chars[j * 2 + 1 + l] = HEX_ARRAY[v & 0x0F];
467         }
468         return new String(chars);
469     }
470
471     private static MessageDigest getMessageDigestInstance() {
472         // SHA1 est un algorithme de hashage qui évite les conflits à 2^80 près entre
473         // les identifiants supposés uniques (SHA1 est mieux que MD5 qui est mieux que CRC32).
474         try {
475             return MessageDigest.getInstance("SHA-1");
476         } catch (final NoSuchAlgorithmException e) {
477             // ne peut arriver car SHA1 est un algorithme disponible par défaut dans le JDK Sun
478             throw new IllegalStateException(e);
479         }
480     }
481
482     /** {@inheritDoc} */
483     @Override
484     public String toString() {
485         return getClass().getSimpleName() + "[name=" + getName() + ", hits=" + getHits() + ", id="
486                 + getId() + ']';
487     }
488 }
489