Skip to content

Commit 2b53be7

Browse files
dougqhclaude
andcommitted
Cache static and per-connection DBM comment fragments in SharedDBCommenter
Eliminate repeated URLEncoder.encode() calls and StringBuilder allocation on every query execution when DBM comment injection is enabled. The static config values (service name, env, version) and per-connection values (dbService, hostname, dbName) are now cached and reused across calls. Only truly dynamic values (traceParent, peerService, baseHash) are computed per-call. Config identity is tracked so caches auto-invalidate if Config is rebuilt (e.g. in tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c13e821 commit 2b53be7

1 file changed

Lines changed: 202 additions & 14 deletions

File tree

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/dbm/SharedDBCommenter.java

Lines changed: 202 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.io.UnsupportedEncodingException;
1010
import java.net.URLEncoder;
1111
import java.nio.charset.StandardCharsets;
12+
import java.util.concurrent.ConcurrentHashMap;
1213
import org.slf4j.Logger;
1314
import org.slf4j.LoggerFactory;
1415

@@ -32,6 +33,28 @@ public class SharedDBCommenter {
3233
private static final String TRACEPARENT = encode("traceparent");
3334
private static final String DD_SERVICE_HASH = encode("ddsh");
3435

36+
/**
37+
* Cached static suffix: "dde='&lt;env&gt;',ddpv='&lt;version&gt;'" Computed lazily on first use
38+
* since Config may not be available at class-load time.
39+
*/
40+
private static volatile String cachedStaticSuffix;
41+
42+
/** Cached URL-encoded service name from Config. */
43+
private static volatile String cachedEncodedServiceName;
44+
45+
/** The Config instance used to compute the cached values, for staleness detection. */
46+
private static volatile Config cachedConfigInstance;
47+
48+
/**
49+
* Cached per-connection prefix fragments keyed by (dbService, hostname, dbName). Each entry holds
50+
* the pre-encoded "ddps='...',dddbs='...',ddh='...',dddb='...'" fragment, combining the static
51+
* service name with per-connection values to preserve the correct field ordering.
52+
*/
53+
private static final int MAX_CONNECTION_CACHE_SIZE = 256;
54+
55+
private static final ConcurrentHashMap<ConnectionKey, String> connectionPrefixCache =
56+
new ConcurrentHashMap<>();
57+
3558
// Used by SQLCommenter and MongoCommentInjector to avoid duplicate comment injection
3659
public static boolean containsTraceComment(String commentContent) {
3760
return commentContent.contains(PARENT_SERVICE + "=")
@@ -50,25 +73,111 @@ public static String buildComment(
5073
String dbService, String dbType, String hostname, String dbName, String traceParent) {
5174

5275
Config config = Config.get();
53-
StringBuilder sb = new StringBuilder();
5476

55-
int initSize = 0; // No initial content for pure comment
56-
append(sb, PARENT_SERVICE, config.getServiceName(), initSize);
57-
append(sb, DATABASE_SERVICE, dbService, initSize);
58-
append(sb, DD_HOSTNAME, hostname, initSize);
59-
append(sb, DD_DB_NAME, dbName, initSize);
60-
append(sb, DD_PEER_SERVICE, getPeerService(), initSize);
61-
append(sb, DD_ENV, config.getEnv(), initSize);
62-
append(sb, DD_VERSION, config.getVersion(), initSize);
63-
append(sb, TRACEPARENT, traceParent, initSize);
77+
// Ensure static values are initialized
78+
ensureStaticValuesInitialized(config);
79+
80+
// Get or compute the prefix: ddps='...',dddbs='...',ddh='...',dddb='...'
81+
String prefix = getConnectionPrefix(dbService, hostname, dbName);
82+
83+
// Get the cached static suffix: dde='...',ddpv='...'
84+
String suffix = cachedStaticSuffix;
6485

65-
if (config.isDbmInjectSqlBaseHash() && config.isExperimentalPropagateProcessTagsEnabled()) {
66-
append(sb, DD_SERVICE_HASH, BaseHash.getBaseHashStr(), initSize);
86+
// Dynamic values that change per-call
87+
String peerService = getPeerService();
88+
boolean injectBaseHash =
89+
config.isDbmInjectSqlBaseHash() && config.isExperimentalPropagateProcessTagsEnabled();
90+
String baseHash = injectBaseHash ? BaseHash.getBaseHashStr() : null;
91+
92+
// Estimate capacity
93+
int capacity =
94+
(prefix != null ? prefix.length() : 0)
95+
+ (suffix != null ? suffix.length() : 0)
96+
+ 128; // generous estimate for dynamic parts
97+
StringBuilder sb = new StringBuilder(capacity);
98+
99+
// Append prefix: ddps, dddbs, ddh, dddb (preserves original field order)
100+
if (prefix != null) {
101+
sb.append(prefix);
102+
}
103+
104+
// Append dynamic: ddprs
105+
appendEncoded(sb, DD_PEER_SERVICE, peerService);
106+
107+
// Append cached static suffix: dde, ddpv
108+
if (suffix != null) {
109+
if (sb.length() > 0) {
110+
sb.append(COMMA);
111+
}
112+
sb.append(suffix);
113+
}
114+
115+
// Append dynamic: traceparent
116+
appendEncoded(sb, TRACEPARENT, traceParent);
117+
118+
// Append conditional: ddsh
119+
if (injectBaseHash) {
120+
appendEncoded(sb, DD_SERVICE_HASH, baseHash);
67121
}
68122

69123
return sb.length() > 0 ? sb.toString() : null;
70124
}
71125

126+
/**
127+
* Initializes the cached static values (service name, env, version) from Config. These never
128+
* change for the agent's lifetime. Identity-checks the Config instance to handle test scenarios
129+
* where Config is rebuilt.
130+
*/
131+
private static void ensureStaticValuesInitialized(Config config) {
132+
if (cachedConfigInstance == config) {
133+
return;
134+
}
135+
// Config instance changed (or first call) -- recompute all cached values
136+
// URL-encode the service name
137+
String serviceName = config.getServiceName();
138+
cachedEncodedServiceName = encodeValue(serviceName);
139+
140+
// Build the static suffix: dde='...', ddpv='...'
141+
StringBuilder sb = new StringBuilder(48);
142+
appendEncoded(sb, DD_ENV, config.getEnv());
143+
appendEncoded(sb, DD_VERSION, config.getVersion());
144+
cachedStaticSuffix = sb.length() > 0 ? sb.toString() : null;
145+
146+
// Clear connection prefix cache since it includes the service name
147+
connectionPrefixCache.clear();
148+
149+
// Publish the config instance last so other threads see all updates
150+
cachedConfigInstance = config;
151+
}
152+
153+
/**
154+
* Returns a cached prefix fragment combining ddps (static service name) with dddbs, ddh, and dddb
155+
* (per-connection values). This preserves the original field ordering:
156+
* ddps='...',dddbs='...',ddh='...',dddb='...'
157+
*/
158+
private static String getConnectionPrefix(String dbService, String hostname, String dbName) {
159+
ConnectionKey key = new ConnectionKey(dbService, hostname, dbName);
160+
String fragment = connectionPrefixCache.get(key);
161+
if (fragment != null) {
162+
return fragment;
163+
}
164+
// Build the prefix fragment preserving original order
165+
StringBuilder sb = new StringBuilder(96);
166+
appendPreEncoded(sb, PARENT_SERVICE, cachedEncodedServiceName);
167+
appendEncoded(sb, DATABASE_SERVICE, dbService);
168+
appendEncoded(sb, DD_HOSTNAME, hostname);
169+
appendEncoded(sb, DD_DB_NAME, dbName);
170+
fragment = sb.length() > 0 ? sb.toString() : null;
171+
if (fragment != null) {
172+
// Evict all entries if cache grows too large to bound memory
173+
if (connectionPrefixCache.size() >= MAX_CONNECTION_CACHE_SIZE) {
174+
connectionPrefixCache.clear();
175+
}
176+
connectionPrefixCache.put(key, fragment);
177+
}
178+
return fragment;
179+
}
180+
72181
private static String getPeerService() {
73182
AgentSpan span = activeSpan();
74183
Object peerService = null;
@@ -89,7 +198,36 @@ private static String encode(String val) {
89198
return val;
90199
}
91200

92-
private static void append(StringBuilder sb, String key, String value, int initSize) {
201+
/** URL-encodes a value, returning null if the input is null or empty. */
202+
private static String encodeValue(String value) {
203+
if (value == null || value.isEmpty()) {
204+
return null;
205+
}
206+
try {
207+
return URLEncoder.encode(value, UTF8);
208+
} catch (UnsupportedEncodingException e) {
209+
return value;
210+
}
211+
}
212+
213+
/**
214+
* Appends a key='already-encoded-value' pair to the StringBuilder. Used for values that have
215+
* already been URL-encoded and cached.
216+
*/
217+
private static void appendPreEncoded(StringBuilder sb, String key, String encodedValue) {
218+
if (null == encodedValue || encodedValue.isEmpty()) {
219+
return;
220+
}
221+
if (sb.length() > 0) {
222+
sb.append(COMMA);
223+
}
224+
sb.append(key).append(EQUALS).append(QUOTE).append(encodedValue).append(QUOTE);
225+
}
226+
227+
/**
228+
* Appends an encoded key='value' pair to the StringBuilder if value is non-null and non-empty.
229+
*/
230+
private static void appendEncoded(StringBuilder sb, String key, String value) {
93231
if (null == value || value.isEmpty()) {
94232
return;
95233
}
@@ -99,9 +237,59 @@ private static void append(StringBuilder sb, String key, String value, int initS
99237
} catch (UnsupportedEncodingException e) {
100238
encodedValue = value;
101239
}
102-
if (sb.length() > initSize) {
240+
if (sb.length() > 0) {
103241
sb.append(COMMA);
104242
}
105243
sb.append(key).append(EQUALS).append(QUOTE).append(encodedValue).append(QUOTE);
106244
}
245+
246+
/** Resets cached state. Visible for testing. */
247+
static void resetCache() {
248+
cachedConfigInstance = null;
249+
cachedStaticSuffix = null;
250+
cachedEncodedServiceName = null;
251+
connectionPrefixCache.clear();
252+
}
253+
254+
/**
255+
* Composite key for per-connection prefix cache. Holds the raw (unencoded) dbService, hostname,
256+
* and dbName values.
257+
*/
258+
private static final class ConnectionKey {
259+
private final String dbService;
260+
private final String hostname;
261+
private final String dbName;
262+
private final int hashCode;
263+
264+
ConnectionKey(String dbService, String hostname, String dbName) {
265+
this.dbService = dbService;
266+
this.hostname = hostname;
267+
this.dbName = dbName;
268+
// Pre-compute hash code since this is an immutable key
269+
int h = 17;
270+
h = 31 * h + (dbService != null ? dbService.hashCode() : 0);
271+
h = 31 * h + (hostname != null ? hostname.hashCode() : 0);
272+
h = 31 * h + (dbName != null ? dbName.hashCode() : 0);
273+
this.hashCode = h;
274+
}
275+
276+
@Override
277+
public boolean equals(Object o) {
278+
if (this == o) return true;
279+
if (!(o instanceof ConnectionKey)) return false;
280+
ConnectionKey that = (ConnectionKey) o;
281+
return equals(dbService, that.dbService)
282+
&& equals(hostname, that.hostname)
283+
&& equals(dbName, that.dbName);
284+
}
285+
286+
private static boolean equals(String a, String b) {
287+
return a == null ? b == null : a.equals(b);
288+
}
289+
290+
@Override
291+
public int hashCode() {
292+
return hashCode;
293+
}
294+
}
107295
}

0 commit comments

Comments
 (0)