Skip to content

Commit 1aa7bac

Browse files
dougqhclaude
andcommitted
Add multi-threaded SQLCommenter benchmark for containsTraceComment
Benchmarks baseline (substring + String.contains) vs optimized (range-based regionMatches) for trace comment detection and first-word extraction. Includes 1-thread and 8-thread variants with prepended, appended, and no-comment SQL scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a26939e commit 1aa7bac

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package datadog.trace.bootstrap.instrumentation.dbm;
2+
3+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
4+
import static java.util.concurrent.TimeUnit.SECONDS;
5+
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Fork;
9+
import org.openjdk.jmh.annotations.Measurement;
10+
import org.openjdk.jmh.annotations.Mode;
11+
import org.openjdk.jmh.annotations.OutputTimeUnit;
12+
import org.openjdk.jmh.annotations.Param;
13+
import org.openjdk.jmh.annotations.Scope;
14+
import org.openjdk.jmh.annotations.Setup;
15+
import org.openjdk.jmh.annotations.State;
16+
import org.openjdk.jmh.annotations.Threads;
17+
import org.openjdk.jmh.annotations.Warmup;
18+
19+
/**
20+
* Benchmarks the trace comment detection and first-word extraction optimizations in SQLCommenter.
21+
*
22+
* <p>Compares:
23+
*
24+
* <ul>
25+
* <li>Baseline: substring allocation + String.contains for trace comment detection
26+
* <li>Optimized: range-based regionMatches with zero allocation
27+
* <li>Baseline: getFirstWord substring + startsWith/equalsIgnoreCase
28+
* <li>Optimized: firstWordStartsWith/firstWordEqualsIgnoreCase via regionMatches
29+
* </ul>
30+
*
31+
* <p>Run with:
32+
*
33+
* <pre>
34+
* ./gradlew :dd-java-agent:agent-bootstrap:jmhJar
35+
* java -jar dd-java-agent/agent-bootstrap/build/libs/agent-bootstrap-*-jmh.jar SQLCommenterBenchmark
36+
* </pre>
37+
*/
38+
@State(Scope.Benchmark)
39+
@Warmup(iterations = 3, time = 5, timeUnit = SECONDS)
40+
@Measurement(iterations = 5, time = 5, timeUnit = SECONDS)
41+
@BenchmarkMode(Mode.Throughput)
42+
@OutputTimeUnit(NANOSECONDS)
43+
@Fork(value = 1)
44+
public class SQLCommenterBenchmark {
45+
46+
// Realistic SQL with a prepended DBM comment (the common case for hasDDComment check)
47+
private static final String SQL_WITH_COMMENT =
48+
"/*ddps='myservice',dddbs='mydb',ddh='db-host.example.com',dddb='production_db',"
49+
+ "dde='prod',ddpv='1.2.3'*/ SELECT u.id, u.name, u.email FROM users u "
50+
+ "WHERE u.active = ? AND u.created_at > ? ORDER BY u.name";
51+
52+
// SQL without any comment (the common case for normal queries)
53+
private static final String SQL_NO_COMMENT =
54+
"SELECT u.id, u.name, u.email FROM users u "
55+
+ "WHERE u.active = ? AND u.created_at > ? ORDER BY u.name";
56+
57+
// SQL with appended comment (MySQL/CALL style)
58+
private static final String SQL_APPENDED_COMMENT =
59+
"CALL get_user_data(?, ?) /*ddps='myservice',dddbs='mydb',ddh='db-host.example.com',"
60+
+ "dddb='production_db',dde='prod',ddpv='1.2.3'*/";
61+
62+
// Short SQL for first-word extraction benchmarks
63+
private static final String SQL_SELECT = "SELECT * FROM users WHERE id = ?";
64+
private static final String SQL_CALL = "CALL get_user_data(?, ?)";
65+
private static final String SQL_BRACE = "{ call get_user_data(?, ?) }";
66+
67+
@Param({"prepended", "appended", "none"})
68+
String commentStyle;
69+
70+
String sql;
71+
boolean appendComment;
72+
73+
// Pre-computed indices for the optimized range-based check
74+
int commentStartIdx;
75+
int commentEndIdx;
76+
String commentContent; // Pre-extracted for baseline comparison
77+
78+
@Setup
79+
public void setup() {
80+
switch (commentStyle) {
81+
case "prepended":
82+
sql = SQL_WITH_COMMENT;
83+
appendComment = false;
84+
break;
85+
case "appended":
86+
sql = SQL_APPENDED_COMMENT;
87+
appendComment = true;
88+
break;
89+
case "none":
90+
default:
91+
sql = SQL_NO_COMMENT;
92+
appendComment = false;
93+
break;
94+
}
95+
96+
// Pre-compute the comment bounds for the range-based benchmark
97+
if (appendComment) {
98+
commentStartIdx = sql.lastIndexOf("/*");
99+
commentEndIdx = sql.lastIndexOf("*/");
100+
} else {
101+
commentStartIdx = sql.indexOf("/*");
102+
commentEndIdx = sql.indexOf("*/");
103+
}
104+
105+
// Pre-extract the comment content for the baseline benchmark
106+
if (commentStartIdx != -1 && commentEndIdx != -1 && commentEndIdx > commentStartIdx) {
107+
commentContent = sql.substring(commentStartIdx + 2, commentEndIdx);
108+
} else {
109+
commentContent = "";
110+
}
111+
}
112+
113+
// --- containsTraceComment benchmarks ---
114+
115+
/**
116+
* Baseline: extract substring then check with String.contains. This is what the old code did via
117+
* extractCommentContent() + containsTraceComment(String).
118+
*/
119+
@Benchmark
120+
@Threads(1)
121+
public boolean containsTraceComment_baseline_substring_1T() {
122+
if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) {
123+
return false;
124+
}
125+
// Allocates a substring — the old extractCommentContent() behavior
126+
String extracted = sql.substring(commentStartIdx + 2, commentEndIdx);
127+
return SharedDBCommenter.containsTraceComment(extracted);
128+
}
129+
130+
/**
131+
* Optimized: range-based check with regionMatches, zero allocation. This is what the new code
132+
* does via containsTraceComment(String, int, int).
133+
*/
134+
@Benchmark
135+
@Threads(1)
136+
public boolean containsTraceComment_optimized_range_1T() {
137+
if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) {
138+
return false;
139+
}
140+
return SharedDBCommenter.containsTraceComment(sql, commentStartIdx + 2, commentEndIdx);
141+
}
142+
143+
/** Multi-threaded baseline — exposes GC pressure from substring allocation under contention. */
144+
@Benchmark
145+
@Threads(8)
146+
public boolean containsTraceComment_baseline_substring_8T() {
147+
if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) {
148+
return false;
149+
}
150+
String extracted = sql.substring(commentStartIdx + 2, commentEndIdx);
151+
return SharedDBCommenter.containsTraceComment(extracted);
152+
}
153+
154+
/** Multi-threaded optimized — no allocation, no GC pressure. */
155+
@Benchmark
156+
@Threads(8)
157+
public boolean containsTraceComment_optimized_range_8T() {
158+
if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) {
159+
return false;
160+
}
161+
return SharedDBCommenter.containsTraceComment(sql, commentStartIdx + 2, commentEndIdx);
162+
}
163+
164+
// --- firstWord benchmarks ---
165+
166+
/**
167+
* Baseline: allocate substring via getFirstWord, then call startsWith. This is what the old
168+
* inject() code did.
169+
*/
170+
@Benchmark
171+
@Threads(1)
172+
public boolean firstWord_baseline_substring_1T() {
173+
String firstWord = getFirstWord(sql);
174+
return firstWord.startsWith("{");
175+
}
176+
177+
/** Optimized: regionMatches-based check, zero allocation. */
178+
@Benchmark
179+
@Threads(1)
180+
public boolean firstWord_optimized_regionMatches_1T() {
181+
return firstWordStartsWith(sql, "{");
182+
}
183+
184+
/** Multi-threaded baseline — substring allocation under contention. */
185+
@Benchmark
186+
@Threads(8)
187+
public boolean firstWord_baseline_substring_8T() {
188+
String firstWord = getFirstWord(sql);
189+
return firstWord.startsWith("{");
190+
}
191+
192+
/** Multi-threaded optimized — zero allocation. */
193+
@Benchmark
194+
@Threads(8)
195+
public boolean firstWord_optimized_regionMatches_8T() {
196+
return firstWordStartsWith(sql, "{");
197+
}
198+
199+
// --- Inlined helper methods (mirror the implementations for fair comparison) ---
200+
201+
/** Original getFirstWord — allocates a substring. */
202+
private static String getFirstWord(String sql) {
203+
int beginIndex = 0;
204+
while (beginIndex < sql.length() && Character.isWhitespace(sql.charAt(beginIndex))) {
205+
beginIndex++;
206+
}
207+
int endIndex = beginIndex;
208+
while (endIndex < sql.length() && !Character.isWhitespace(sql.charAt(endIndex))) {
209+
endIndex++;
210+
}
211+
return sql.substring(beginIndex, endIndex);
212+
}
213+
214+
/** Optimized firstWordStartsWith — zero allocation via regionMatches. */
215+
private static boolean firstWordStartsWith(String sql, String prefix) {
216+
int beginIndex = 0;
217+
int len = sql.length();
218+
while (beginIndex < len && Character.isWhitespace(sql.charAt(beginIndex))) {
219+
beginIndex++;
220+
}
221+
return sql.regionMatches(beginIndex, prefix, 0, prefix.length());
222+
}
223+
}

0 commit comments

Comments
 (0)