-
Notifications
You must be signed in to change notification settings - Fork 331
Reduce per-query allocations in SQLCommenter.inject() #11154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dougqh
wants to merge
3
commits into
master
Choose a base branch
from
dougqh/reduce-sqlcommenter-allocs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+316
−20
Draft
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
223 changes: 223 additions & 0 deletions
223
...strap/src/jmh/java/datadog/trace/bootstrap/instrumentation/dbm/SQLCommenterBenchmark.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| package datadog.trace.bootstrap.instrumentation.dbm; | ||
|
|
||
| import static java.util.concurrent.TimeUnit.NANOSECONDS; | ||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||
|
|
||
| import org.openjdk.jmh.annotations.Benchmark; | ||
| import org.openjdk.jmh.annotations.BenchmarkMode; | ||
| import org.openjdk.jmh.annotations.Fork; | ||
| import org.openjdk.jmh.annotations.Measurement; | ||
| import org.openjdk.jmh.annotations.Mode; | ||
| import org.openjdk.jmh.annotations.OutputTimeUnit; | ||
| import org.openjdk.jmh.annotations.Param; | ||
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.Setup; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.Threads; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
|
|
||
| /** | ||
| * Benchmarks the trace comment detection and first-word extraction optimizations in SQLCommenter. | ||
| * | ||
| * <p>Compares: | ||
| * | ||
| * <ul> | ||
| * <li>Baseline: substring allocation + String.contains for trace comment detection | ||
| * <li>Optimized: range-based regionMatches with zero allocation | ||
| * <li>Baseline: getFirstWord substring + startsWith/equalsIgnoreCase | ||
| * <li>Optimized: firstWordStartsWith/firstWordEqualsIgnoreCase via regionMatches | ||
| * </ul> | ||
| * | ||
| * <p>Run with: | ||
| * | ||
| * <pre> | ||
| * ./gradlew :dd-java-agent:agent-bootstrap:jmhJar | ||
| * java -jar dd-java-agent/agent-bootstrap/build/libs/agent-bootstrap-*-jmh.jar SQLCommenterBenchmark | ||
| * </pre> | ||
| */ | ||
| @State(Scope.Benchmark) | ||
| @Warmup(iterations = 3, time = 5, timeUnit = SECONDS) | ||
| @Measurement(iterations = 5, time = 5, timeUnit = SECONDS) | ||
| @BenchmarkMode(Mode.Throughput) | ||
| @OutputTimeUnit(NANOSECONDS) | ||
| @Fork(value = 1) | ||
| public class SQLCommenterBenchmark { | ||
|
|
||
| // Realistic SQL with a prepended DBM comment (the common case for hasDDComment check) | ||
| private static final String SQL_WITH_COMMENT = | ||
| "/*ddps='myservice',dddbs='mydb',ddh='db-host.example.com',dddb='production_db'," | ||
| + "dde='prod',ddpv='1.2.3'*/ SELECT u.id, u.name, u.email FROM users u " | ||
| + "WHERE u.active = ? AND u.created_at > ? ORDER BY u.name"; | ||
|
|
||
| // SQL without any comment (the common case for normal queries) | ||
| private static final String SQL_NO_COMMENT = | ||
| "SELECT u.id, u.name, u.email FROM users u " | ||
| + "WHERE u.active = ? AND u.created_at > ? ORDER BY u.name"; | ||
|
|
||
| // SQL with appended comment (MySQL/CALL style) | ||
| private static final String SQL_APPENDED_COMMENT = | ||
| "CALL get_user_data(?, ?) /*ddps='myservice',dddbs='mydb',ddh='db-host.example.com'," | ||
| + "dddb='production_db',dde='prod',ddpv='1.2.3'*/"; | ||
|
|
||
| // Short SQL for first-word extraction benchmarks | ||
| private static final String SQL_SELECT = "SELECT * FROM users WHERE id = ?"; | ||
| private static final String SQL_CALL = "CALL get_user_data(?, ?)"; | ||
| private static final String SQL_BRACE = "{ call get_user_data(?, ?) }"; | ||
|
|
||
| @Param({"prepended", "appended", "none"}) | ||
| String commentStyle; | ||
|
|
||
| String sql; | ||
| boolean appendComment; | ||
|
|
||
| // Pre-computed indices for the optimized range-based check | ||
| int commentStartIdx; | ||
| int commentEndIdx; | ||
| String commentContent; // Pre-extracted for baseline comparison | ||
|
|
||
| @Setup | ||
| public void setup() { | ||
| switch (commentStyle) { | ||
| case "prepended": | ||
| sql = SQL_WITH_COMMENT; | ||
| appendComment = false; | ||
| break; | ||
| case "appended": | ||
| sql = SQL_APPENDED_COMMENT; | ||
| appendComment = true; | ||
| break; | ||
| case "none": | ||
| default: | ||
| sql = SQL_NO_COMMENT; | ||
| appendComment = false; | ||
| break; | ||
| } | ||
|
|
||
| // Pre-compute the comment bounds for the range-based benchmark | ||
| if (appendComment) { | ||
| commentStartIdx = sql.lastIndexOf("/*"); | ||
| commentEndIdx = sql.lastIndexOf("*/"); | ||
| } else { | ||
| commentStartIdx = sql.indexOf("/*"); | ||
| commentEndIdx = sql.indexOf("*/"); | ||
| } | ||
|
|
||
| // Pre-extract the comment content for the baseline benchmark | ||
| if (commentStartIdx != -1 && commentEndIdx != -1 && commentEndIdx > commentStartIdx) { | ||
| commentContent = sql.substring(commentStartIdx + 2, commentEndIdx); | ||
| } else { | ||
| commentContent = ""; | ||
| } | ||
| } | ||
|
|
||
| // --- containsTraceComment benchmarks --- | ||
|
|
||
| /** | ||
| * Baseline: extract substring then check with String.contains. This is what the old code did via | ||
| * extractCommentContent() + containsTraceComment(String). | ||
| */ | ||
| @Benchmark | ||
| @Threads(1) | ||
| public boolean containsTraceComment_baseline_substring_1T() { | ||
| if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) { | ||
| return false; | ||
| } | ||
| // Allocates a substring — the old extractCommentContent() behavior | ||
| String extracted = sql.substring(commentStartIdx + 2, commentEndIdx); | ||
| return SharedDBCommenter.containsTraceComment(extracted); | ||
| } | ||
|
|
||
| /** | ||
| * Optimized: range-based check with regionMatches, zero allocation. This is what the new code | ||
| * does via containsTraceComment(String, int, int). | ||
| */ | ||
| @Benchmark | ||
| @Threads(1) | ||
| public boolean containsTraceComment_optimized_range_1T() { | ||
| if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) { | ||
| return false; | ||
| } | ||
| return SharedDBCommenter.containsTraceComment(sql, commentStartIdx + 2, commentEndIdx); | ||
| } | ||
|
|
||
| /** Multi-threaded baseline — exposes GC pressure from substring allocation under contention. */ | ||
| @Benchmark | ||
| @Threads(8) | ||
| public boolean containsTraceComment_baseline_substring_8T() { | ||
| if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) { | ||
| return false; | ||
| } | ||
| String extracted = sql.substring(commentStartIdx + 2, commentEndIdx); | ||
| return SharedDBCommenter.containsTraceComment(extracted); | ||
| } | ||
|
|
||
| /** Multi-threaded optimized — no allocation, no GC pressure. */ | ||
| @Benchmark | ||
| @Threads(8) | ||
| public boolean containsTraceComment_optimized_range_8T() { | ||
| if (commentStartIdx == -1 || commentEndIdx == -1 || commentEndIdx <= commentStartIdx) { | ||
| return false; | ||
| } | ||
| return SharedDBCommenter.containsTraceComment(sql, commentStartIdx + 2, commentEndIdx); | ||
| } | ||
|
|
||
| // --- firstWord benchmarks --- | ||
|
|
||
| /** | ||
| * Baseline: allocate substring via getFirstWord, then call startsWith. This is what the old | ||
| * inject() code did. | ||
| */ | ||
| @Benchmark | ||
| @Threads(1) | ||
| public boolean firstWord_baseline_substring_1T() { | ||
| String firstWord = getFirstWord(sql); | ||
| return firstWord.startsWith("{"); | ||
| } | ||
|
|
||
| /** Optimized: regionMatches-based check, zero allocation. */ | ||
| @Benchmark | ||
| @Threads(1) | ||
| public boolean firstWord_optimized_regionMatches_1T() { | ||
| return firstWordStartsWith(sql, "{"); | ||
| } | ||
|
|
||
| /** Multi-threaded baseline — substring allocation under contention. */ | ||
| @Benchmark | ||
| @Threads(8) | ||
| public boolean firstWord_baseline_substring_8T() { | ||
| String firstWord = getFirstWord(sql); | ||
| return firstWord.startsWith("{"); | ||
| } | ||
|
|
||
| /** Multi-threaded optimized — zero allocation. */ | ||
| @Benchmark | ||
| @Threads(8) | ||
| public boolean firstWord_optimized_regionMatches_8T() { | ||
| return firstWordStartsWith(sql, "{"); | ||
| } | ||
|
|
||
| // --- Inlined helper methods (mirror the implementations for fair comparison) --- | ||
|
|
||
| /** Original getFirstWord — allocates a substring. */ | ||
| private static String getFirstWord(String sql) { | ||
| int beginIndex = 0; | ||
| while (beginIndex < sql.length() && Character.isWhitespace(sql.charAt(beginIndex))) { | ||
| beginIndex++; | ||
| } | ||
| int endIndex = beginIndex; | ||
| while (endIndex < sql.length() && !Character.isWhitespace(sql.charAt(endIndex))) { | ||
| endIndex++; | ||
| } | ||
| return sql.substring(beginIndex, endIndex); | ||
| } | ||
|
|
||
| /** Optimized firstWordStartsWith — zero allocation via regionMatches. */ | ||
| private static boolean firstWordStartsWith(String sql, String prefix) { | ||
| int beginIndex = 0; | ||
| int len = sql.length(); | ||
| while (beginIndex < len && Character.isWhitespace(sql.charAt(beginIndex))) { | ||
| beginIndex++; | ||
| } | ||
| return sql.regionMatches(beginIndex, prefix, 0, prefix.length()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,17 +32,56 @@ public class SharedDBCommenter { | |
| private static final String TRACEPARENT = encode("traceparent"); | ||
| private static final String DD_SERVICE_HASH = encode("ddsh"); | ||
|
|
||
| // Pre-computed marker strings for trace comment detection | ||
| private static final String PARENT_SERVICE_EQ = PARENT_SERVICE + "="; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These constants are obviously a good idea, keep them |
||
| private static final String DATABASE_SERVICE_EQ = DATABASE_SERVICE + "="; | ||
| private static final String DD_HOSTNAME_EQ = DD_HOSTNAME + "="; | ||
| private static final String DD_DB_NAME_EQ = DD_DB_NAME + "="; | ||
| private static final String DD_PEER_SERVICE_EQ = DD_PEER_SERVICE + "="; | ||
| private static final String DD_ENV_EQ = DD_ENV + "="; | ||
| private static final String DD_VERSION_EQ = DD_VERSION + "="; | ||
| private static final String TRACEPARENT_EQ = TRACEPARENT + "="; | ||
| private static final String DD_SERVICE_HASH_EQ = DD_SERVICE_HASH + "="; | ||
|
|
||
| // Used by SQLCommenter and MongoCommentInjector to avoid duplicate comment injection | ||
| public static boolean containsTraceComment(String commentContent) { | ||
| return commentContent.contains(PARENT_SERVICE + "=") | ||
| || commentContent.contains(DATABASE_SERVICE + "=") | ||
| || commentContent.contains(DD_HOSTNAME + "=") | ||
| || commentContent.contains(DD_DB_NAME + "=") | ||
| || commentContent.contains(DD_PEER_SERVICE + "=") | ||
| || commentContent.contains(DD_ENV + "=") | ||
| || commentContent.contains(DD_VERSION + "=") | ||
| || commentContent.contains(TRACEPARENT + "=") | ||
| || commentContent.contains(DD_SERVICE_HASH + "="); | ||
| return commentContent.contains(PARENT_SERVICE_EQ) | ||
| || commentContent.contains(DATABASE_SERVICE_EQ) | ||
| || commentContent.contains(DD_HOSTNAME_EQ) | ||
| || commentContent.contains(DD_DB_NAME_EQ) | ||
| || commentContent.contains(DD_PEER_SERVICE_EQ) | ||
| || commentContent.contains(DD_ENV_EQ) | ||
| || commentContent.contains(DD_VERSION_EQ) | ||
| || commentContent.contains(TRACEPARENT_EQ) | ||
| || commentContent.contains(DD_SERVICE_HASH_EQ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks for trace comment markers within a range of the given string, without allocating a | ||
| * substring. Searches within [fromIndex, toIndex) of the source string. | ||
| */ | ||
| public static boolean containsTraceComment(String sql, int fromIndex, int toIndex) { | ||
| return containsInRange(sql, PARENT_SERVICE_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DATABASE_SERVICE_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DD_HOSTNAME_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DD_DB_NAME_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DD_PEER_SERVICE_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DD_ENV_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DD_VERSION_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, TRACEPARENT_EQ, fromIndex, toIndex) | ||
| || containsInRange(sql, DD_SERVICE_HASH_EQ, fromIndex, toIndex); | ||
| } | ||
|
|
||
| /** Checks if {@code target} appears within the range [fromIndex, toIndex) of {@code source}. */ | ||
| private static boolean containsInRange(String source, String target, int fromIndex, int toIndex) { | ||
| int targetLen = target.length(); | ||
| int limit = toIndex - targetLen; | ||
| for (int i = fromIndex; i <= limit; i++) { | ||
| if (source.regionMatches(i, target, 0, targetLen)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| // Build database comment content without comment delimiters such as /* */ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot decide how I feel about this benchmark.
This definitely less readable than if I wrote it by hand, but I also feel this is more comprehensive.