Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package datadog.trace.bootstrap.instrumentation.decorator;

import static datadog.trace.bootstrap.instrumentation.api.Tags.DB_USER;
import static java.util.concurrent.TimeUnit.SECONDS;

import datadog.trace.api.GlobalTracer;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.common.writer.Writer;
import datadog.trace.core.CoreTracer;
import datadog.trace.core.DDSpan;
import java.util.List;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
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;

/**
* Acceptance test for the {@code dbUser} peel: measures the param-injected connection-tags {@link
* TagExtractor} form of {@link DatabaseClientDecorator#onConnection(AgentSpan, Object,
* TagExtractor)} — the shape that replaced the sparsely-overridden {@code dbUser} template method.
*
* <p>One decorator type is used throughout (so the receiver and the {@code dbInstance}/{@code
* dbHostname} template calls stay monomorphic); only the injected extractor varies, to isolate the
* question raised in review: does passing the extractor as a caller-side constant devirtualize and
* inline?
*
* <p>{@code mode}:
*
* <ul>
* <li><b>mono</b> — a single {@code static final} extractor at the call site (the production
* shape: each integration's advice passes its own constant). Expect {@code extract()} to
* devirtualize and inline when {@code onConnection} inlines.
* <li><b>mega</b> — {@link #TYPES} distinct extractor types cycled through the one site (the
* worst case: a single shared site that never sees a stable type). Characterizes the downside
* if the call is <em>not</em> inlined.
* </ul>
*
* <p>Run with {@code -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining} to confirm the mechanism
* (the tree, not just the number) — look for {@code DatabaseClientDecorator::onConnection} and the
* extractor {@code extract} bodies.
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(SECONDS)
@Warmup(iterations = 5, time = 2)
@Measurement(iterations = 5, time = 2)
@Fork(3)
@Threads(8)
public class DatabaseClientConnectionBenchmark {

private static final int TYPES = 8;
private static final String CONN = "bench-user";

/** The mono call site: a single static-final constant extractor. */
private static final TagExtractor<String> MONO = new E0();

@Param({"mono", "mega"})
String mode;

private boolean mega;
private BenchDbDecorator decorator;
private AgentSpan span;
private TagExtractor<String>[] extractors;
private int idx;

@SuppressWarnings("unchecked")
@Setup(Level.Trial)
public void setUp() {
CoreTracer tracer =
CoreTracer.builder().strictTraceWrites(true).writer(new NoOpWriter()).build();
GlobalTracer.forceRegister(tracer);
decorator = new BenchDbDecorator();
span = tracer.startSpan("benchmark", "db.query");
extractors =
new TagExtractor[] {
new E0(), new E1(), new E2(), new E3(), new E4(), new E5(), new E6(), new E7()
};
mega = "mega".equals(mode);
}

@Benchmark
public AgentSpan onConnection() {
if (mega) {
int i = idx;
idx = (i + 1 == TYPES) ? 0 : i + 1;
return decorator.onConnection(span, CONN, extractors[i]);
}
return decorator.onConnection(span, CONN, MONO);
}

// Eight structurally-identical but distinct extractor types (so the mega site sees a rotating
// type).
static final class E0 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E1 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E2 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E3 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E4 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E5 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E6 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class E7 implements TagExtractor<String> {
public void extract(String c, AgentSpan s) {
s.setTag(DB_USER, c);
}
}

static final class BenchDbDecorator extends DatabaseClientDecorator<String> {
private static final CharSequence COMPONENT = UTF8BytesString.create("benchmark");

@Override
protected String[] instrumentationNames() {
return new String[] {"benchmark"};
}

@Override
protected String service() {
return "benchmark-db";
}

@Override
protected CharSequence component() {
return COMPONENT;
}

@Override
protected CharSequence spanType() {
return "sql";
}

@Override
protected String dbType() {
return "benchdb";
}

@Override
protected String dbInstance(String connection) {
return connection;
}

@Override
protected CharSequence dbHostname(String connection) {
return null;
}
}

private static class NoOpWriter implements Writer {
@Override
public void write(final List<DDSpan> trace) {}

@Override
public void start() {}

@Override
public boolean flush() {
return false;
}

@Override
public void close() {}

@Override
public void incrementDropCounts(final int spanCount) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import java.util.function.BiConsumer;
Expand Down Expand Up @@ -55,12 +56,15 @@ public String getDbType() {

protected abstract String dbType();

protected abstract String dbUser(CONNECTION connection);

protected abstract String dbInstance(CONNECTION connection);

protected abstract CharSequence dbHostname(CONNECTION connection);

/**
* No-op connection-tag extractor: the default for stores that contribute no pure connection tags.
*/
private static final TagExtractor<Object> NO_CONNECTION_TAGS = (connection, span) -> {};

/**
* This should be called when the connection is being used, not when it's created.
*
Expand All @@ -69,8 +73,26 @@ public String getDbType() {
* @return
*/
public AgentSpan onConnection(final AgentSpan span, final CONNECTION connection) {
return onConnection(span, connection, noConnectionTags());
}

/**
* As {@link #onConnection(AgentSpan, Object)}, but with a {@link TagExtractor} for the store's
* pure connection tags (e.g. {@code db.user} for SQL) injected as a parameter.
*
* <p>Passing the extractor as a caller-side argument — rather than fetching it from a virtual
* method — is what preserves inlining: at a monomorphic advice site the extractor is a {@code
* static final} constant, so when this (small) method inlines, {@code extractor.extract(...)}
* devirtualizes and inlines too. It replaces the previous per-store template methods (the
* sparsely overridden {@code dbUser}, which most NoSQL stores could only answer with {@code
* null}) and is the seam for disentangling pure tag extraction from the derivation below.
*/
public AgentSpan onConnection(
final AgentSpan span,
final CONNECTION connection,
final TagExtractor<CONNECTION> connectionTags) {
if (connection != null) {
span.setTag(Tags.DB_USER, dbUser(connection));
connectionTags.extract(connection, span);
onInstance(span, dbInstance(connection));
CharSequence hostName = dbHostname(connection);
if (hostName != null) {
Expand All @@ -84,6 +106,11 @@ public AgentSpan onConnection(final AgentSpan span, final CONNECTION connection)
return span;
}

@SuppressWarnings("unchecked")
protected static <CONNECTION> TagExtractor<CONNECTION> noConnectionTags() {
return (TagExtractor<CONNECTION>) NO_CONNECTION_TAGS;
}

protected AgentSpan onInstance(final AgentSpan span, final String dbInstance) {
if (dbInstance != null) {
span.setTag(Tags.DB_INSTANCE, dbInstance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ class DBTypeProcessingDatabaseClientDecoratorTest extends ClientDecoratorTest {
return "test-db"
}

@Override
protected String dbUser(Map map) {
return map.user
}

@Override
protected String dbInstance(Map map) {
return map.instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest {

then:
if (session) {
1 * span.setTag(Tags.DB_USER, session.user)
// db.user is no longer set by the 2-arg onConnection (it used to come from the dbUser template
// method); it now arrives via the connection-tags extractor of the 3-arg form (see below).
if (session.instance != null) {
1 * span.setTag(Tags.DB_INSTANCE, session.instance)
}
Expand Down Expand Up @@ -88,6 +89,32 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest {
true | true | true | [user: "test-user", instance: "test-instance"]
}

def "test onConnection applies the connection-tags extractor"() {
setup:
def decorator = newDecorator()
def session = [user: "test-user"]

when:
decorator.onConnection(span, session, { Map m, AgentSpan s -> s.setTag(Tags.DB_USER, m.user) })

then:
1 * span.setTag(Tags.DB_USER, "test-user")
0 * _
}

def "test onConnection with a null connection skips the extractor"() {
setup:
def decorator = newDecorator()
def extractor = Mock(datadog.trace.bootstrap.instrumentation.api.TagExtractor)

when:
decorator.onConnection(span, null, extractor)

then:
0 * extractor.extract(_, _)
0 * _
}

def "test onStatement"() {
setup:
def decorator = newDecorator()
Expand Down Expand Up @@ -134,11 +161,6 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest {
return "test-db"
}

@Override
protected String dbUser(Map map) {
return map.user
}

@Override
protected String dbInstance(Map map) {
return map.instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ class OrmClientDecoratorTest extends DatabaseClientDecoratorTest {
return "test-db"
}

@Override
protected String dbUser(Object o) {
return "test-user"
}

@Override
protected String dbInstance(Object o) {
return "test-user"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ protected String dbType() {
return DB_TYPE;
}

@Override
protected String dbUser(final Node node) {
return null;
}

@Override
protected String dbInstance(final Node node) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ protected String dbType() {
return "couchbase";
}

@Override
protected String dbUser(final Object o) {
return null;
}

@Override
protected String dbInstance(final Object o) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ protected String dbType() {
return DB_TYPE;
}

@Override
protected String dbUser(final Object o) {
return null;
}

@Override
protected String dbInstance(final Object o) {
return null;
Expand Down
Loading
Loading