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.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.LinkedHashMap;
25 import java.util.List;
26 import java.util.Map;
27
28 import javax.servlet.http.HttpServletRequest;
29
30 import net.bull.javamelody.internal.model.CounterRequest.ICounterRequestContext;
31
32 /**
33  * Contexte d'une requête pour un compteur (non synchronisé).
34  * Le contexte sera initialisé dans un ThreadLocal puis sera utilisé à l'enregistrement de la requête parente.
35  * Par exemple, le contexte d'une requête http a zéro ou plusieurs requêtes sql.
36  * @author Emeric Vernat
37  */

38 public class CounterRequestContext implements ICounterRequestContext, Cloneable, Serializable {
39     private static final long serialVersionUID = 1L;
40     private static final Long ONE = 1L;
41     private static final String SPRING_BEST_MATCHING_PATTERN_ATTRIBUTE = "org.springframework.web.servlet.HandlerMapping.bestMatchingPattern";
42     // attention de ne pas sérialiser le counter d'origine vers le serveur de collecte, le vrai ayant été cloné
43     private Counter parentCounter;
44     private final CounterRequestContext parentContext;
45     private CounterRequestContext currentChildContext;
46     private final String requestName;
47     private final String completeRequestName;
48     private final transient HttpServletRequest httpRequest;
49     private final String remoteUser;
50     private final long threadId;
51     // attention, si sérialisation vers serveur de collecte, la durée peut être impactée s'il y a désynchronisation d'horloge
52     private final long startTime;
53     private final long startCpuTime;
54     private final long startAllocatedBytes;
55     private final String sessionId;
56     // ces 2 champs sont initialisés à 0
57     private int childHits;
58     private int childDurationsSum;
59     @SuppressWarnings("all")
60     private Map<String, Long> childRequestsExecutionsByRequestId;
61
62     // CHECKSTYLE:OFF
63     public CounterRequestContext(Counter parentCounter, CounterRequestContext parentContext,
64             String requestName, String completeRequestName, HttpServletRequest httpRequest,
65             String remoteUser, long startCpuTime, long startAllocatedBytes, String sessionId) {
66         // CHECKSTYLE:ON
67         this(parentCounter, parentContext, requestName, completeRequestName, httpRequest,
68                 remoteUser, Thread.currentThread().getId(), System.currentTimeMillis(),
69                 startCpuTime, startAllocatedBytes, sessionId);
70         if (parentContext != null) {
71             parentContext.setCurrentChildContext(this);
72         }
73     }
74
75     // constructeur privé pour la méthode clone
76     // CHECKSTYLE:OFF
77     private CounterRequestContext(Counter parentCounter, CounterRequestContext parentContext, // NOPMD
78             String requestName, String completeRequestName, HttpServletRequest httpRequest,
79             String remoteUser, long threadId, long startTime, long startCpuTime,
80             long startAllocatedBytes, String sessionId) {
81         // CHECKSTYLE:ON
82         super();
83         assert parentCounter != null;
84         assert requestName != null;
85         assert completeRequestName != null;
86         this.parentCounter = parentCounter;
87         // parentContext est non null si on a ejb dans http
88         // et il est null pour http ou pour ejb sans http
89         this.parentContext = parentContext;
90         this.requestName = requestName;
91         this.completeRequestName = completeRequestName;
92         this.httpRequest = httpRequest;
93         this.remoteUser = remoteUser;
94         this.threadId = threadId;
95         this.startTime = startTime;
96         this.startCpuTime = startCpuTime;
97         this.startAllocatedBytes = startAllocatedBytes;
98         this.sessionId = sessionId;
99     }
100
101     public Counter getParentCounter() {
102         return parentCounter;
103     }
104
105     void setParentCounter(Counter parentCounter) {
106         assert parentCounter != null
107                 && this.parentCounter.getName().equals(parentCounter.getName());
108         this.parentCounter = parentCounter;
109     }
110
111     public static void replaceParentCounters(List<CounterRequestContext> rootCurrentContexts,
112             List<Counter> newParentCounters) {
113         final Map<String, Counter> newParentCountersByName = new HashMap<>(
114                 newParentCounters.size());
115         for (final Counter counter : newParentCounters) {
116             newParentCountersByName.put(counter.getName(), counter);
117         }
118         replaceParentCounters(rootCurrentContexts, newParentCountersByName);
119     }
120
121     private static void replaceParentCounters(List<CounterRequestContext> rootCurrentContexts,
122             Map<String, Counter> newParentCountersByName) {
123         for (final CounterRequestContext context : rootCurrentContexts) {
124             final Counter newParentCounter = newParentCountersByName
125                     .get(context.getParentCounter().getName());
126             if (newParentCounter != null) {
127                 // si le counter n'est pas/plus affiché, newParentCounter peut être null
128                 context.setParentCounter(newParentCounter);
129             }
130             final List<CounterRequestContext> childContexts = context.getChildContexts();
131             if (!childContexts.isEmpty()) {
132                 replaceParentCounters(childContexts, newParentCountersByName);
133             }
134         }
135     }
136
137     public CounterRequestContext getParentContext() {
138         return parentContext;
139     }
140
141     public static String getHttpRequestName(HttpServletRequest httpRequest, String requestName) {
142         if (httpRequest == null) {
143             return requestName;
144         }
145         final String bestMatchingPattern = (String) httpRequest
146                 .getAttribute(SPRING_BEST_MATCHING_PATTERN_ATTRIBUTE);
147         if (bestMatchingPattern != null) {
148             final int indexOfSpace = requestName.indexOf(' ');
149             if (indexOfSpace != -1) {
150                 // ajoute GET ou POST ou POST ajax ou autre après bestMatchingPattern
151                 return bestMatchingPattern + requestName.substring(indexOfSpace);
152             }
153             return bestMatchingPattern;
154         }
155         return requestName;
156     }
157
158     public String getRequestName() {
159         return getHttpRequestName(httpRequest, requestName);
160     }
161
162     public String getCompleteRequestName() {
163         return completeRequestName;
164     }
165
166     public String getRemoteUser() {
167         return remoteUser;
168     }
169
170     public long getThreadId() {
171         return threadId;
172     }
173
174     public int getDuration(long timeOfSnapshot) {
175         // durée écoulée (non négative même si resynchro d'horloge)
176         return (int) Math.max(timeOfSnapshot - startTime, 0);
177     }
178
179     public int getCpuTime() {
180         if (startCpuTime < 0) {
181             return -1;
182         }
183         final int cpuTime = (int) ((ThreadInformations.getThreadCpuTime(getThreadId())
184                 - startCpuTime) / 1000000L);
185         // pas de négatif ici sinon on peut avoir une assertion si elles sont activées
186         return Math.max(cpuTime, 0);
187     }
188
189     public int getAllocatedKBytes() {
190         if (startAllocatedBytes < 0) {
191             return -1;
192         }
193         final int allocatedKBytes = (int) ((ThreadInformations
194                 .getThreadAllocatedBytes(getThreadId()) - startAllocatedBytes) / 1024L);
195         return Math.max(allocatedKBytes, 0);
196     }
197
198     /** {@inheritDoc} */
199     @Override
200     public int getChildHits() {
201         return childHits;
202     }
203
204     /** {@inheritDoc} */
205     @Override
206     public int getChildDurationsSum() {
207         return childDurationsSum;
208     }
209
210     public Map<String, Long> getChildRequestsExecutionsByRequestId() {
211         if (childRequestsExecutionsByRequestId == null) {
212             return Collections.emptyMap();
213         }
214         // pas de nouvelle instance de map ici pour raison de perf
215         // (la méthode est utilisée sur un seul thread)
216         return childRequestsExecutionsByRequestId;
217     }
218
219     public int getTotalChildHits() {
220         // childHits de ce contexte plus tous ceux des contextes fils,
221         // il vaut mieux appeler cette méthode sur un clone du contexte pour avoir un résultat stable
222         // puisque les contextes fils des requêtes en cours peuvent changer à tout moment
223         int result = getChildHits();
224         CounterRequestContext childContext = getCurrentChildContext();
225         while (childContext != null) {
226             result += childContext.getChildHits();
227             childContext = childContext.getCurrentChildContext();
228         }
229         return result;
230     }
231
232     public int getTotalChildDurationsSum() {
233         // childDurationsSum de ce contexte plus tous ceux des contextes fils,
234         // il vaut mieux appeler cette méthode sur un clone du contexte pour avoir un résultat stable
235         // puisque les contextes fils des requêtes en cours peuvent changer à tout moment
236         int result = getChildDurationsSum();
237         CounterRequestContext childContext = getCurrentChildContext();
238         while (childContext != null) {
239             result += childContext.getChildDurationsSum();
240             childContext = childContext.getCurrentChildContext();
241         }
242         return result;
243     }
244
245     public boolean hasChildHits() {
246         return parentCounter.getChildCounterName() != null
247                 && (getTotalChildHits() > 0 || parentCounter.hasChildHits());
248     }
249
250     public List<CounterRequestContext> getChildContexts() {
251         // il vaut mieux appeler cette méthode sur un clone du contexte pour avoir un résultat stable
252         // puisque les contextes fils des requêtes en cours peuvent changer à tout moment
253         final List<CounterRequestContext> childContexts;
254         CounterRequestContext childContext = getCurrentChildContext();
255         if (childContext == null) {
256             childContexts = Collections.emptyList();
257         } else {
258             childContexts = new ArrayList<>(2);
259         }
260         while (childContext != null) {
261             childContexts.add(childContext);
262             childContext = childContext.getCurrentChildContext();
263         }
264         return Collections.unmodifiableList(childContexts);
265     }
266
267     private CounterRequestContext getCurrentChildContext() {
268         return currentChildContext;
269     }
270
271     private void setCurrentChildContext(CounterRequestContext currentChildContext) {
272         this.currentChildContext = currentChildContext;
273     }
274
275     @SuppressWarnings("unused")
276     void addChildRequest(Counter childCounter, String request, String requestId, long duration,
277             boolean systemError, long responseSize) {
278         // si je suis le counter fils du counter du contexte parent
279         // comme sql pour http alors on ajoute la requête fille
280         if (parentContext != null && parentCounter.getName()
281                 .equals(parentContext.getParentCounter().getChildCounterName())) {
282             childHits++;
283             childDurationsSum += (int) duration;
284         }
285
286         // pour drill-down on conserve pour chaque requête mère, les requêtes filles appelées et le
287         // nombre d'exécutions pour chacune
288         if (parentContext == null) {
289             addChildRequestForDrillDown(requestId);
290         } else {
291             parentContext.addChildRequestForDrillDown(requestId);
292         }
293     }
294
295     private void addChildRequestForDrillDown(String requestId) {
296         if (childRequestsExecutionsByRequestId == null) {
297             childRequestsExecutionsByRequestId = new LinkedHashMap<>();
298         }
299         Long nbExecutions = childRequestsExecutionsByRequestId.get(requestId);
300         if (nbExecutions == null) {
301             nbExecutions = ONE;
302         } else {
303             nbExecutions += 1;
304         }
305         childRequestsExecutionsByRequestId.put(requestId, nbExecutions);
306     }
307
308     void closeChildContext() {
309         final CounterRequestContext childContext = getCurrentChildContext();
310         childHits += childContext.getChildHits();
311         childDurationsSum += childContext.getChildDurationsSum();
312         // ce contexte fils est terminé
313         setCurrentChildContext(null);
314     }
315
316     /** {@inheritDoc} */
317     @Override
318     //CHECKSTYLE:OFF
319     public CounterRequestContext clone() { // NOPMD
320         //CHECKSTYLE:ON
321         // ce clone n'est valide que pour un contexte root sans parent
322         assert getParentContext() == null;
323         return clone(null);
324     }
325
326     private CounterRequestContext clone(CounterRequestContext parentContextClone) {
327         final Counter counter = getParentCounter();
328         // s'il fallait un clone du parentCounter pour sérialiser, on pourrait faire seulement ça:
329         //        final Counter parentCounterClone = new Counter(counter.getName(), counter.getStorageName(),
330         //                counter.getIconName(), counter.getChildCounterName(), null);
331         final CounterRequestContext clone = new CounterRequestContext(counter, parentContextClone,
332                 getRequestName(), getCompleteRequestName(), httpRequest, getRemoteUser(),
333                 getThreadId(), startTime, startCpuTime, startAllocatedBytes, sessionId);
334         clone.childHits = getChildHits();
335         clone.childDurationsSum = getChildDurationsSum();
336         final CounterRequestContext childContext = getCurrentChildContext();
337         if (childContext != null) {
338             clone.currentChildContext = childContext.clone(clone);
339         }
340         if (childRequestsExecutionsByRequestId != null) {
341             clone.childRequestsExecutionsByRequestId = new LinkedHashMap<>(
342                     childRequestsExecutionsByRequestId);
343         }
344         return clone;
345     }
346
347     /** {@inheritDoc} */
348     @Override
349     public String toString() {
350         return getClass().getSimpleName() + "[parentCounter=" + getParentCounter().getName()
351                 + ", completeRequestName=" + getCompleteRequestName() + ", threadId="
352                 + getThreadId() + ", startTime=" + startTime + ", childHits=" + getChildHits()
353                 + ", childDurationsSum=" + getChildDurationsSum() + ", childContexts="
354                 + getChildContexts() + ']';
355     }
356 }
357