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