Skip to content

Commit d0e5d51

Browse files
committed
Add breakdown
1 parent 8bc8dd1 commit d0e5d51

File tree

13 files changed

+618
-19
lines changed

13 files changed

+618
-19
lines changed

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ public void endSpan(Span span) {
374374
if (span.isSampled()) {
375375
long spanFramesMinDurationMs = stacktraceConfiguration.getSpanFramesMinDurationMs();
376376
if (spanFramesMinDurationMs != 0 && span.isSampled()) {
377-
if (span.getDuration() >= spanFramesMinDurationMs) {
377+
if (span.getDurationMs() >= spanFramesMinDurationMs) {
378378
span.withStacktrace(new Throwable());
379379
}
380380
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.concurrent.Callable;
2929
import java.util.concurrent.TimeUnit;
3030
import java.util.concurrent.atomic.AtomicInteger;
31+
import java.util.concurrent.atomic.AtomicLong;
3132

3233
public abstract class AbstractSpan<T extends AbstractSpan> extends TraceContextHolder<T> {
3334
private static final Logger logger = LoggerFactory.getLogger(AbstractSpan.class);
@@ -40,18 +41,70 @@ public abstract class AbstractSpan<T extends AbstractSpan> extends TraceContextH
4041
protected final StringBuilder name = new StringBuilder();
4142
private long timestamp;
4243

43-
/**
44-
* How long the transaction took to complete, in ms with 3 decimal points
45-
* (Required)
46-
*/
47-
protected double duration;
44+
// in microseconds
45+
protected long duration;
46+
protected ReentrantTimer childDurations = new ReentrantTimer();
4847
protected AtomicInteger references = new AtomicInteger();
4948
protected volatile boolean finished = true;
5049

5150
public int getReferenceCount() {
5251
return references.get();
5352
}
5453

54+
public static class ReentrantTimer implements Recyclable {
55+
56+
private AtomicInteger nestingLevel = new AtomicInteger();
57+
private AtomicLong start = new AtomicLong();
58+
private AtomicLong duration = new AtomicLong();
59+
60+
/**
61+
* Starts the timer if it has not been started already.
62+
*
63+
* @param startTimestamp
64+
*/
65+
public void start(long startTimestamp) {
66+
if (nestingLevel.incrementAndGet() == 1) {
67+
start.set(startTimestamp);
68+
}
69+
}
70+
71+
/**
72+
* Stops the timer and increments the duration if no other direct children are still running
73+
* @param endTimestamp
74+
*/
75+
public void stop(long endTimestamp) {
76+
if (nestingLevel.decrementAndGet() == 0) {
77+
incrementDuration(endTimestamp);
78+
}
79+
}
80+
81+
/**
82+
* Stops the timer and increments the duration even if there are direct children which are still running
83+
*
84+
* @param endTimestamp
85+
*/
86+
public void forceStop(long endTimestamp) {
87+
if (nestingLevel.getAndSet(0) != 0) {
88+
incrementDuration(endTimestamp);
89+
}
90+
}
91+
92+
private void incrementDuration(long epochMicros) {
93+
duration.addAndGet(epochMicros - start.get());
94+
}
95+
96+
@Override
97+
public void resetState() {
98+
nestingLevel.set(0);
99+
start.set(0);
100+
duration.set(0);
101+
}
102+
103+
public long getDuration() {
104+
return duration.get();
105+
}
106+
}
107+
55108
public AbstractSpan(ElasticApmTracer tracer) {
56109
super(tracer);
57110
traceContext = TraceContext.with64BitId(this.tracer);
@@ -62,13 +115,20 @@ public boolean isReferenced() {
62115
}
63116

64117
/**
65-
* How long the transaction took to complete, in ms with 3 decimal points
66-
* (Required)
118+
* How long the transaction took to complete, in µs
67119
*/
68-
public double getDuration() {
120+
public long getDuration() {
69121
return duration;
70122
}
71123

124+
public long getSelfDuration() {
125+
return duration - childDurations.getDuration();
126+
}
127+
128+
public double getDurationMs() {
129+
return duration / AbstractSpan.MS_IN_MICROS;
130+
}
131+
72132
/**
73133
* Generic designation of a transaction in the scope of a single service (eg: 'GET /users/:id')
74134
*/
@@ -122,6 +182,7 @@ public void resetState() {
122182
timestamp = 0;
123183
duration = 0;
124184
traceContext.resetState();
185+
childDurations.resetState();
125186
references.set(0);
126187
}
127188

@@ -164,10 +225,11 @@ public void end() {
164225

165226
public final void end(long epochMicros) {
166227
if (!finished) {
167-
this.duration = (epochMicros - timestamp) / AbstractSpan.MS_IN_MICROS;
228+
this.duration = (epochMicros - timestamp);
168229
if (name.length() == 0) {
169230
name.append("unnamed");
170231
}
232+
childDurations.forceStop(epochMicros);
171233
doEnd(epochMicros);
172234
// has to be set last so doEnd callbacks don't think it has already been finished
173235
this.finished = true;
@@ -231,9 +293,11 @@ public void setStartTimestamp(long epochMicros) {
231293

232294
private void onChildStart(Span span, long epochMicros) {
233295
incrementReferences();
296+
childDurations.start(epochMicros);
234297
}
235298

236299
void onChildEnd(Span span, long epochMicros) {
300+
childDurations.stop(epochMicros);
237301
decrementReferences();
238302
}
239303

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Span.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ public void doEnd(long epochMicros) {
190190
if (type == null) {
191191
type = "custom";
192192
}
193+
if (transaction != null) {
194+
transaction.incrementTimer(getType(), getSelfDuration());
195+
}
193196
if (parent != null) {
194197
parent.onChildEnd(this, epochMicros);
195198
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@
2222
import co.elastic.apm.agent.impl.ElasticApmTracer;
2323
import co.elastic.apm.agent.impl.context.TransactionContext;
2424
import co.elastic.apm.agent.impl.sampling.Sampler;
25+
import co.elastic.apm.agent.metrics.Labels;
26+
import co.elastic.apm.agent.metrics.Timer;
2527
import org.slf4j.Logger;
2628
import org.slf4j.LoggerFactory;
2729

2830
import javax.annotation.Nullable;
31+
import java.util.Map;
32+
import java.util.concurrent.ConcurrentHashMap;
33+
import java.util.concurrent.ConcurrentMap;
2934

3035
/**
3136
* Data captured by an agent representing an event occurring in a monitored service
@@ -43,6 +48,7 @@ public class Transaction extends AbstractSpan<Transaction> {
4348
*/
4449
private final TransactionContext context = new TransactionContext();
4550
private final SpanCount spanCount = new SpanCount();
51+
private final ConcurrentMap<String, Timer> spanTimings = new ConcurrentHashMap<>();
4652

4753
/**
4854
* The result of the transaction. HTTP status code for HTTP-related transactions.
@@ -169,20 +175,26 @@ public void setUser(String id, String email, String username) {
169175

170176
@Override
171177
public void doEnd(long epochMicros) {
178+
incrementTimer("transaction", getSelfDuration());
172179
if (!isSampled()) {
173180
context.resetState();
174181
}
175182
if (type == null) {
176183
type = "custom";
177184
}
178185
context.onTransactionEnd();
186+
trackMetrics();
179187
this.tracer.endTransaction(this);
180188
}
181189

182190
public SpanCount getSpanCount() {
183191
return spanCount;
184192
}
185193

194+
public ConcurrentMap<String, Timer> getSpanTimings() {
195+
return spanTimings;
196+
}
197+
186198
@Override
187199
public void resetState() {
188200
super.resetState();
@@ -226,4 +238,43 @@ public void decrementReferences() {
226238
tracer.recycle(this);
227239
}
228240
}
241+
242+
void incrementTimer(@Nullable String type, long duration) {
243+
if (type != null && !finished) {
244+
Timer timer = spanTimings.get(type);
245+
if (timer == null) {
246+
timer = new Timer();
247+
Timer racyTimer = spanTimings.putIfAbsent(type, timer);
248+
if (racyTimer != null) {
249+
timer = racyTimer;
250+
}
251+
}
252+
timer.update(duration);
253+
if (finished) {
254+
// in case end()->trackMetrics() has been called concurrently
255+
// don't leak timers
256+
timer.resetState();
257+
}
258+
}
259+
}
260+
261+
private void trackMetrics() {
262+
final String type = getType();
263+
if (type == null) {
264+
return;
265+
}
266+
final StringBuilder transactionName = getName();
267+
final Labels labels = new Labels();
268+
for (Map.Entry<String, Timer> entry : getSpanTimings().entrySet()) {
269+
final Timer timer = entry.getValue();
270+
if (timer.getCount() > 0) {
271+
labels.resetState();
272+
labels.transactionName(transactionName)
273+
.transactionType(type)
274+
.spanType(entry.getKey());
275+
tracer.getMetricRegistry().timer("self_time", labels).update(timer.getTotalTimeNs(), timer.getCount());
276+
timer.resetState();
277+
}
278+
}
279+
}
229280
}

apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricRegistry.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ public Map<Labels, MetricSet> getMetricSets() {
121121
return metricSets;
122122
}
123123

124+
public Timer timer(String timerName, Labels labels) {
125+
return getOrCreateMetricSet(labels).timer(timerName);
126+
}
127+
124128
private MetricSet getOrCreateMetricSet(Labels labels) {
125129
MetricSet metricSet = metricSets.get(labels);
126130
if (metricSet == null) {

apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricSet.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
public class MetricSet {
4141
private final Labels labels;
4242
private final ConcurrentMap<String, DoubleSupplier> gauges = new ConcurrentHashMap<>();
43+
private final ConcurrentMap<String, Timer> timers = new ConcurrentHashMap<>();
4344
private volatile boolean hasNonEmptyTimer;
4445

4546
public MetricSet(Labels labels) {
@@ -62,6 +63,20 @@ public Map<String, DoubleSupplier> getGauges() {
6263
return gauges;
6364
}
6465

66+
public Timer timer(String timerName) {
67+
hasNonEmptyTimer = true;
68+
Timer timer = timers.get(timerName);
69+
if (timer == null) {
70+
timers.putIfAbsent(timerName, new Timer());
71+
timer = timers.get(timerName);
72+
}
73+
return timer;
74+
}
75+
76+
public Map<String, Timer> getTimers() {
77+
return timers;
78+
}
79+
6580
public boolean hasContent() {
6681
return !gauges.isEmpty() || hasNonEmptyTimer;
6782
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*-
2+
* #%L
3+
* Elastic APM Java agent
4+
* %%
5+
* Copyright (C) 2018 - 2019 Elastic and contributors
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package co.elastic.apm.agent.metrics;
21+
22+
import co.elastic.apm.agent.objectpool.Recyclable;
23+
24+
import java.util.concurrent.TimeUnit;
25+
import java.util.concurrent.atomic.AtomicLong;
26+
27+
public class Timer implements Recyclable {
28+
private static final double MS_IN_MICROS = TimeUnit.MILLISECONDS.toMicros(1);
29+
30+
private AtomicLong totalTime = new AtomicLong();
31+
private AtomicLong count = new AtomicLong();
32+
33+
public void update(long durationNs) {
34+
update(durationNs, 1);
35+
}
36+
37+
public void update(long durationNs, long count) {
38+
this.totalTime.addAndGet(durationNs);
39+
this.count.addAndGet(count);
40+
}
41+
42+
public long getTotalTimeNs() {
43+
return totalTime.get();
44+
}
45+
46+
public double getTotalTimeMs() {
47+
return totalTime.get() / MS_IN_MICROS;
48+
}
49+
50+
public double getAverageMs() {
51+
return totalTime.get() / MS_IN_MICROS / count.get();
52+
}
53+
54+
public long getCount() {
55+
return count.get();
56+
}
57+
58+
public boolean hasContent() {
59+
return count.get() > 0;
60+
}
61+
62+
@Override
63+
public void resetState() {
64+
totalTime.set(0);
65+
count.set(0);
66+
}
67+
}

0 commit comments

Comments
 (0)