Skip to content

Commit 4d0bac2

Browse files
dougqhbric3devflow.devflow-routing-intake
authored
Reduce allocation overhead of ExceptionLogger / LogCollector (#10684)
Reducing overhead of getting ExcetionLogger Unlike most loggers, ExceptionLogger can be looked up repeatedly by the exception handling code woven into instrumentation. If an instrumentation constantly creates an exception, that could cause the Logger construction to become which results in creating SLCompatHelper constantly which in turn creates String-s and byte[]-s while determined the looger level. To avoid that problem, the LoggerHelper is cached for ExceptionLogger. We should also consider changing how the Logger is acuired, but that will be addressed in a separate PR. Reducing allocation overhead of LogCollector LogCollector creates RawLogMessage objects that act as keys for de-duplication. If instrumentation has a hot exception, RawLogMessage construction can become a bottleneck. While this change doesn't eliminate RawLogMessage, it does reduce the cost of hashCode & equals which are used by the Map to de-duplicate. hashCode is improved by using the new HashingUtils.hash which avoids the array creation needed to call the var-arg method Objects.hash. equals is improved by special casing, this.throwable == that.throwable. While this case seems unlikely, it occurs when the JVM's hot throw optimization kicks in for NPEs. equals is further improved by caching the result of getStackTrace. Because getStackTrace returns an array, it makes a defensive copy on each call. By caching getStackTrace, we avoid making a copy each time equals is called. Clean-up Making cachedStackTrace private For clarity, returning local stackTrace rather than cachedStackTrace Clarifying comments Changed hot exception to fast throw, since that in comments, since that is the name used by the JVM Merge branch 'master' into dougqh/exception-logger-quick-fixes Fix broken import Introduced a broken import while reincorporating stand-alone reproduction into code base Sizing list appropriately Merge branch 'dougqh/exception-logger-quick-fixes' of github.com:DataDog/dd-trace-java into dougqh/exception-logger-quick-fixes Fixing oversight in handling of null Throwable Adding noException case Fixed bad copy job from stand-alone reproduction spotless Fixing imports Merge branch 'master' into dougqh/exception-logger-quick-fixes Merge branch 'master' into dougqh/exception-logger-quick-fixes Merge branch 'master' into dougqh/exception-logger-quick-fixes Merge branch 'master' into dougqh/exception-logger-quick-fixes Merge branch 'master' into dougqh/exception-logger-quick-fixes Co-authored-by: bric3 <brice.dutheil@gmail.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 7be2605 commit 4d0bac2

File tree

5 files changed

+143
-13
lines changed

5 files changed

+143
-13
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package datadog.trace.bootstrap;
2+
3+
import org.openjdk.jmh.annotations.Benchmark;
4+
import org.openjdk.jmh.annotations.Fork;
5+
import org.openjdk.jmh.annotations.Measurement;
6+
import org.openjdk.jmh.annotations.Threads;
7+
import org.openjdk.jmh.annotations.Warmup;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
/**
12+
* Benchmark showing impact of ExceptionLogger
13+
*
14+
* <p>NOTE: This benchmark exists to check the efficiency of retrieving the ExceptionLogger.
15+
* Previously, this caused significant allocation.
16+
*/
17+
@Fork(2)
18+
@Warmup(iterations = 2)
19+
@Measurement(iterations = 5)
20+
@Threads(8)
21+
public class ExceptionLoggerBenchmark {
22+
@Benchmark
23+
public Logger getExceptionLogger() {
24+
// This matches what happens in the bytecode weaving that defends against
25+
// exception leaking out of instrumentation.
26+
return LoggerFactory.getLogger(ExceptionLogger.class);
27+
}
28+
}

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/ExceptionLogger.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*
99
* <p>See datadog.trace.agent.tooling.ExceptionHandlers
1010
*/
11-
public class ExceptionLogger {
11+
public final class ExceptionLogger {
1212
public static final Logger LOGGER = LoggerFactory.getLogger(ExceptionLogger.class);
13+
14+
private ExceptionLogger() {}
1315
}

dd-java-agent/agent-logging/src/main/java/datadog/trace/logging/simplelogger/SLCompatFactory.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
* <p>Tries to initialize lazily to mimic the behavior of {@code SimpleLogger} as much as possible.
1313
*/
1414
public final class SLCompatFactory extends LoggerHelperFactory {
15-
1615
static final long START_TIME = System.currentTimeMillis();
1716

17+
// DQH - ExceptionLogger isn't visible to this class, so hard-coding instead
18+
static final String EXCEPTION_LOGGER_NAME = "datadog.trace.bootstrap.ExceptionLogger";
19+
LoggerHelper cachedExceptionLoggerHelper;
20+
1821
private static Properties getProperties() {
1922
try {
2023
return System.getProperties();
@@ -55,6 +58,27 @@ private SLCompatSettings getSettings() {
5558

5659
@Override
5760
public LoggerHelper loggerHelperForName(String name) {
61+
if (EXCEPTION_LOGGER_NAME.equals(name)) {
62+
// DQH - This is ugly. The exception propagation protection that handles
63+
// unexpected exceptions in instrumentation always calls LoggerFactory.getLogger.
64+
65+
// getLogger is actually a factory method that always creates a new instance,
66+
// which can cause this method to become extremely hot -- leading to significant
67+
// amount of allocation and garbage collection.
68+
69+
// To guard against that scenario, cache the ExceptionLogger LoggerHelper instance
70+
LoggerHelper helper = cachedExceptionLoggerHelper;
71+
if (helper != null) return helper;
72+
73+
helper = createLoggerHelperForName(name);
74+
cachedExceptionLoggerHelper = helper;
75+
return helper;
76+
} else {
77+
return createLoggerHelperForName(name);
78+
}
79+
}
80+
81+
private LoggerHelper createLoggerHelperForName(String name) {
5882
SLCompatSettings settings = getSettings();
5983
return new SLCompatHelper(name, settings);
6084
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package datadog.trace.api.telemetry;
2+
3+
import org.openjdk.jmh.annotations.Benchmark;
4+
import org.openjdk.jmh.annotations.Fork;
5+
import org.openjdk.jmh.annotations.Measurement;
6+
import org.openjdk.jmh.annotations.Threads;
7+
import org.openjdk.jmh.annotations.Warmup;
8+
9+
@Fork(2)
10+
@Warmup(iterations = 2)
11+
@Measurement(iterations = 5)
12+
@Threads(8)
13+
public class LogCollectorBenchmark {
14+
@Benchmark
15+
public void noException_before() {
16+
LogCollector.get().addLogMessage("error", "ugh!", null);
17+
}
18+
19+
static final Object NULL = null;
20+
21+
@Benchmark
22+
public void nullPointerException() {
23+
// Represents the fast throw case where the JVM switches to using
24+
// a single Exception instance to handle a hot throw location
25+
// of NullPointerException, ArrayIndexOutOfBoundsException, etc.
26+
// In this case, the stacktrace of the exception will not be available.
27+
try {
28+
NULL.hashCode();
29+
} catch (Throwable t) {
30+
LogCollector.get().addLogMessage("error", "npe", t);
31+
}
32+
}
33+
34+
@Benchmark
35+
public void unsupportedOperationException() {
36+
// Represents the common case where stack trace is preserved
37+
// despite hot throw
38+
try {
39+
unsupportedOperation();
40+
} catch (Throwable t) {
41+
LogCollector.get().addLogMessage("error", "unsupported", t);
42+
}
43+
}
44+
45+
static void unsupportedOperation() {
46+
throw new UnsupportedOperationException();
47+
}
48+
}

internal-api/src/main/java/datadog/trace/api/telemetry/LogCollector.java

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package datadog.trace.api.telemetry;
22

3+
import datadog.trace.util.HashingUtils;
34
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45
import java.util.ArrayList;
56
import java.util.Collection;
@@ -68,7 +69,7 @@ public Collection<RawLogMessage> drain() {
6869
return Collections.emptyList();
6970
}
7071

71-
List<RawLogMessage> list = new ArrayList<>();
72+
List<RawLogMessage> list = new ArrayList<>(rawLogMessages.size());
7273
Iterator<Map.Entry<RawLogMessage, AtomicInteger>> iterator =
7374
rawLogMessages.entrySet().iterator();
7475

@@ -87,14 +88,16 @@ public Collection<RawLogMessage> drain() {
8788
return list;
8889
}
8990

90-
public static class RawLogMessage {
91+
public static final class RawLogMessage {
9192
public final String message;
9293
public final String logLevel;
9394
public final Throwable throwable;
9495
public final String tags;
9596
public final long timestamp;
9697
public int count;
9798

99+
private StackTraceElement[] cachedStackTrace = null;
100+
98101
public RawLogMessage(
99102
String logLevel, String message, Throwable throwable, String tags, long timestamp) {
100103
this.logLevel = logLevel;
@@ -104,24 +107,49 @@ public RawLogMessage(
104107
this.timestamp = timestamp;
105108
}
106109

110+
public StackTraceElement[] stackTrace() {
111+
if (throwable == null) return null;
112+
113+
// DQH - getStackTrace makes a defensive copy, so getStackTrace can become a significant
114+
// source of allocation
115+
// In the worst case of a hot exception, we'll constantly call hashCode & equals to
116+
// check against the key stored in the map, so avoiding repeated allocation on each
117+
// comparison does provide a measurable gain
118+
StackTraceElement[] stackTrace = cachedStackTrace;
119+
if (stackTrace != null) return stackTrace;
120+
121+
cachedStackTrace = stackTrace = throwable.getStackTrace();
122+
return stackTrace;
123+
}
124+
107125
@Override
108126
public boolean equals(Object o) {
109127
if (this == o) return true;
110128
if (o == null || getClass() != o.getClass()) return false;
111129
RawLogMessage that = (RawLogMessage) o;
112-
return Objects.equals(logLevel, that.logLevel)
113-
&& Objects.equals(message, that.message)
114-
&& Objects.equals(
115-
throwable == null ? null : throwable.getClass(),
116-
that.throwable == null ? null : that.throwable.getClass())
117-
&& Objects.deepEquals(
118-
throwable == null ? null : throwable.getStackTrace(),
119-
that.throwable == null ? null : that.throwable.getStackTrace());
130+
131+
if (!Objects.equals(logLevel, that.logLevel)) return false;
132+
if (!Objects.equals(message, that.message)) return false;
133+
134+
if (throwable == that.throwable) {
135+
// DQH - While this path may seem unlikely, it does happen if the JVM fast
136+
// throws optimization kicks-in (for NPE, etc), so this case is worth optimizing.
137+
138+
// This also covers the case where both throwables are null
139+
return true;
140+
} else if (throwable != null && that.throwable != null) {
141+
// Both have a throwable perform a deeper comparison
142+
return throwable.getClass().equals(that.throwable.getClass())
143+
&& Objects.deepEquals(stackTrace(), that.stackTrace());
144+
} else {
145+
// One has an exception & the other doesn't, not equal
146+
return false;
147+
}
120148
}
121149

122150
@Override
123151
public int hashCode() {
124-
return Objects.hash(logLevel, message, throwable == null ? null : throwable.getClass());
152+
return HashingUtils.hash(logLevel, message, throwable == null ? null : throwable.getClass());
125153
}
126154
}
127155
}

0 commit comments

Comments
 (0)