|
| 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