Skip to content
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

Add an optimized Attributes implementation for instrumenter #3136

Merged
merged 1 commit into from
May 31, 2021
Merged
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
2 changes: 2 additions & 0 deletions benchmark/benchmark.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ plugins {
apply from: "$rootDir/gradle/java.gradle"

dependencies {
jmh platform(project(":dependencyManagement"))

jmh "io.opentelemetry:opentelemetry-api"
jmh "net.bytebuddy:byte-buddy-agent"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.benchmark;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.Nullable;
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.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@Fork(3)
@Warmup(iterations = 10, time = 1)
@Measurement(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
public class InstrumenterBenchmark {

private static final Instrumenter<Void, Void> INSTRUMENTER =
Instrumenter.<Void, Void>newBuilder(
OpenTelemetry.noop(),
"benchmark",
HttpSpanNameExtractor.create(ConstantHttpAttributesExtractor.INSTANCE))
.addAttributesExtractor(ConstantHttpAttributesExtractor.INSTANCE)
.addAttributesExtractor(new ConstantNetAttributesExtractor())
.newInstrumenter();

@Benchmark
public Context start() {
return INSTRUMENTER.start(Context.root(), null);
}

@Benchmark
public Context startEnd() {
Context context = INSTRUMENTER.start(Context.root(), null);
INSTRUMENTER.end(context, null, null, null);
return context;
}

static class ConstantHttpAttributesExtractor extends HttpAttributesExtractor<Void, Void> {
static HttpAttributesExtractor<Void, Void> INSTANCE = new ConstantHttpAttributesExtractor();

@Override
protected @Nullable String method(Void unused) {
return "GET";
}

@Override
protected @Nullable String url(Void unused) {
return "https://opentelemetry.io/benchmark";
}

@Override
protected @Nullable String target(Void unused) {
return "/benchmark";
}

@Override
protected @Nullable String host(Void unused) {
return "opentelemetry.io";
}

@Override
protected @Nullable String route(Void unused) {
return "/benchmark";
}

@Override
protected @Nullable String scheme(Void unused) {
return "https";
}

@Override
protected @Nullable String userAgent(Void unused) {
return "OpenTelemetryBot";
}

@Override
protected @Nullable Long requestContentLength(Void unused, @Nullable Void unused2) {
return 100L;
}

@Override
protected @Nullable Long requestContentLengthUncompressed(Void unused, @Nullable Void unused2) {
return null;
}

@Override
protected @Nullable String flavor(Void unused, @Nullable Void unused2) {
return SemanticAttributes.HttpFlavorValues.HTTP_2_0;
}

@Override
protected @Nullable String serverName(Void unused, @Nullable Void unused2) {
return null;
}

@Override
protected @Nullable String clientIp(Void unused, @Nullable Void unused2) {
return null;
}

@Override
protected @Nullable Integer statusCode(Void unused, Void unused2) {
return 200;
}

@Override
protected @Nullable Long responseContentLength(Void unused, Void unused2) {
return 100L;
}

@Override
protected @Nullable Long responseContentLengthUncompressed(Void unused, Void unused2) {
return null;
}
}

static class ConstantNetAttributesExtractor
extends InetSocketAddressNetAttributesExtractor<Void, Void> {

private static final InetSocketAddress ADDRESS =
InetSocketAddress.createUnresolved("localhost", 8080);

@Override
public @Nullable InetSocketAddress getAddress(Void unused, @Nullable Void unused2) {
return ADDRESS;
}

@Override
public @Nullable String transport(Void unused) {
return SemanticAttributes.NetTransportValues.IP_TCP;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
package io.opentelemetry.instrumentation.api.instrumenter;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
Expand Down Expand Up @@ -121,19 +119,19 @@ public Context start(Context parentContext, REQUEST request) {
spanBuilder.setStartTimestamp(startTimeExtractor.extract(request));
}

AttributesBuilder attributesBuilder = Attributes.builder();
UnsafeAttributes attributesBuilder = new UnsafeAttributes();
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor : extractors) {
extractor.onStart(attributesBuilder, request);
}
Attributes attributes = attributesBuilder.build();
Attributes attributes = attributesBuilder;

Context context = parentContext;

for (RequestListener requestListener : requestListeners) {
context = requestListener.start(context, attributes);
}

attributes.forEach((key, value) -> spanBuilder.setAttribute((AttributeKey) key, value));
spanBuilder.setAllAttributes(attributes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any ideas how we can init the SpanBuilder with the already built Attributes map, to avoid this copy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - I guess it requires an internal use API for that in the SDK. We can try it but it'll be more hacky.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to clarify, did not mean for this pr, something (possibly) for the future..

Span span = spanBuilder.startSpan();
context = context.with(span);
switch (spanKind) {
Expand All @@ -155,17 +153,17 @@ public Context start(Context parentContext, REQUEST request) {
public void end(Context context, REQUEST request, RESPONSE response, @Nullable Throwable error) {
Span span = Span.fromContext(context);

AttributesBuilder attributesBuilder = Attributes.builder();
UnsafeAttributes attributesBuilder = new UnsafeAttributes();
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor : extractors) {
extractor.onEnd(attributesBuilder, request, response);
}
Attributes attributes = attributesBuilder.build();
Attributes attributes = attributesBuilder;

for (RequestListener requestListener : requestListeners) {
requestListener.end(context, attributes);
}

attributes.forEach((key, value) -> span.setAttribute((AttributeKey) key, value));
span.setAllAttributes(attributes);

if (error != null) {
error = errorCauseExtractor.extractCause(error);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.instrumenter;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import java.util.HashMap;
import java.util.Map;

/**
* The {@link AttributesBuilder} and {@link Attributes} used by the instrumentation API. We are able
* to take advantage of the fact that we know our attributes builder cannot be reused to create
* multiple Attributes instances. So we use just one storage for both the builder and attributes. A
* couple of methods still require copying to satisfy the interface contracts, but in practice
* should never be called by user code even though they can.
*/
final class UnsafeAttributes extends HashMap<AttributeKey<?>, Object>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkwatson It's your favorite pattern again :)

implements Attributes, AttributesBuilder {

// Attributes

@SuppressWarnings("unchecked")
@Override
public <T> T get(AttributeKey<T> key) {
return (T) super.get(key);
}

@Override
public Map<AttributeKey<?>, Object> asMap() {
return this;
}

// This can be called by user code in a RequestListener so copy. In practice, it should not be
// called as there is no real use case.
@Override
public AttributesBuilder toBuilder() {
return Attributes.builder().putAll(this);
}

// AttributesBuilder

// This can be called by user code in an AttributesExtractor so copy. In practice, it should not
// be called as there is no real use case.
@Override
public Attributes build() {
return toBuilder().build();
}

@Override
public <T> AttributesBuilder put(AttributeKey<Long> key, int value) {
return put(key, (long) value);
}

@Override
public <T> AttributesBuilder put(AttributeKey<T> key, T value) {
super.put(key, value);
return this;
}

@Override
public AttributesBuilder putAll(Attributes attributes) {
attributes.forEach(this::put);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.instrumenter;

import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import org.junit.jupiter.api.Test;

class UnsafeAttributesTest {

@Test
void buildAndUse() {
Attributes previous =
new UnsafeAttributes().put("world", "earth").put("country", "japan").build();

UnsafeAttributes attributes = new UnsafeAttributes();
attributes.put(AttributeKey.stringKey("animal"), "cat");
attributes.put("needs_catnip", false);
// Overwrites
attributes.put("needs_catnip", true);
attributes.put(AttributeKey.longKey("lives"), 9);
attributes.putAll(previous);

assertThat((Attributes) attributes)
.containsOnly(
attributeEntry("world", "earth"),
attributeEntry("country", "japan"),
attributeEntry("animal", "cat"),
attributeEntry("needs_catnip", true),
attributeEntry("lives", 9L));

Attributes built = attributes.build();
assertThat(built)
.containsOnly(
attributeEntry("world", "earth"),
attributeEntry("country", "japan"),
attributeEntry("animal", "cat"),
attributeEntry("needs_catnip", true),
attributeEntry("lives", 9L));

attributes.put("clothes", "fur");
assertThat((Attributes) attributes)
.containsOnly(
attributeEntry("world", "earth"),
attributeEntry("country", "japan"),
attributeEntry("animal", "cat"),
attributeEntry("needs_catnip", true),
attributeEntry("lives", 9L),
attributeEntry("clothes", "fur"));

// Unmodified
assertThat(built)
.containsOnly(
attributeEntry("world", "earth"),
attributeEntry("country", "japan"),
attributeEntry("animal", "cat"),
attributeEntry("needs_catnip", true),
attributeEntry("lives", 9L));

Attributes modified = attributes.toBuilder().put("country", "us").build();
assertThat(modified)
.containsOnly(
attributeEntry("world", "earth"),
attributeEntry("country", "us"),
attributeEntry("animal", "cat"),
attributeEntry("needs_catnip", true),
attributeEntry("lives", 9L),
attributeEntry("clothes", "fur"));

// Unmodified
assertThat((Attributes) attributes)
.containsOnly(
attributeEntry("world", "earth"),
attributeEntry("country", "japan"),
attributeEntry("animal", "cat"),
attributeEntry("needs_catnip", true),
attributeEntry("lives", 9L),
attributeEntry("clothes", "fur"));
}
}