Skip to content

Commit dffbba0

Browse files
committed
Make tracking breakdown metrics garbage free
1 parent d0e5d51 commit dffbba0

File tree

7 files changed

+178
-79
lines changed

7 files changed

+178
-79
lines changed

apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/ElasticApmContinuousBenchmark.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ public void setUp(Blackhole blackhole) {
145145

146146
@TearDown
147147
public void tearDown() throws ExecutionException, InterruptedException {
148+
Thread.sleep(1000);
148149
tracer.getReporter().flush().get();
149150
server.stop();
150151
System.out.println("Reported: " + tracer.getReporter().getReported());

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

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,25 @@
2424
import co.elastic.apm.agent.impl.sampling.Sampler;
2525
import co.elastic.apm.agent.metrics.Labels;
2626
import co.elastic.apm.agent.metrics.Timer;
27+
import co.elastic.apm.agent.util.KeyListConcurrentHashMap;
2728
import org.slf4j.Logger;
2829
import org.slf4j.LoggerFactory;
2930

3031
import javax.annotation.Nullable;
31-
import java.util.Map;
32-
import java.util.concurrent.ConcurrentHashMap;
33-
import java.util.concurrent.ConcurrentMap;
32+
import java.util.List;
3433

3534
/**
3635
* Data captured by an agent representing an event occurring in a monitored service
3736
*/
3837
public class Transaction extends AbstractSpan<Transaction> {
3938

4039
private static final Logger logger = LoggerFactory.getLogger(Transaction.class);
40+
private static final ThreadLocal<Labels> labelsThreadLocal = new ThreadLocal<>() {
41+
@Override
42+
protected Labels initialValue() {
43+
return new Labels();
44+
}
45+
};
4146

4247
public static final String TYPE_REQUEST = "request";
4348

@@ -48,7 +53,7 @@ public class Transaction extends AbstractSpan<Transaction> {
4853
*/
4954
private final TransactionContext context = new TransactionContext();
5055
private final SpanCount spanCount = new SpanCount();
51-
private final ConcurrentMap<String, Timer> spanTimings = new ConcurrentHashMap<>();
56+
private final KeyListConcurrentHashMap<String, Timer> spanTimings = new KeyListConcurrentHashMap<>();
5257

5358
/**
5459
* The result of the transaction. HTTP status code for HTTP-related transactions.
@@ -175,7 +180,6 @@ public void setUser(String id, String email, String username) {
175180

176181
@Override
177182
public void doEnd(long epochMicros) {
178-
incrementTimer("transaction", getSelfDuration());
179183
if (!isSampled()) {
180184
context.resetState();
181185
}
@@ -191,7 +195,7 @@ public SpanCount getSpanCount() {
191195
return spanCount;
192196
}
193197

194-
public ConcurrentMap<String, Timer> getSpanTimings() {
198+
public KeyListConcurrentHashMap<String, Timer> getSpanTimings() {
195199
return spanTimings;
196200
}
197201

@@ -263,18 +267,20 @@ private void trackMetrics() {
263267
if (type == null) {
264268
return;
265269
}
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+
final Labels labels = labelsThreadLocal.get();
271+
labels.resetState();
272+
labels.transactionName(name).transactionType(type);
273+
final KeyListConcurrentHashMap<String, Timer> spanTimings = getSpanTimings();
274+
List<String> keyList = spanTimings.keyList();
275+
for (int i = 0; i < keyList.size(); i++) {
276+
String spanType = keyList.get(i);
277+
final Timer timer = spanTimings.get(spanType);
270278
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());
279+
tracer.getMetricRegistry().timer("self_time", labels.spanType(spanType)).update(timer.getTotalTimeUs(), timer.getCount());
276280
timer.resetState();
277281
}
278282
}
283+
tracer.getMetricRegistry().timer("self_time", labels.spanType("transaction")).update(getSelfDuration());
284+
tracer.getMetricRegistry().timer("duration", labels.spanType("transaction")).update(getDuration());
279285
}
280286
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public List<CharSequence> getValues() {
142142
}
143143

144144
public boolean isEmpty() {
145-
return keys.isEmpty();
145+
return keys.isEmpty() && transactionName == null && transactionType == null && spanType == null;
146146
}
147147

148148
public int size() {
@@ -162,11 +162,11 @@ public boolean equals(Object o) {
162162
if (this == o) return true;
163163
if (o == null || getClass() != o.getClass()) return false;
164164
Labels labels = (Labels) o;
165-
return keys.equals(labels.keys) &&
166-
isEqual(values, labels.values) &&
167-
contentEquals(transactionName, labels.transactionName) &&
165+
return Objects.equals(spanType, labels.spanType) &&
168166
Objects.equals(transactionType, labels.transactionType) &&
169-
Objects.equals(spanType, labels.spanType);
167+
contentEquals(transactionName, labels.transactionName) &&
168+
keys.equals(labels.keys) &&
169+
isEqual(values, labels.values);
170170
}
171171

172172
@Override
@@ -179,8 +179,8 @@ public int hashCode() {
179179
h = 31 * h + hash(i);
180180
}
181181
h = 31 * h + hash(transactionName);
182-
h = 31 * h + Objects.hashCode(transactionType);
183-
h = 31 * h + Objects.hashCode(spanType);
182+
h = 31 * h + (transactionType != null ? transactionType.hashCode() : 0);
183+
h = 31 * h + (spanType != null ? spanType.hashCode() : 0);
184184
return h;
185185
}
186186

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
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<>();
43+
// low load factor as hash collisions are quite costly when tracking breakdown metrics
44+
private final ConcurrentMap<String, Timer> timers = new ConcurrentHashMap<>(32, 0.5f, Runtime.getRuntime().availableProcessors());
4445
private volatile boolean hasNonEmptyTimer;
4546

4647
public MetricSet(Labels labels) {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ public class Timer implements Recyclable {
3030
private AtomicLong totalTime = new AtomicLong();
3131
private AtomicLong count = new AtomicLong();
3232

33-
public void update(long durationNs) {
34-
update(durationNs, 1);
33+
public void update(long durationUs) {
34+
update(durationUs, 1);
3535
}
3636

37-
public void update(long durationNs, long count) {
38-
this.totalTime.addAndGet(durationNs);
37+
public void update(long durationUs, long count) {
38+
this.totalTime.addAndGet(durationUs);
3939
this.count.addAndGet(count);
4040
}
4141

42-
public long getTotalTimeNs() {
42+
public long getTotalTimeUs() {
4343
return totalTime.get();
4444
}
4545

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package co.elastic.apm.agent.util;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
9+
/**
10+
* A subclass of {@link ConcurrentHashMap} which maintains a {@link List} of all the keys in the map.
11+
* It can be used to iterate over the map's keys without allocating an {@link java.util.Iterator}
12+
*/
13+
public class KeyListConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {
14+
private final List<K> keyList = Collections.synchronizedList(new ArrayList<>());
15+
16+
@Override
17+
public V put(K key, V value) {
18+
final V previousValue = super.put(key, value);
19+
if (previousValue == null) {
20+
keyList.add(key);
21+
}
22+
return previousValue;
23+
}
24+
25+
@Override
26+
public void putAll(Map<? extends K, ? extends V> m) {
27+
for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
28+
put(entry.getKey(), entry.getValue());
29+
}
30+
}
31+
32+
@Override
33+
public V remove(Object key) {
34+
keyList.remove(key);
35+
return super.remove(key);
36+
}
37+
38+
@Override
39+
public void clear() {
40+
keyList.clear();
41+
super.clear();
42+
}
43+
44+
@Override
45+
public V putIfAbsent(K key, V value) {
46+
final V previousValue = super.putIfAbsent(key, value);
47+
if (previousValue == null) {
48+
keyList.add(key);
49+
}
50+
return previousValue;
51+
}
52+
53+
@Override
54+
public boolean remove(Object key, Object value) {
55+
final boolean remove = super.remove(key, value);
56+
if (remove) {
57+
keyList.remove(key);
58+
}
59+
return remove;
60+
}
61+
62+
/**
63+
* Returns a mutable {@link List}, roughly equal to the {@link #keySet()}.
64+
* <p>
65+
* Note that in concurrent scenarios, the key list may be a subset of the values of the respective {@link #keySet()}.
66+
* Entries added via the {@code compute*} family of methods are not reflected in the list.
67+
* </p>
68+
* <p>
69+
* Do not modify this list.
70+
* </p>
71+
*
72+
* @return a {@link List}, roughly equal to the {@link #keySet()}
73+
*/
74+
public List<K> keyList() {
75+
return keyList;
76+
}
77+
}

0 commit comments

Comments
 (0)