Skip to content

Commit 1cbed06

Browse files
authored
remove useless allocation in TraceState.copyOf() (#1508)
* use full header to minimize allocation
1 parent ea984c7 commit 1cbed06

File tree

9 files changed

+68
-63
lines changed

9 files changed

+68
-63
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ endif::[]
2828
2929
[float]
3030
===== Bug fixes
31+
* Fix small memory allocation regression introduced with tracestate header {pull}1508[#1508]
3132
3233
[float]
3334
===== Refactors

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package co.elastic.apm.agent.impl.sampling;
2626

2727
import co.elastic.apm.agent.impl.transaction.Id;
28+
import co.elastic.apm.agent.impl.transaction.TraceState;
2829

2930
/**
3031
* This is a implementation of {@link Sampler} which always returns the same sampling decision.
@@ -36,12 +37,13 @@ public class ConstantSampler implements Sampler {
3637

3738
private final boolean decision;
3839
private final double rate;
39-
private final String rateString;
40+
41+
private final String traceStateHeader;
4042

4143
private ConstantSampler(boolean decision) {
4244
this.decision = decision;
43-
rate = decision ? 1.0d : 0.0d;
44-
rateString = Double.toString(rate);
45+
this.rate = decision ? 1.0d : 0.0d;
46+
this.traceStateHeader = TraceState.getHeaderValue(rate);
4547
}
4648

4749
public static Sampler of(boolean decision) {
@@ -63,7 +65,7 @@ public double getSampleRate() {
6365
}
6466

6567
@Override
66-
public String getSampleRateString() {
67-
return rateString;
68+
public String getTraceStateHeader() {
69+
return traceStateHeader;
6870
}
6971
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ public class ProbabilitySampler implements Sampler {
5656
private final double sampleRate;
5757

5858
// Because header value only contains sampling rate, we can cache it here
59-
private final String rateString;
59+
private final String traceStateHeader;
6060

6161
private ProbabilitySampler(double samplingRate) {
62-
higherBound = (long) (Long.MAX_VALUE * samplingRate);
63-
lowerBound = -higherBound;
62+
this.higherBound = (long) (Long.MAX_VALUE * samplingRate);
63+
this.lowerBound = -higherBound;
6464
this.sampleRate = samplingRate;
65-
rateString = Double.toString(samplingRate);
65+
this.traceStateHeader = TraceState.getHeaderValue(samplingRate);
6666
}
6767

6868
public static Sampler of(double samplingRate) {
@@ -87,7 +87,7 @@ public double getSampleRate() {
8787
}
8888

8989
@Override
90-
public String getSampleRateString() {
91-
return rateString;
90+
public String getTraceStateHeader() {
91+
return traceStateHeader;
9292
}
9393
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,14 @@ public interface Sampler {
5757
*/
5858
double getSampleRate();
5959

60+
6061
/**
61-
* @return current sample rate as constant string
62+
* @return sample rate as (constant) header for context propagation
63+
*
64+
* <p>
65+
* While the {@code tracestate} header is not related to sampler itself, putting this here allows to reuse the same
66+
* {@link String} instance as long as the sample rate does not change to minimize allocation
67+
* </p>
6268
*/
63-
String getSampleRateString();
69+
String getTraceStateHeader();
6470
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ public void asRootSpan(Sampler sampler) {
427427
transactionId.copyFrom(id);
428428
if (sampler.isSampled(traceId)) {
429429
flags = FLAG_RECORDED;
430-
traceState.set(sampler.getSampleRate(), sampler.getSampleRateString());
430+
traceState.set(sampler.getSampleRate(), sampler.getTraceStateHeader());
431431
}
432432
clock.init();
433433
onMutation();

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

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,9 @@ public class TraceState implements Recyclable {
4242

4343
private int sizeLimit;
4444

45-
private final StringBuilder header;
46-
4745
private final StringBuilder rewriteBuffer;
4846

49-
private final List<CharSequence> tracestate;
47+
private final List<String> tracestate;
5048

5149
/**
5250
* sample rate, {@link Double#NaN} if unknown or not set
@@ -58,20 +56,17 @@ public TraceState() {
5856
sizeLimit = DEFAULT_SIZE_LIMIT;
5957
tracestate = new ArrayList<>(1);
6058
rewriteBuffer = new StringBuilder();
61-
header = new StringBuilder(FULL_PREFIX.length());
6259
}
6360

6461
public void copyFrom(TraceState other) {
6562
sampleRate = other.sampleRate;
6663
sizeLimit = other.sizeLimit;
6764
tracestate.clear();
68-
// copy and make sure we have the immutable variant
6965
for (int i = 0; i < other.tracestate.size(); i++) {
70-
tracestate.add(other.tracestate.get(i).toString());
66+
//noinspection UseBulkOperation
67+
tracestate.add(other.tracestate.get(i));
7168
}
7269
rewriteBuffer.setLength(0);
73-
header.setLength(0);
74-
header.append(other.header);
7570
}
7671

7772
public void addTextHeader(String headerValue) {
@@ -125,23 +120,32 @@ public void addTextHeader(String headerValue) {
125120
/**
126121
* Sets value for trace state. Provided rate and string value are assumed to be correct and consistent
127122
*
128-
* @param rate sample rate
129-
* @param rateString rate written as a string, used to minimize allocation
123+
* @param rate sample rate
124+
* @param headerValue header value, as provided by a call to {@link #getHeaderValue(double)}
130125
* @throws IllegalStateException if sample rate has already been set
131126
* @throws IllegalArgumentException if rate has an invalid value
132127
*/
133-
public void set(double rate, String rateString) {
128+
public void set(double rate, String headerValue) {
134129
if (!Double.isNaN(sampleRate)) {
135130
// sample rate is set either explicitly from this method (for root transactions)
136131
// or through upstream header, thus there is no need to change after. This allows to only
137132
// write/rewrite headers once
138133
throw new IllegalStateException("sample rate has already been set from headers");
139134
}
135+
140136
sampleRate = rate;
141-
header.setLength(0);
142-
header.append(FULL_PREFIX);
143-
header.append(rateString);
144-
tracestate.add(header);
137+
tracestate.add(headerValue);
138+
}
139+
140+
/**
141+
* Generates the header value for the provided sample rate. As this method allocates a new string on each
142+
* call, we should minimize the number of calls, ideally once for a given sample rate value.
143+
*
144+
* @param sampleRate sample rate
145+
* @return header value
146+
*/
147+
public static String getHeaderValue(double sampleRate) {
148+
return FULL_PREFIX + sampleRate;
145149
}
146150

147151
/**
@@ -169,7 +173,7 @@ public void resetState() {
169173
}
170174

171175
public void setSizeLimit(int limit) {
172-
if(!tracestate.isEmpty()) {
176+
if (!tracestate.isEmpty()) {
173177
throw new IllegalStateException("can't change size limit once headers have been added");
174178
}
175179
this.sizeLimit = limit;
@@ -189,27 +193,24 @@ private TextTracestateAppender() {
189193
}
190194

191195
@Nullable
192-
public String join(List<? extends CharSequence> tracestate, int tracestateSizeLimit) {
193-
Object singleEntry = tracestate.size() != 1 ? null : tracestate.get(0);
194-
if (singleEntry instanceof String) {
195-
String entry = (String) singleEntry;
196-
if (entry.length() <= tracestateSizeLimit) {
197-
return entry;
198-
}
196+
public String join(List<String> tracestate, int tracestateSizeLimit) {
197+
String singleEntry = tracestate.size() != 1 ? null : tracestate.get(0);
198+
if (singleEntry != null && singleEntry.length() <= tracestateSizeLimit) {
199+
return singleEntry;
199200
}
200201

201202
StringBuilder buffer = getTracestateBuffer();
202203
//noinspection ForLoopReplaceableByForEach
203204
for (int i = 0, size = tracestate.size(); i < size; i++) {
204-
CharSequence value = tracestate.get(i);
205+
String value = tracestate.get(i);
205206
if (value != null) { // ignore null entries to allow removing entries without resizing collection
206207
appendTracestateHeaderValue(value, buffer, tracestateSizeLimit);
207208
}
208209
}
209210
return buffer.length() == 0 ? null : buffer.toString();
210211
}
211212

212-
void appendTracestateHeaderValue(CharSequence headerValue, StringBuilder tracestateBuffer, int tracestateSizeLimit) {
213+
void appendTracestateHeaderValue(String headerValue, StringBuilder tracestateBuffer, int tracestateSizeLimit) {
213214
int requiredLength = headerValue.length();
214215
boolean needsComma = tracestateBuffer.length() > 0;
215216
if (needsComma) {

apm-agent-core/src/test/java/co/elastic/apm/agent/impl/sampling/ProbabilitySamplerTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ void headerCaching(double rate) {
5353
// will indirectly test ConstantSampler as we delegate to it for 0 and 1 values
5454
sampler = ProbabilitySampler.of(rate);
5555

56-
String rateString = sampler.getSampleRateString();
57-
assertThat(rateString).isEqualTo(Double.toString(rate));
56+
String rateHeader = sampler.getTraceStateHeader();
57+
assertThat(rateHeader).isEqualTo(TraceState.getHeaderValue(rate));
5858

59-
assertThat(rateString)
60-
.describedAs("rate as string should return same instance on each call")
61-
.isSameAs(sampler.getSampleRateString());
59+
assertThat(rateHeader)
60+
.describedAs("sample rate header should return same instance on each call")
61+
.isSameAs(sampler.getTraceStateHeader());
6262
}
6363

6464
@Test

apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceContextTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ void testSetSampled() {
494494
assertThat(traceContext.getSampleRate()).isNaN();
495495

496496
// sampled with sample rate
497-
traceContext.getTraceState().set(0.5d, Double.toString(0.5d));
497+
traceContext.getTraceState().set(0.5d, TraceState.getHeaderValue(0.5d));
498498

499499
assertThat(traceContext.isSampled()).isTrue();
500500
assertThat(traceContext.getSampleRate()).isEqualTo(0.5d);
@@ -518,7 +518,7 @@ private TraceContext createRootSpan(double sampleRate){
518518
Sampler sampler = mock(Sampler.class);
519519
when(sampler.isSampled(any(Id.class))).thenReturn(true);
520520
when(sampler.getSampleRate()).thenReturn(sampleRate);
521-
when(sampler.getSampleRateString()).thenReturn(Double.toString(sampleRate));
521+
when(sampler.getTraceStateHeader()).thenReturn(TraceState.getHeaderValue(sampleRate));
522522

523523
traceContext.asRootSpan(sampler);
524524
return traceContext;

apm-agent-core/src/test/java/co/elastic/apm/agent/impl/transaction/TraceStateTest.java

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void createEmpty() {
7575
"17|one=two_three=four,five=six,seven=eight_nine=ten,eleven-twelve|one=two,nine=ten", // just fits
7676
})
7777
void sizeLimit(int limit, String headers, @Nullable String expected) {
78-
Function<String,String> replaceSpaces = s -> s == null ? null : s.replace('X', ' ');
78+
Function<String, String> replaceSpaces = s -> s == null ? null : s.replace('X', ' ');
7979

8080
traceState.setSizeLimit(limit);
8181
for (String h : headers.split("_")) {
@@ -89,9 +89,14 @@ void sizeLimit(int limit, String headers, @Nullable String expected) {
8989

9090
@Test
9191
void addSampleRate() {
92-
traceState.set(0.5d, Double.toString(0.5d));
92+
String headerValue = TraceState.getHeaderValue(0.5d);
93+
assertThat(headerValue).isEqualTo("es=s:0.5");
94+
traceState.set(0.5d, headerValue);
9395
assertThat(traceState.getSampleRate()).isEqualTo(0.5d);
94-
assertThat(traceState.toTextHeader()).isEqualTo("es=s:0.5");
96+
97+
assertThat(traceState.toTextHeader())
98+
.describedAs("should reuse the same string without allocating a new one")
99+
.isSameAs(headerValue);
95100
}
96101

97102
@Test
@@ -123,15 +128,16 @@ void sampleRateAddedToHeaders() {
123128
traceState.addTextHeader("aa=1|2|3");
124129
traceState.addTextHeader("bb=4|5|6");
125130

126-
traceState.set(0.444d, Double.toString(0.444d));
131+
traceState.set(0.444d, TraceState.getHeaderValue(0.444d));
127132
assertThat(traceState.getSampleRate()).isEqualTo(0.444d);
128133
assertThat(traceState.toTextHeader()).isEqualTo("aa=1|2|3,bb=4|5|6,es=s:0.444");
129134
}
130135

131136
@Test
132137
void buildCopy() {
133138
TraceState other = new TraceState();
134-
other.set(0.2d, Double.toString(0.2d));
139+
140+
other.set(0.2d, TraceState.getHeaderValue(0.2d));
135141
other.addTextHeader("aa=1_2");
136142

137143
traceState.copyFrom(other);
@@ -162,7 +168,7 @@ void unknownKeysAreIgnored(String header, String rewrittenHeader) {
162168
"es=s:aa"
163169
})
164170
void invalidValuesIgnored(String header) {
165-
traceState.addTextHeader( header);
171+
traceState.addTextHeader(header);
166172
assertThat(traceState.getSampleRate()).isNaN();
167173
}
168174

@@ -177,15 +183,4 @@ void appliesRoundingOnUpstreamHeader(String headerRate, Double expectedRate) {
177183
assertThat(traceState.toTextHeader()).isEqualTo("es=s:" + expectedRate);
178184
}
179185

180-
@Test
181-
void useProvidedValueForTextHeader() {
182-
double rate = 0.43d;
183-
String headerValue = Double.toString(rate + 0.01d);
184-
traceState.set(rate, headerValue);
185-
assertThat(traceState.getSampleRate()).isEqualTo(rate);
186-
187-
String textHeader = traceState.toTextHeader();
188-
assertThat(textHeader).isEqualTo("es=s:0.44"); // not exactly the same on purpose
189-
}
190-
191186
}

0 commit comments

Comments
 (0)