From 98f71f6bb16e3b8782745b70e474c3543547dca4 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Tue, 15 Jul 2025 09:41:27 -0400 Subject: [PATCH] squashing & signing This change introduces TagMap as replacement for HashMap when working with tags. TagMap has two different implementations... - one that extends a regular HashMap - another that uses a different approach that allows for Entry sharing This change currently uses the HashMap approach by default, but allows for switching to the optimized Map via a configuration flag. The optimized TagMap is designed to be good at operations that the tracer performs regularly but HashMap isn't great at. Specifically, Map-to-Map copies and storing primitives. To get the benefit of the optimized TagMap, calling code needs to use TagMap-s for both the source and destination map. The calling code also needs to make sure to use the bulk operations that which are the most optimized. To take advantage of TagMap in span creation, a mechanism was introduced that allows for bypassing TagInterceptors. Bypassing TagInterceptors is done by analyzing a TagMap that is going to be reused ahead-of-time to determine if interception is needed. If interception isn't needed, then SpanBuilder can use a bulk operation to update the map. To maintain the insertion order semantics of SpanBuilder, TagMap also includes a Ledger. A Ledger is concatenative ledger of entry modifications to a map A Ledger is now used in place of a LinkedHashMap in SpanBuilder to provide the insertion order semantics. Since these changes primarily serve to reduce allocation, they should the biggest gain in memory constrained environments. In memory constrained environments, these changes yield a 10% increase in sustainable throughput with spring-petclinic. --- .../domain/AbstractTestSession.java | 2 +- .../trace/civisibility/domain/TestImpl.java | 4 +- .../DefaultExceptionDebuggerTest.java | 4 +- .../sink/HstsMissingHeaderModuleTest.groovy | 5 +- .../InsecureAuthProtocolModuleTest.groovy | 5 +- .../sink/XContentTypeOptionsModuleTest.groovy | 9 +- .../appsec/AppSecSystemSpecification.groovy | 3 +- .../gateway/GatewayBridgeSpecification.groovy | 13 +- .../src/test/groovy/WebsocketTest.groovy | 3 +- .../trace/api/config/GeneralConfig.java | 1 + .../java/datadog/trace/core/CoreTracer.java | 204 +- .../main/java/datadog/trace/core/DDSpan.java | 3 +- .../datadog/trace/core/DDSpanContext.java | 91 +- .../java/datadog/trace/core/Metadata.java | 9 +- .../trace/core/propagation/B3HttpCodec.java | 11 +- .../core/propagation/ContextInterpreter.java | 30 +- .../core/propagation/ExtractedContext.java | 33 +- .../core/taginterceptor/TagInterceptor.java | 45 + .../core/tagprocessor/BaseServiceAdder.java | 9 +- .../core/tagprocessor/IntegrationAdder.java | 12 +- .../tagprocessor/PayloadTagsProcessor.java | 21 +- .../tagprocessor/PeerServiceCalculator.java | 19 +- .../core/tagprocessor/PostProcessorChain.java | 12 +- .../core/tagprocessor/QueryObfuscator.java | 14 +- .../tagprocessor/RemoteHostnameAdder.java | 9 +- .../tagprocessor/SpanPointersProcessor.java | 10 +- .../core/tagprocessor/TagsPostProcessor.java | 18 +- .../trace/common/writer/TraceGenerator.groovy | 3 +- .../trace/core/CoreSpanBuilderTest.groovy | 7 +- .../datadog/trace/core/DDSpanTest.groovy | 3 +- .../DefaultPathwayContextTest.groovy | 7 +- .../PostProcessorChainTest.groovy | 27 +- .../groovy/TraceGenerator.groovy | 5 +- .../main/java/datadog/trace/api/Config.java | 17 +- .../main/java/datadog/trace/api/TagMap.java | 3009 +++++++++++++++++ .../datadog/trace/api/gateway/IGSpanInfo.java | 4 +- .../trace/api/naming/NamingSchema.java | 7 +- .../api/naming/v0/PeerServiceNamingV0.java | 7 +- .../api/naming/v1/PeerServiceNamingV1.java | 17 +- .../instrumentation/api/AgentSpan.java | 4 + .../instrumentation/api/ExtractedSpan.java | 13 +- .../instrumentation/api/NoopSpan.java | 8 +- .../instrumentation/api/TagContext.java | 18 +- .../api/ExtractedSpanTest.groovy | 3 +- .../trace/api/TagMapBucketGroupTest.java | 382 +++ .../datadog/trace/api/TagMapEntryTest.java | 580 ++++ .../datadog/trace/api/TagMapFuzzTest.java | 1220 +++++++ .../datadog/trace/api/TagMapLedgerTest.java | 272 ++ .../java/datadog/trace/api/TagMapTest.java | 896 +++++ .../java/datadog/trace/api/TagMapType.java | 20 + .../datadog/trace/api/TagMapTypePair.java | 16 + 51 files changed, 6897 insertions(+), 247 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/TagMap.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapBucketGroupTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapEntryTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapLedgerTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapTest.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapType.java create mode 100644 internal-api/src/test/java/datadog/trace/api/TagMapTypePair.java diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/AbstractTestSession.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/AbstractTestSession.java index 51e9392732b..4338a9a7ba8 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/AbstractTestSession.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/AbstractTestSession.java @@ -73,7 +73,7 @@ public AbstractTestSession( AgentSpanContext traceContext = new TagContext( CIConstants.CIAPP_TEST_ORIGIN, - Collections.emptyMap(), + null, null, null, PrioritySampling.UNSET, diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java index 47042b7effa..a2d8d155ea6 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java @@ -46,7 +46,6 @@ import datadog.trace.civisibility.test.ExecutionResults; import java.lang.reflect.Method; import java.util.Collection; -import java.util.Collections; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.annotation.Nonnull; @@ -105,8 +104,7 @@ public TestImpl( this.context = new TestContextImpl(coverageStore); - AgentSpanContext traceContext = - new TagContext(CIConstants.CIAPP_TEST_ORIGIN, Collections.emptyMap()); + AgentSpanContext traceContext = new TagContext(CIConstants.CIAPP_TEST_ORIGIN, null); AgentTracer.SpanBuilder spanBuilder = AgentTracer.get() .buildSpan(CI_VISIBILITY_INSTRUMENTATION_NAME, testDecorator.component() + ".test") diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/DefaultExceptionDebuggerTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/DefaultExceptionDebuggerTest.java index a399fee533b..09dd8917dd6 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/DefaultExceptionDebuggerTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/DefaultExceptionDebuggerTest.java @@ -27,6 +27,7 @@ import com.datadog.debugger.util.ExceptionHelper; import com.datadog.debugger.util.TestSnapshotListener; import datadog.trace.api.Config; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.debugger.CapturedContext; import datadog.trace.bootstrap.debugger.CapturedStackFrame; import datadog.trace.bootstrap.debugger.MethodLocation; @@ -41,7 +42,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -57,7 +57,7 @@ public class DefaultExceptionDebuggerTest { private ConfigurationUpdater configurationUpdater; private DefaultExceptionDebugger exceptionDebugger; private TestSnapshotListener listener; - private Map spanTags = new HashMap<>(); + private TagMap spanTags = TagMap.create(); @BeforeEach public void setUp() { diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy index e86040d9cc2..54a9e744dba 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/HstsMissingHeaderModuleTest.groovy @@ -7,6 +7,7 @@ import com.datadog.iast.model.Vulnerability import com.datadog.iast.model.VulnerabilityType import com.datadog.iast.overhead.Operation import com.datadog.iast.overhead.OverheadController +import datadog.trace.api.TagMap import datadog.trace.api.gateway.Flow import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.internal.TraceSegment @@ -45,10 +46,10 @@ class HstsMissingHeaderModuleTest extends IastModuleImplTestBase { final handler = new RequestEndedHandler(dependencies) ctx.xForwardedProto = 'https' ctx.contentType = "text/html" - span.getTags() >> [ + span.getTags() >> TagMap.fromMap([ 'http.url': 'https://localhost/a', 'http.status_code': 200i - ] + ]) when: def flow = handler.apply(reqCtx, span) diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/InsecureAuthProtocolModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/InsecureAuthProtocolModuleTest.groovy index 882f533e6f7..646778e34b7 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/InsecureAuthProtocolModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/InsecureAuthProtocolModuleTest.groovy @@ -5,6 +5,7 @@ import com.datadog.iast.Reporter import com.datadog.iast.RequestEndedHandler import com.datadog.iast.model.Vulnerability import com.datadog.iast.model.VulnerabilityType +import datadog.trace.api.TagMap import datadog.trace.api.gateway.Flow import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.InsecureAuthProtocolModule @@ -42,9 +43,9 @@ class InsecureAuthProtocolModuleTest extends IastModuleImplTestBase{ given: final handler = new RequestEndedHandler(dependencies) ctx.authorization = value - span.getTags() >> [ + span.getTags() >> TagMap.fromMap([ 'http.status_code': status_code - ] + ]) when: def flow = handler.apply(reqCtx, span) diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/XContentTypeOptionsModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/XContentTypeOptionsModuleTest.groovy index 7224ca49a24..c7f0eae4198 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/XContentTypeOptionsModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/XContentTypeOptionsModuleTest.groovy @@ -5,6 +5,7 @@ import com.datadog.iast.Reporter import com.datadog.iast.RequestEndedHandler import com.datadog.iast.model.Vulnerability import com.datadog.iast.model.VulnerabilityType +import datadog.trace.api.TagMap import datadog.trace.api.gateway.Flow import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.internal.TraceSegment @@ -33,9 +34,9 @@ class XContentTypeOptionsModuleTest extends IastModuleImplTestBase { given: final handler = new RequestEndedHandler(dependencies) ctx.contentType = "text/html" - span.getTags() >> [ + span.getTags() >> TagMap.fromMap([ 'http.status_code': 200i - ] + ]) when: def flow = handler.apply(reqCtx, span) @@ -56,10 +57,10 @@ class XContentTypeOptionsModuleTest extends IastModuleImplTestBase { final handler = new RequestEndedHandler(dependencies) ctx.xForwardedProto = 'https' ctx.contentType = "text/html" - span.getTags() >> [ + span.getTags() >> TagMap.fromMap([ 'http.url': url, 'http.status_code': status - ] + ]) when: def flow = handler.apply(reqCtx, span) diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy index 110458c1be2..0f00d917100 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy @@ -13,6 +13,7 @@ import datadog.remoteconfig.Product import datadog.remoteconfig.state.ConfigKey import datadog.remoteconfig.state.ProductListener import datadog.trace.api.Config +import datadog.trace.api.TagMap import datadog.trace.api.gateway.Flow import datadog.trace.api.gateway.IGSpanInfo import datadog.trace.api.gateway.RequestContext @@ -91,7 +92,7 @@ class AppSecSystemSpecification extends DDSpecification { requestEndedCB.apply(requestContext, span) then: - 1 * span.getTags() >> ['http.client_ip':'1.1.1.1'] + 1 * span.getTags() >> TagMap.fromMap(['http.client_ip':'1.1.1.1']) 1 * subService.registerCallback(EVENTS.requestEnded(), _) >> { requestEndedCB = it[1]; null } 1 * requestContext.getData(RequestContextSlot.APPSEC) >> appSecReqCtx 1 * requestContext.traceSegment >> traceSegment diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy index 38eaf9f1208..9de9f59f22c 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy @@ -9,6 +9,7 @@ import com.datadog.appsec.event.data.DataBundle import com.datadog.appsec.event.data.KnownAddresses import com.datadog.appsec.report.AppSecEvent import com.datadog.appsec.report.AppSecEventWrapper +import datadog.trace.api.TagMap import datadog.trace.api.ProductTraceSource import datadog.trace.api.config.GeneralConfig import static datadog.trace.api.config.IastConfig.IAST_DEDUPLICATION_ENABLED @@ -173,7 +174,7 @@ class GatewayBridgeSpecification extends DDSpecification { def flow = requestEndedCB.apply(mockCtx, spanInfo) then: - 1 * spanInfo.getTags() >> ['http.client_ip': '1.1.1.1'] + 1 * spanInfo.getTags() >> TagMap.fromMap(['http.client_ip': '1.1.1.1']) 1 * mockAppSecCtx.transferCollectedEvents() >> [event] 1 * mockAppSecCtx.peerAddress >> '2001::1' 1 * mockAppSecCtx.close() @@ -212,7 +213,7 @@ class GatewayBridgeSpecification extends DDSpecification { then: 1 * mockAppSecCtx.transferCollectedEvents() >> [Stub(AppSecEvent)] - 1 * spanInfo.getTags() >> ['http.client_ip': '8.8.8.8'] + 1 * spanInfo.getTags() >> TagMap.fromMap(['http.client_ip': '8.8.8.8']) 1 * traceSegment.setTagTop('actor.ip', '8.8.8.8') } @@ -1008,7 +1009,7 @@ class GatewayBridgeSpecification extends DDSpecification { getTraceSegment() >> traceSegment } final spanInfo = Mock(AgentSpan) { - getTags() >> ['http.route':'/'] + getTags() >> TagMap.fromMap(['http.route':'/']) } when: @@ -1196,7 +1197,7 @@ class GatewayBridgeSpecification extends DDSpecification { def flow = requestEndedCB.apply(mockCtx, spanInfo) then: 1 * mockAppSecCtx.transferCollectedEvents() >> [] - 1 * spanInfo.getTags() >> ['http.route': 'route'] + 1 * spanInfo.getTags() >> TagMap.fromMap(['http.route': 'route']) 1 * requestSampler.preSampleRequest(_) >> true 0 * traceSegment.setTagTop(Tags.ASM_KEEP, true) 0 * traceSegment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM) @@ -1214,7 +1215,7 @@ class GatewayBridgeSpecification extends DDSpecification { def flow = requestEndedCB.apply(mockCtx, spanInfo) then: 1 * mockAppSecCtx.transferCollectedEvents() >> [] - 1 * spanInfo.getTags() >> ['http.route': 'route'] + 1 * spanInfo.getTags() >> TagMap.fromMap(['http.route': 'route']) 1 * requestSampler.preSampleRequest(_) >> false 0 * traceSegment.setTagTop(Tags.ASM_KEEP, true) 0 * traceSegment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM) @@ -1233,7 +1234,7 @@ class GatewayBridgeSpecification extends DDSpecification { def flow = requestEndedCB.apply(mockCtx, spanInfo) then: 1 * mockAppSecCtx.transferCollectedEvents() >> [] - 1 * spanInfo.getTags() >> ['http.route': 'route'] + 1 * spanInfo.getTags() >> TagMap.fromMap(['http.route': 'route']) 1 * requestSampler.preSampleRequest(_) >> true 1 * traceSegment.setTagTop(Tags.ASM_KEEP, true) 1 * traceSegment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM) diff --git a/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/test/groovy/WebsocketTest.groovy b/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/test/groovy/WebsocketTest.groovy index eff2cb53483..b478d5ea8af 100644 --- a/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/test/groovy/WebsocketTest.groovy +++ b/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/test/groovy/WebsocketTest.groovy @@ -1,5 +1,6 @@ import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.DDTags +import datadog.trace.api.TagMap import datadog.trace.api.sampling.PrioritySampling import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags @@ -584,7 +585,7 @@ class WebsocketTest extends AgentTestRunner { clientHandshake.setSamplingPriority(PrioritySampling.SAMPLER_DROP) // simulate sampler drop def serverHandshake = createHandshakeSpan("servlet.request", url, new ExtractedContext(clientHandshake.context().getTraceId(), clientHandshake.context().getSpanId(), clientHandshake.context().getSamplingPriority(), - "test", 0, ["example_baggage": "test"], null, null, null, null, null)) // simulate server span + "test", 0, ["example_baggage": "test"], TagMap.EMPTY, null, null, null, null)) // simulate server span def session = deployEndpointAndConnect(new Endpoints.TestEndpoint(new Endpoints.FullStringHandler()), clientHandshake, serverHandshake, url) diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java index 74a928245e6..6d2e8eda4e6 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java @@ -103,6 +103,7 @@ public final class GeneralConfig { public static final String APM_TRACING_ENABLED = "apm.tracing.enabled"; public static final String JDK_SOCKET_ENABLED = "jdk.socket.enabled"; + public static final String OPTIMIZED_MAP_ENABLED = "optimized.map.enabled"; public static final String STACK_TRACE_LENGTH_LIMIT = "stack.trace.length.limit"; public static final String SSI_INJECTION_ENABLED = "injection.enabled"; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 28ba0832e30..9e835cd9e7c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -31,6 +31,7 @@ import datadog.trace.api.EndpointTracker; import datadog.trace.api.IdGenerationStrategy; import datadog.trace.api.StatsDClient; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.datastreams.AgentDataStreamsMonitoring; @@ -105,7 +106,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -187,10 +187,14 @@ public static CoreTracerBuilder builder() { private final DynamicConfig dynamicConfig; /** A set of tags that are added only to the application's root span */ - private final Map localRootSpanTags; + private final TagMap localRootSpanTags; + + private final boolean localRootSpanTagsNeedIntercept; /** A set of tags that are added to every span */ - private final Map defaultSpanTags; + private final TagMap defaultSpanTags; + + private final boolean defaultSpanTagsNeedsIntercept; /** number of spans in a pending trace before they get flushed */ private final int partialFlushMinSpans; @@ -304,8 +308,8 @@ public static class CoreTracerBuilder { private SingleSpanSampler singleSpanSampler; private HttpCodec.Injector injector; private HttpCodec.Extractor extractor; - private Map localRootSpanTags; - private Map defaultSpanTags; + private TagMap localRootSpanTags; + private TagMap defaultSpanTags; private Map serviceNameMappings; private Map taggedHeaders; private Map baggageMapping; @@ -365,12 +369,22 @@ public CoreTracerBuilder extractor(HttpCodec.Extractor extractor) { } public CoreTracerBuilder localRootSpanTags(Map localRootSpanTags) { - this.localRootSpanTags = tryMakeImmutableMap(localRootSpanTags); + this.localRootSpanTags = TagMap.fromMapImmutable(localRootSpanTags); + return this; + } + + public CoreTracerBuilder localRootSpanTags(TagMap tagMap) { + this.localRootSpanTags = tagMap.immutableCopy(); return this; } public CoreTracerBuilder defaultSpanTags(Map defaultSpanTags) { - this.defaultSpanTags = tryMakeImmutableMap(defaultSpanTags); + this.defaultSpanTags = TagMap.fromMapImmutable(defaultSpanTags); + return this; + } + + public CoreTracerBuilder defaultSpanTags(TagMap defaultSpanTags) { + this.defaultSpanTags = defaultSpanTags.immutableCopy(); return this; } @@ -520,6 +534,63 @@ public CoreTracer build() { } } + @Deprecated + private CoreTracer( + final Config config, + final String serviceName, + SharedCommunicationObjects sharedCommunicationObjects, + final Writer writer, + final IdGenerationStrategy idGenerationStrategy, + final Sampler sampler, + final SingleSpanSampler singleSpanSampler, + final HttpCodec.Injector injector, + final HttpCodec.Extractor extractor, + final Map localRootSpanTags, + final TagMap defaultSpanTags, + final Map serviceNameMappings, + final Map taggedHeaders, + final Map baggageMapping, + final int partialFlushMinSpans, + final StatsDClient statsDClient, + final TagInterceptor tagInterceptor, + final boolean strictTraceWrites, + final InstrumentationGateway instrumentationGateway, + final TimeSource timeSource, + final DataStreamsMonitoring dataStreamsMonitoring, + final ProfilingContextIntegration profilingContextIntegration, + final boolean pollForTracerFlareRequests, + final boolean pollForTracingConfiguration, + final boolean injectBaggageAsTags, + final boolean flushOnClose) { + this( + config, + serviceName, + sharedCommunicationObjects, + writer, + idGenerationStrategy, + sampler, + singleSpanSampler, + injector, + extractor, + TagMap.fromMap(localRootSpanTags), + defaultSpanTags, + serviceNameMappings, + taggedHeaders, + baggageMapping, + partialFlushMinSpans, + statsDClient, + tagInterceptor, + strictTraceWrites, + instrumentationGateway, + timeSource, + dataStreamsMonitoring, + profilingContextIntegration, + pollForTracerFlareRequests, + pollForTracingConfiguration, + injectBaggageAsTags, + flushOnClose); + } + // These field names must be stable to ensure the builder api is stable. private CoreTracer( final Config config, @@ -531,8 +602,8 @@ private CoreTracer( final SingleSpanSampler singleSpanSampler, final HttpCodec.Injector injector, final HttpCodec.Extractor extractor, - final Map localRootSpanTags, - final Map defaultSpanTags, + final TagMap localRootSpanTags, + final TagMap defaultSpanTags, final Map serviceNameMappings, final Map taggedHeaders, final Map baggageMapping, @@ -585,7 +656,11 @@ private CoreTracer( spanSamplingRules = SpanSamplingRules.deserializeFile(spanSamplingRulesFile); } + this.tagInterceptor = + null == tagInterceptor ? new TagInterceptor(new RuleFlags(config)) : tagInterceptor; + this.defaultSpanTags = defaultSpanTags; + this.defaultSpanTagsNeedsIntercept = this.tagInterceptor.needsIntercept(this.defaultSpanTags); this.dynamicConfig = DynamicConfig.create(ConfigSnapshot::new) @@ -721,9 +796,6 @@ private CoreTracer( Propagators.register(BAGGAGE_CONCERN, new BaggagePropagator(config)); } - this.tagInterceptor = - null == tagInterceptor ? new TagInterceptor(new RuleFlags(config)) : tagInterceptor; - if (config.isCiVisibilityEnabled()) { if (config.isCiVisibilityTraceSanitationEnabled()) { addTraceInterceptor(CiVisibilityTraceInterceptor.INSTANCE); @@ -775,12 +847,15 @@ private CoreTracer( this.flushOnClose = flushOnClose; this.allowInferredServices = SpanNaming.instance().namingSchema().allowInferredServices(); if (profilingContextIntegration != ProfilingContextIntegration.NoOp.INSTANCE) { - Map tmp = new HashMap<>(localRootSpanTags); + TagMap tmp = TagMap.fromMap(localRootSpanTags); tmp.put(PROFILING_CONTEXT_ENGINE, profilingContextIntegration.name()); - this.localRootSpanTags = tryMakeImmutableMap(tmp); + this.localRootSpanTags = tmp.freeze(); } else { - this.localRootSpanTags = localRootSpanTags; + this.localRootSpanTags = TagMap.fromMapImmutable(localRootSpanTags); } + + this.localRootSpanTagsNeedIntercept = + this.tagInterceptor.needsIntercept(this.localRootSpanTags); } /** Used by AgentTestRunner to inject configuration into the test tracer. */ @@ -869,7 +944,7 @@ long getTimeWithNanoTicks(long nanoTicks) { @Override public CoreSpanBuilder buildSpan( final String instrumentationName, final CharSequence operationName) { - return new CoreSpanBuilder(instrumentationName, operationName, this); + return new CoreSpanBuilder(this, instrumentationName, operationName); } @Override @@ -1293,13 +1368,13 @@ private static Map invertMap(Map map) { } /** Spans are built using this builder */ - public class CoreSpanBuilder implements AgentTracer.SpanBuilder { + public static class CoreSpanBuilder implements AgentTracer.SpanBuilder { private final String instrumentationName; private final CharSequence operationName; private final CoreTracer tracer; // Builder attributes - private Map tags; + private TagMap.Ledger tagLedger; private long timestampMicro; private AgentSpanContext parent; private String serviceName; @@ -1314,7 +1389,9 @@ public class CoreSpanBuilder implements AgentTracer.SpanBuilder { private long spanId; CoreSpanBuilder( - final String instrumentationName, final CharSequence operationName, CoreTracer tracer) { + final CoreTracer tracer, + final String instrumentationName, + final CharSequence operationName) { this.instrumentationName = instrumentationName; this.operationName = operationName; this.tracer = tracer; @@ -1370,7 +1447,7 @@ private void addTerminatedContextAsLinks() { public AgentSpan start() { AgentSpanContext pc = parent; if (pc == null && !ignoreScope) { - final AgentSpan span = activeSpan(); + final AgentSpan span = tracer.activeSpan(); if (span != null) { pc = span.context(); } @@ -1445,14 +1522,21 @@ public CoreSpanBuilder withTag(final String tag, final Object value) { if (tag == null) { return this; } - Map tagMap = tags; - if (tagMap == null) { - tags = tagMap = new LinkedHashMap<>(); // Insertion order is important + TagMap.Ledger tagLedger = this.tagLedger; + if (tagLedger == null) { + // Insertion order is important, so using TagLedger which builds up a set + // of Entry modifications in order + this.tagLedger = tagLedger = TagMap.ledger(); } if (value == null) { - tagMap.remove(tag); + // DQH - Use of smartRemove is important to avoid clobbering entries added by another map + // smartRemove only records the removal if a prior matching put has already occurred in the + // ledger + // smartRemove is O(n) but since removes are rare, this is preferable to a more complicated + // implementation in setAll + tagLedger.smartRemove(tag); } else { - tagMap.put(tag, value); + tagLedger.set(tag, value); } return this; } @@ -1504,9 +1588,10 @@ private DDSpanContext buildSpanContext() { final TraceCollector parentTraceCollector; final int samplingPriority; final CharSequence origin; - final Map coreTags; - final Map rootSpanTags; - + final TagMap coreTags; + final boolean coreTagsNeedsIntercept; + final TagMap rootSpanTags; + final boolean rootSpanTagsNeedsIntercept; final DDSpanContext context; Object requestContextDataAppSec; Object requestContextDataIast; @@ -1515,7 +1600,7 @@ private DDSpanContext buildSpanContext() { final PropagationTags propagationTags; if (this.spanId == 0) { - spanId = idGenerationStrategy.generateSpanId(); + spanId = tracer.idGenerationStrategy.generateSpanId(); } else { spanId = this.spanId; } @@ -1524,7 +1609,7 @@ private DDSpanContext buildSpanContext() { AgentSpanContext parentContext = parent; if (parentContext == null && !ignoreScope) { // use the Scope as parent unless overridden or ignored. - final AgentSpan activeSpan = scopeManager.activeSpan(); + final AgentSpan activeSpan = tracer.scopeManager.activeSpan(); if (activeSpan != null) { parentContext = activeSpan.context(); } @@ -1558,7 +1643,9 @@ private DDSpanContext buildSpanContext() { samplingPriority = PrioritySampling.UNSET; origin = null; coreTags = null; + coreTagsNeedsIntercept = false; rootSpanTags = null; + rootSpanTagsNeedsIntercept = false; parentServiceName = ddsc.getServiceName(); if (serviceName == null) { serviceName = parentServiceName; @@ -1573,7 +1660,7 @@ private DDSpanContext buildSpanContext() { requestContextDataIast = null; ciVisibilityContextData = null; } - propagationTags = propagationTagsFactory.empty(); + propagationTags = tracer.propagationTagsFactory.empty(); } else { long endToEndStartTime; @@ -1588,19 +1675,19 @@ private DDSpanContext buildSpanContext() { } else if (parentContext != null) { traceId = parentContext.getTraceId() == DDTraceId.ZERO - ? idGenerationStrategy.generateTraceId() + ? tracer.idGenerationStrategy.generateTraceId() : parentContext.getTraceId(); parentSpanId = parentContext.getSpanId(); samplingPriority = parentContext.getSamplingPriority(); endToEndStartTime = 0; - propagationTags = propagationTagsFactory.empty(); + propagationTags = tracer.propagationTagsFactory.empty(); } else { // Start a new trace - traceId = idGenerationStrategy.generateTraceId(); + traceId = tracer.idGenerationStrategy.generateTraceId(); parentSpanId = DDSpanId.ZERO; samplingPriority = PrioritySampling.UNSET; endToEndStartTime = 0; - propagationTags = propagationTagsFactory.empty(); + propagationTags = tracer.propagationTagsFactory.empty(); } ConfigSnapshot traceConfig; @@ -1610,6 +1697,7 @@ private DDSpanContext buildSpanContext() { TagContext tc = (TagContext) parentContext; traceConfig = (ConfigSnapshot) tc.getTraceConfig(); coreTags = tc.getTags(); + coreTagsNeedsIntercept = true; // maybe intercept isn't needed? origin = tc.getOrigin(); baggage = tc.getBaggage(); requestContextDataAppSec = tc.getRequestContextDataAppSec(); @@ -1618,6 +1706,7 @@ private DDSpanContext buildSpanContext() { } else { traceConfig = null; coreTags = null; + coreTagsNeedsIntercept = false; origin = null; baggage = null; requestContextDataAppSec = null; @@ -1625,9 +1714,10 @@ private DDSpanContext buildSpanContext() { ciVisibilityContextData = null; } - rootSpanTags = localRootSpanTags; + rootSpanTags = tracer.localRootSpanTags; + rootSpanTagsNeedsIntercept = tracer.localRootSpanTagsNeedIntercept; - parentTraceCollector = createTraceCollector(traceId, traceConfig); + parentTraceCollector = tracer.createTraceCollector(traceId, traceConfig); if (endToEndStartTime > 0) { parentTraceCollector.beginEndToEnd(endToEndStartTime); @@ -1642,11 +1732,11 @@ private DDSpanContext buildSpanContext() { && parentContext.getPathwayContext() != null && parentContext.getPathwayContext().isStarted() ? parentContext.getPathwayContext() - : dataStreamsMonitoring.newPathwayContext(); + : tracer.dataStreamsMonitoring.newPathwayContext(); // when removing fake services the best upward service name to pick is the local root one // since a split by tag (i.e. servlet context) might have happened on it. - if (!allowInferredServices) { + if (!tracer.allowInferredServices) { final DDSpan rootSpan = parentTraceCollector.getRootSpan(); serviceName = rootSpan != null ? rootSpan.getServiceName() : null; } @@ -1670,17 +1760,18 @@ private DDSpanContext buildSpanContext() { if (serviceName == null) { // it could be on the initial snapshot but may be overridden to null and service name // cannot be null - serviceName = CoreTracer.this.serviceName; + serviceName = tracer.serviceName; } final CharSequence operationName = this.operationName != null ? this.operationName : resourceName; - final Map mergedTracerTags = traceConfig.mergedTracerTags; + final TagMap mergedTracerTags = traceConfig.mergedTracerTags; + boolean mergedTracerTagsNeedsIntercept = traceConfig.mergedTracerTagsNeedsIntercept; final int tagsSize = mergedTracerTags.size() - + (null == tags ? 0 : tags.size()) + + (null == tagLedger ? 0 : tagLedger.estimateSize()) + (null == coreTags ? 0 : coreTags.size()) + (null == rootSpanTags ? 0 : rootSpanTags.size()) + (null == contextualTags ? 0 : contextualTags.size()); @@ -1716,18 +1807,18 @@ private DDSpanContext buildSpanContext() { requestContextDataIast, ciVisibilityContextData, pathwayContext, - disableSamplingMechanismValidation, + tracer.disableSamplingMechanismValidation, propagationTags, - profilingContextIntegration, - injectBaggageAsTags); + tracer.profilingContextIntegration, + tracer.injectBaggageAsTags); // By setting the tags on the context we apply decorators to any tags that have been set via // the builder. This is the order that the tags were added previously, but maybe the `tags` // set in the builder should come last, so that they override other tags. - context.setAllTags(mergedTracerTags); - context.setAllTags(tags); - context.setAllTags(coreTags); - context.setAllTags(rootSpanTags); + context.setAllTags(mergedTracerTags, mergedTracerTagsNeedsIntercept); + context.setAllTags(tagLedger); + context.setAllTags(coreTags, coreTagsNeedsIntercept); + context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept); context.setAllTags(contextualTags); return context; } @@ -1753,7 +1844,8 @@ public void run() { protected class ConfigSnapshot extends DynamicConfig.Snapshot { final Sampler sampler; - final Map mergedTracerTags; + final TagMap mergedTracerTags; + final boolean mergedTracerTagsNeedsIntercept; protected ConfigSnapshot( DynamicConfig.Builder builder, ConfigSnapshot oldSnapshot) { @@ -1769,11 +1861,15 @@ protected ConfigSnapshot( } if (null == oldSnapshot) { - mergedTracerTags = CoreTracer.this.defaultSpanTags; + mergedTracerTags = CoreTracer.this.defaultSpanTags.immutableCopy(); + this.mergedTracerTagsNeedsIntercept = CoreTracer.this.defaultSpanTagsNeedsIntercept; } else if (getTracingTags().equals(oldSnapshot.getTracingTags())) { mergedTracerTags = oldSnapshot.mergedTracerTags; + mergedTracerTagsNeedsIntercept = oldSnapshot.mergedTracerTagsNeedsIntercept; } else { mergedTracerTags = withTracerTags(getTracingTags(), CoreTracer.this.initialConfig, this); + mergedTracerTagsNeedsIntercept = + CoreTracer.this.tagInterceptor.needsIntercept(mergedTracerTags); } } } @@ -1781,9 +1877,9 @@ protected ConfigSnapshot( /** * Tags added by the tracer to all spans; combines user-supplied tags with tracer-defined tags. */ - static Map withTracerTags( + static TagMap withTracerTags( Map userSpanTags, Config config, TraceConfig traceConfig) { - final Map result = new HashMap<>(userSpanTags.size() + 5, 1f); + final TagMap result = TagMap.create(userSpanTags.size() + 5); result.putAll(userSpanTags); if (null != config) { // static if (!config.getEnv().isEmpty()) { @@ -1806,6 +1902,6 @@ protected ConfigSnapshot( result.remove(DSM_ENABLED); } } - return Collections.unmodifiableMap(result); + return result.freeze(); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index a150f5246cf..2c282f15a57 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -13,6 +13,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.EndpointTracker; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.gateway.Flow; import datadog.trace.api.gateway.RequestContext; @@ -692,7 +693,7 @@ public String getSpanType() { } @Override - public Map getTags() { + public TagMap getTags() { // This is an imutable copy of the tags return context.getTags(); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 53697ca8351..482d1efe09e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -10,6 +10,7 @@ import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; import datadog.trace.api.ProcessTags; +import datadog.trace.api.TagMap; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; import datadog.trace.api.config.TracerConfig; @@ -98,7 +99,7 @@ public class DDSpanContext * rather read and accessed in a serial fashion on thread after thread. The synchronization can * then be wrapped around bulk operations to minimize the costly atomic operations. */ - private final Map unsafeTags; + private final TagMap unsafeTags; /** The service name is required, otherwise the span are dropped by the agent */ private volatile String serviceName; @@ -341,7 +342,8 @@ public DDSpanContext( // The +1 is the magic number from the tags below that we set at the end, // and "* 4 / 3" is to make sure that we don't resize immediately final int capacity = Math.max((tagsSize <= 0 ? 3 : (tagsSize + 1)) * 4 / 3, 8); - this.unsafeTags = new HashMap<>(capacity); + this.unsafeTags = TagMap.create(capacity); + // must set this before setting the service and resource names below this.profilingContextIntegration = profilingContextIntegration; // as fast as we can try to make this operation, we still might need to activate/deactivate @@ -757,16 +759,73 @@ public void setTag(final String tag, final Object value) { } } - void setAllTags(final Map map) { - if (map == null || map.isEmpty()) { + void setAllTags(final TagMap map) { + setAllTags(map, true); + } + + void setAllTags(final TagMap map, boolean needsIntercept) { + if (map == null) { + return; + } + + synchronized (unsafeTags) { + if (needsIntercept) { + // forEach out-performs the iterator of TagMap + // Taking advantage of ability to pass through other context arguments + // to avoid using a capturing lambda + map.forEach( + this, + traceCollector.getTracer().getTagInterceptor(), + (ctx, tagInterceptor, tagEntry) -> { + String tag = tagEntry.tag(); + Object value = tagEntry.objectValue(); + + if (!tagInterceptor.interceptTag(ctx, tag, value)) { + ctx.unsafeTags.set(tagEntry); + } + }); + } else { + unsafeTags.putAll(map); + } + } + } + + void setAllTags(final TagMap.Ledger ledger) { + if (ledger == null) { return; } TagInterceptor tagInterceptor = traceCollector.getTracer().getTagInterceptor(); synchronized (unsafeTags) { - for (final Map.Entry tag : map.entrySet()) { - if (!tagInterceptor.interceptTag(this, tag.getKey(), tag.getValue())) { - unsafeSetTag(tag.getKey(), tag.getValue()); + for (final TagMap.EntryChange entryChange : ledger) { + if (entryChange.isRemoval()) { + unsafeTags.remove(entryChange.tag()); + } else { + TagMap.Entry entry = (TagMap.Entry) entryChange; + + String tag = entry.tag(); + Object value = entry.objectValue(); + + if (!tagInterceptor.interceptTag(this, tag, value)) { + unsafeTags.set(entry); + } + } + } + } + } + + void setAllTags(final Map map) { + if (map == null) { + return; + } else if (map instanceof TagMap) { + setAllTags((TagMap) map); + } else if (!map.isEmpty()) { + TagInterceptor tagInterceptor = traceCollector.getTracer().getTagInterceptor(); + synchronized (unsafeTags) { + for (final Map.Entry tag : map.entrySet()) { + if (!tagInterceptor.interceptTag(this, tag.getKey(), tag.getValue())) { + unsafeSetTag(tag.getKey(), tag.getValue()); + } } } } @@ -803,12 +862,14 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return unsafeTags.get(tag); + return unsafeTags.getObject(tag); } - public Map getTags() { + @Deprecated + public TagMap getTags() { synchronized (unsafeTags) { - Map tags = new HashMap<>(unsafeTags); + TagMap tags = unsafeTags.copy(); + tags.put(DDTags.THREAD_ID, threadId); // maintain previously observable type of the thread name :| tags.put(DDTags.THREAD_NAME, threadName.toString()); @@ -823,7 +884,7 @@ public Map getTags() { if (value != null) { tags.put(Tags.HTTP_URL, value.toString()); } - return Collections.unmodifiableMap(tags); + return tags.freeze(); } } @@ -855,11 +916,11 @@ public void processTagsAndBaggage( final MetadataConsumer consumer, int longRunningVersion, List links) { synchronized (unsafeTags) { // Tags - Map tags = - TagsPostProcessorFactory.instance().processTags(unsafeTags, this, links); + TagsPostProcessorFactory.instance().processTags(unsafeTags, this, links); + String linksTag = DDSpanLink.toTag(links); if (linksTag != null) { - tags.put(SPAN_LINKS, linksTag); + unsafeTags.put(SPAN_LINKS, linksTag); } // Baggage Map baggageItemsWithPropagationTags; @@ -874,7 +935,7 @@ public void processTagsAndBaggage( new Metadata( threadId, threadName, - tags, + unsafeTags, baggageItemsWithPropagationTags, samplingPriority != PrioritySampling.UNSET ? samplingPriority : getSamplingPriority(), measured, diff --git a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java index 01054a91638..d116d19f77f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java @@ -2,6 +2,7 @@ import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.util.Map; @@ -9,7 +10,7 @@ public final class Metadata { private final long threadId; private final UTF8BytesString threadName; private final UTF8BytesString httpStatusCode; - private final Map tags; + private final TagMap tags; private final Map baggage; private final int samplingPriority; @@ -22,7 +23,7 @@ public final class Metadata { public Metadata( long threadId, UTF8BytesString threadName, - Map tags, + TagMap tags, Map baggage, int samplingPriority, boolean measured, @@ -60,8 +61,8 @@ public UTF8BytesString getThreadName() { return threadName; } - public Map getTags() { - return tags; + public TagMap getTags() { + return this.tags; } public Map getBaggage() { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/B3HttpCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/B3HttpCodec.java index 910449b7dd1..ef807f6b080 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/B3HttpCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/B3HttpCodec.java @@ -16,7 +16,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.TreeMap; import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -186,10 +185,7 @@ public B3BaseContextInterpreter(Config config) { protected void setSpanId(final String sId) { spanId = DDSpanId.fromHex(sId); - if (tags.isEmpty()) { - tags = new TreeMap<>(); - } - tags.put(B3_SPAN_ID, sId); + tagLedger().set(B3_SPAN_ID, sId); } protected boolean setTraceId(final String tId) { @@ -202,10 +198,7 @@ protected boolean setTraceId(final String tId) { B3TraceId b3TraceId = B3TraceId.fromHex(tId); traceId = b3TraceId.toLong() == 0 ? DDTraceId.ZERO : b3TraceId; } - if (tags.isEmpty()) { - tags = new TreeMap<>(); - } - tags.put(B3_TRACE_ID, tId); + tagLedger().set(B3_TRACE_ID, tId); return true; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java index d3466f76d8b..e94097db2a4 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ContextInterpreter.java @@ -19,6 +19,7 @@ import datadog.trace.api.DDSpanId; import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.TracePropagationStyle; import datadog.trace.api.cache.DDCache; @@ -44,7 +45,7 @@ public abstract class ContextInterpreter implements AgentPropagation.KeyClassifi protected DDTraceId traceId; protected long spanId; protected int samplingPriority; - protected Map tags; + protected TagMap.Ledger tagLedger; protected Map baggage; protected CharSequence lastParentId; @@ -77,6 +78,13 @@ protected ContextInterpreter(Config config) { this.requestHeaderTagsCommaAllowed = config.isRequestHeaderTagsCommaAllowed(); } + final TagMap.Ledger tagLedger() { + if (tagLedger == null) { + tagLedger = TagMap.ledger(); + } + return tagLedger; + } + /** * Gets the propagation style handled by the context interpreter. * @@ -189,13 +197,11 @@ protected final boolean handleTags(String key, String value) { final String lowerCaseKey = toLowerCase(key); final String mappedKey = headerTags.get(lowerCaseKey); if (null != mappedKey) { - if (tags.isEmpty()) { - tags = new TreeMap<>(); - } - tags.put( - mappedKey, - HttpCodec.decode( - requestHeaderTagsCommaAllowed ? value : HttpCodec.firstHeaderValue(value))); + tagLedger() + .set( + mappedKey, + HttpCodec.decode( + requestHeaderTagsCommaAllowed ? value : HttpCodec.firstHeaderValue(value))); return true; } return false; @@ -224,7 +230,7 @@ public ContextInterpreter reset(TraceConfig traceConfig) { samplingPriority = PrioritySampling.UNSET; origin = null; endToEndStartTime = 0; - tags = Collections.emptyMap(); + if (tagLedger != null) tagLedger.reset(); baggage = Collections.emptyMap(); valid = true; fullContext = true; @@ -252,19 +258,19 @@ protected TagContext build() { origin, endToEndStartTime, baggage, - tags, + tagLedger == null ? null : tagLedger.build(), httpHeaders, propagationTags, traceConfig, style()); } else if (origin != null - || !tags.isEmpty() + || (tagLedger != null && !tagLedger.isDefinitelyEmpty()) || httpHeaders != null || !baggage.isEmpty() || samplingPriority != PrioritySampling.UNSET) { return new TagContext( origin, - tags, + tagLedger == null ? null : tagLedger.build(), httpHeaders, baggage, samplingPriorityOrDefault(traceId, samplingPriority), diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ExtractedContext.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ExtractedContext.java index e799688401d..af503e6a6ed 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ExtractedContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ExtractedContext.java @@ -1,6 +1,7 @@ package datadog.trace.core.propagation; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.TracePropagationStyle; import datadog.trace.api.sampling.PrioritySampling; @@ -44,7 +45,7 @@ public ExtractedContext( final CharSequence origin, final long endToEndStartTime, final Map baggage, - final Map tags, + final TagMap tags, final HttpHeaders httpHeaders, final PropagationTags propagationTags, final TraceConfig traceConfig, @@ -64,6 +65,36 @@ public ExtractedContext( this.propagationTags = propagationTags; } + /* + * DQH - kept for testing purposes only + */ + @Deprecated + public ExtractedContext( + final DDTraceId traceId, + final long spanId, + final int samplingPriority, + final CharSequence origin, + final long endToEndStartTime, + final Map baggage, + final Map tags, + final HttpHeaders httpHeaders, + final PropagationTags propagationTags, + final TraceConfig traceConfig, + final TracePropagationStyle propagationStyle) { + this( + traceId, + spanId, + samplingPriority, + origin, + endToEndStartTime, + baggage, + tags == null ? null : TagMap.fromMap(tags), + httpHeaders, + propagationTags, + traceConfig, + propagationStyle); + } + @Override public final DDTraceId getTraceId() { return traceId; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 931eca80721..3da653c5398 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -22,6 +22,7 @@ import datadog.trace.api.ConfigDefaults; import datadog.trace.api.DDTags; import datadog.trace.api.Pair; +import datadog.trace.api.TagMap; import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.env.CapturedEnvironment; import datadog.trace.api.normalize.HttpResourceNames; @@ -35,6 +36,7 @@ import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; import java.net.URI; +import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -82,6 +84,49 @@ public TagInterceptor( this.jeeSplitByDeployment = jeeSplitByDeployment; } + public boolean needsIntercept(TagMap map) { + for (TagMap.Entry entry : map) { + if (needsIntercept(entry.tag())) return true; + } + return false; + } + + public boolean needsIntercept(Map map) { + for (String tag : map.keySet()) { + if (needsIntercept(tag)) return true; + } + return false; + } + + public boolean needsIntercept(String tag) { + switch (tag) { + case DDTags.RESOURCE_NAME: + case Tags.DB_STATEMENT: + case DDTags.SERVICE_NAME: + case "service": + case Tags.PEER_SERVICE: + case DDTags.MANUAL_KEEP: + case DDTags.MANUAL_DROP: + case Tags.ASM_KEEP: + case Tags.SAMPLING_PRIORITY: + case Tags.PROPAGATED_TRACE_SOURCE: + case Tags.PROPAGATED_DEBUG: + case InstrumentationTags.SERVLET_CONTEXT: + case SPAN_TYPE: + case ANALYTICS_SAMPLE_RATE: + case Tags.ERROR: + case HTTP_STATUS: + case HTTP_METHOD: + case HTTP_URL: + case ORIGIN_KEY: + case MEASURED: + return true; + + default: + return splitServiceTags.contains(tag); + } + } + public boolean interceptTag(DDSpanContext span, String tag, Object value) { switch (tag) { case DDTags.RESOURCE_NAME: diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java index c855262f048..4a2f2d5377f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java @@ -1,14 +1,14 @@ package datadog.trace.core.tagprocessor; import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; -public class BaseServiceAdder implements TagsPostProcessor { +public final class BaseServiceAdder extends TagsPostProcessor { private final UTF8BytesString ddService; public BaseServiceAdder(@Nullable final String ddService) { @@ -16,14 +16,13 @@ public BaseServiceAdder(@Nullable final String ddService) { } @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { if (ddService != null && spanContext != null && !ddService.toString().equalsIgnoreCase(spanContext.getServiceName())) { unsafeTags.put(DDTags.BASE_SERVICE, ddService); unsafeTags.remove("version"); } - return unsafeTags; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/IntegrationAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/IntegrationAdder.java index 79db1f22998..87024d057bd 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/IntegrationAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/IntegrationAdder.java @@ -2,22 +2,20 @@ import static datadog.trace.api.DDTags.DD_INTEGRATION; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; import java.util.List; -import java.util.Map; - -public class IntegrationAdder implements TagsPostProcessor { +public class IntegrationAdder extends TagsPostProcessor { @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { final CharSequence instrumentationName = spanContext.getIntegrationName(); if (instrumentationName != null) { - unsafeTags.put(DD_INTEGRATION, instrumentationName); + unsafeTags.set(DD_INTEGRATION, instrumentationName); } else { unsafeTags.remove(DD_INTEGRATION); } - return unsafeTags; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java index d50c8510295..a52fa938c3e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java @@ -2,6 +2,7 @@ import datadog.trace.api.Config; import datadog.trace.api.ConfigDefaults; +import datadog.trace.api.TagMap; import datadog.trace.api.telemetry.LogCollector; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; @@ -21,7 +22,7 @@ import org.slf4j.LoggerFactory; /** Post-processor that extracts tags from payload data injected as tags by instrumentations. */ -public final class PayloadTagsProcessor implements TagsPostProcessor { +public final class PayloadTagsProcessor extends TagsPostProcessor { private static final Logger log = LoggerFactory.getLogger(PayloadTagsProcessor.class); private static final String REDACTED = "redacted"; @@ -69,21 +70,22 @@ public static PayloadTagsProcessor create(Config config) { } @Override - public Map processTags( - Map spanTags, DDSpanContext spanContext, List spanLinks) { - int spanMaxTags = maxTags + spanTags.size(); + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { + int spanMaxTags = maxTags + unsafeTags.size(); for (Map.Entry tagPrefixRedactionRules : redactionRulesByTagPrefix.entrySet()) { String tagPrefix = tagPrefixRedactionRules.getKey(); RedactionRules redactionRules = tagPrefixRedactionRules.getValue(); - Object tagValue = spanTags.get(tagPrefix); + Object tagValue = unsafeTags.getObject(tagPrefix); if (tagValue instanceof PayloadTagsData) { - if (spanTags.remove(tagPrefix) != null) { + if (unsafeTags.remove(tagPrefix)) { spanMaxTags -= 1; } + PayloadTagsData payloadTagsData = (PayloadTagsData) tagValue; PayloadTagsCollector payloadTagsCollector = - new PayloadTagsCollector(maxDepth, spanMaxTags, redactionRules, tagPrefix, spanTags); + new PayloadTagsCollector(maxDepth, spanMaxTags, redactionRules, tagPrefix, unsafeTags); collectPayloadTags(payloadTagsData, payloadTagsCollector); } else if (tagValue != null) { log.debug( @@ -93,7 +95,6 @@ public Map processTags( tagValue); } } - return spanTags; } private void collectPayloadTags( @@ -187,14 +188,14 @@ private static final class PayloadTagsCollector implements JsonStreamParser.Visi private final RedactionRules redactionRules; private final String tagPrefix; - private final Map collectedTags; + private final TagMap collectedTags; public PayloadTagsCollector( int maxDepth, int maxTags, RedactionRules redactionRules, String tagPrefix, - Map collectedTags) { + TagMap collectedTags) { this.maxDepth = maxDepth; this.maxTags = maxTags; this.redactionRules = redactionRules; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java index 625fae0f839..9a4e9377cb9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java @@ -2,6 +2,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; import datadog.trace.api.naming.NamingSchema; import datadog.trace.api.naming.SpanNaming; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; @@ -11,7 +12,7 @@ import java.util.Map; import javax.annotation.Nonnull; -public class PeerServiceCalculator implements TagsPostProcessor { +public final class PeerServiceCalculator extends TagsPostProcessor { private final NamingSchema.ForPeerService peerServiceNaming; private final Map peerServiceMapping; @@ -32,25 +33,26 @@ public PeerServiceCalculator() { } @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { - Object peerService = unsafeTags.get(Tags.PEER_SERVICE); + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { + Object peerService = unsafeTags.getObject(Tags.PEER_SERVICE); // the user set it if (peerService != null) { if (canRemap) { - return remapPeerService(unsafeTags, peerService); + remapPeerService(unsafeTags, peerService); + return; } } else if (peerServiceNaming.supports()) { // calculate the defaults (if any) peerServiceNaming.tags(unsafeTags); // only remap if the mapping is not empty (saves one get) - return remapPeerService(unsafeTags, canRemap ? unsafeTags.get(Tags.PEER_SERVICE) : null); + remapPeerService(unsafeTags, canRemap ? unsafeTags.getObject(Tags.PEER_SERVICE) : null); + return; } // we have no peer.service and we do not compute defaults. Leave the map untouched - return unsafeTags; } - private Map remapPeerService(Map unsafeTags, Object value) { + private void remapPeerService(TagMap unsafeTags, Object value) { if (value != null) { String mapped = peerServiceMapping.get(value); if (mapped != null) { @@ -58,6 +60,5 @@ private Map remapPeerService(Map unsafeTags, Obj unsafeTags.put(DDTags.PEER_SERVICE_REMAPPED_FROM, value); } } - return unsafeTags; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java index fbf5b511e24..77374d742cb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java @@ -1,13 +1,13 @@ package datadog.trace.core.tagprocessor; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; import java.util.List; -import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; -public class PostProcessorChain implements TagsPostProcessor { +public final class PostProcessorChain extends TagsPostProcessor { private final TagsPostProcessor[] chain; public PostProcessorChain(@Nonnull final TagsPostProcessor... processors) { @@ -15,12 +15,10 @@ public PostProcessorChain(@Nonnull final TagsPostProcessor... processors) { } @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { - Map currentTags = unsafeTags; + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { for (final TagsPostProcessor tagsPostProcessor : chain) { - currentTags = tagsPostProcessor.processTags(currentTags, spanContext, spanLinks); + tagsPostProcessor.processTags(unsafeTags, spanContext, spanLinks); } - return currentTags; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java index 934aa5df1f0..37bbd470596 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java @@ -4,16 +4,16 @@ import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.core.DDSpanContext; import datadog.trace.util.Strings; import java.util.List; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class QueryObfuscator implements TagsPostProcessor { +public final class QueryObfuscator extends TagsPostProcessor { private static final Logger log = LoggerFactory.getLogger(QueryObfuscator.class); @@ -58,20 +58,18 @@ private String obfuscate(String query) { } @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { - Object query = unsafeTags.get(DDTags.HTTP_QUERY); + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { + Object query = unsafeTags.getObject(DDTags.HTTP_QUERY); if (query instanceof CharSequence) { query = obfuscate(query.toString()); unsafeTags.put(DDTags.HTTP_QUERY, query); - Object url = unsafeTags.get(Tags.HTTP_URL); + Object url = unsafeTags.getObject(Tags.HTTP_URL); if (url instanceof CharSequence) { unsafeTags.put(Tags.HTTP_URL, url + "?" + query); } } - - return unsafeTags; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java index b872b824e38..7bd45cd2c92 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java @@ -1,13 +1,13 @@ package datadog.trace.core.tagprocessor; import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; import java.util.List; -import java.util.Map; import java.util.function.Supplier; -public class RemoteHostnameAdder implements TagsPostProcessor { +public final class RemoteHostnameAdder extends TagsPostProcessor { private final Supplier hostnameSupplier; public RemoteHostnameAdder(Supplier hostnameSupplier) { @@ -15,11 +15,10 @@ public RemoteHostnameAdder(Supplier hostnameSupplier) { } @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { if (spanContext.getSpanId() == spanContext.getRootSpanId()) { unsafeTags.put(DDTags.TRACER_HOST, hostnameSupplier.get()); } - return unsafeTags; } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java index c3932b87785..8282583cbf2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java @@ -11,6 +11,7 @@ import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_2_VALUE; import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.S3_ETAG; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; import datadog.trace.bootstrap.instrumentation.api.SpanLink; @@ -25,7 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SpanPointersProcessor implements TagsPostProcessor { +public class SpanPointersProcessor extends TagsPostProcessor { private static final Logger LOG = LoggerFactory.getLogger(SpanPointersProcessor.class); // The pointer direction will always be down. The serverless agent handles cases where the @@ -36,8 +37,9 @@ public class SpanPointersProcessor implements TagsPostProcessor { public static final String LINK_KIND = "span-pointer"; @Override - public Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + public void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { + // DQH - TODO - There's a lot room to optimize this using TagMap's capabilities AgentSpanLink s3Link = handleS3SpanPointer(unsafeTags); if (s3Link != null) { spanLinks.add(s3Link); @@ -47,8 +49,6 @@ public Map processTags( if (dynamoDbLink != null) { spanLinks.add(dynamoDbLink); } - - return unsafeTags; } private static AgentSpanLink handleS3SpanPointer(Map unsafeTags) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java index f188e10d090..d0acedb40ee 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java @@ -1,11 +1,23 @@ package datadog.trace.core.tagprocessor; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; import java.util.List; import java.util.Map; -public interface TagsPostProcessor { - Map processTags( - Map unsafeTags, DDSpanContext spanContext, List spanLinks); +public abstract class TagsPostProcessor { + /* + * DQH - For testing purposes only + */ + @Deprecated + final Map processTags( + Map unsafeTags, DDSpanContext context, List links) { + TagMap map = TagMap.fromMap(unsafeTags); + this.processTags(map, context, links); + return map; + } + + public abstract void processTags( + TagMap unsafeTags, DDSpanContext spanContext, List spanLinks); } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy index 4ce25337ce7..0183844dc7f 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy @@ -5,6 +5,7 @@ import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId import datadog.trace.api.IdGenerationStrategy import datadog.trace.api.ProcessTags +import datadog.trace.api.TagMap import datadog.trace.api.sampling.PrioritySampling import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString import datadog.trace.core.CoreSpan @@ -177,7 +178,7 @@ class TraceGenerator { this.measured = measured this.samplingPriority = samplingPriority this.metadata = new Metadata(Thread.currentThread().getId(), - UTF8BytesString.create(Thread.currentThread().getName()), tags, baggage, samplingPriority, measured, topLevel, + UTF8BytesString.create(Thread.currentThread().getName()), TagMap.fromMap(tags), baggage, samplingPriority, measured, topLevel, statusCode == 0 ? null : UTF8BytesString.create(Integer.toString(statusCode)), origin, 0, ProcessTags.tagsForSerialization) this.httpStatusCode = (short) statusCode diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy index 59521b95ab1..2e533583ea2 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/CoreSpanBuilderTest.groovy @@ -8,6 +8,7 @@ import static datadog.trace.api.DDTags.SCHEMA_VERSION_TAG_KEY import datadog.trace.api.Config import datadog.trace.api.DDSpanId import datadog.trace.api.DDTraceId +import datadog.trace.api.TagMap import datadog.trace.api.gateway.RequestContextSlot import datadog.trace.api.naming.SpanNaming import datadog.trace.api.sampling.PrioritySampling @@ -396,9 +397,9 @@ class CoreSpanBuilderTest extends DDCoreSpecification { ] + productTags() where: - tagContext | _ - new TagContext(null, [:]) | _ - new TagContext("some-origin", ["asdf": "qwer"]) | _ + tagContext | _ + new TagContext(null, TagMap.fromMap([:])) | _ + new TagContext("some-origin", TagMap.fromMap(["asdf": "qwer"])) | _ } def "global span tags populated on each span"() { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy index a66042c50d9..54edfd095df 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/DDSpanTest.groovy @@ -3,6 +3,7 @@ package datadog.trace.core import datadog.trace.api.DDSpanId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId +import datadog.trace.api.TagMap import datadog.trace.api.gateway.RequestContextSlot import datadog.trace.api.sampling.PrioritySampling import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext @@ -274,7 +275,7 @@ class DDSpanTest extends DDCoreSpecification { where: extractedContext | _ - new TagContext("some-origin", [:]) | _ + new TagContext("some-origin", TagMap.fromMap([:])) | _ new ExtractedContext(DDTraceId.ONE, 2, PrioritySampling.SAMPLER_DROP, "some-origin", propagationTagsFactory.empty(), DATADOG) | _ } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/datastreams/DefaultPathwayContextTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/datastreams/DefaultPathwayContextTest.groovy index 3fcf8b48b78..80249d77cda 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/datastreams/DefaultPathwayContextTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/datastreams/DefaultPathwayContextTest.groovy @@ -4,6 +4,7 @@ import datadog.communication.ddagent.DDAgentFeaturesDiscovery import datadog.trace.api.Config import datadog.trace.api.DDTraceId import datadog.trace.api.ProcessTags +import datadog.trace.api.TagMap import datadog.trace.api.TraceConfig import datadog.trace.api.WellKnownTags import datadog.trace.api.datastreams.StatsPoint @@ -478,7 +479,6 @@ class DefaultPathwayContextTest extends DDCoreSpecification { "someotherkey": "someothervalue" ] def contextVisitor = new Base64MapContextVisitor() - def propagator = dataStreams.propagator() when: @@ -520,6 +520,7 @@ class DefaultPathwayContextTest extends DDCoreSpecification { timeSource.advance(MILLISECONDS.toNanos(50)) context.setCheckpoint(fromTags(new LinkedHashMap<>(["type": "internal"])), pointConsumer) def encoded = context.encode() + Map carrier = [(PROPAGATION_KEY_BASE64): encoded, "someotherkey": "someothervalue"] def contextVisitor = new Base64MapContextVisitor() def propagator = dataStreams.propagator() @@ -564,7 +565,7 @@ class DefaultPathwayContextTest extends DDCoreSpecification { def encoded = context.encode() Map carrier = [(PROPAGATION_KEY_BASE64): encoded, "someotherkey": "someothervalue"] def contextVisitor = new Base64MapContextVisitor() - def spanContext = new ExtractedContext(DDTraceId.ONE, 1, 0, null, 0, null, null, null, null, null, DATADOG) + def spanContext = new ExtractedContext(DDTraceId.ONE, 1, 0, null, 0, null, (TagMap)null, null, null, null, DATADOG) def baseContext = AgentSpan.fromSpanContext(spanContext).storeInto(root()) def propagator = dataStreams.propagator() @@ -627,4 +628,4 @@ class DefaultPathwayContextTest extends DDCoreSpecification { } } } -} +} \ No newline at end of file diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy index 2a1dc2583f3..8961c8d41f8 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy @@ -1,5 +1,6 @@ package datadog.trace.core.tagprocessor +import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink import datadog.trace.core.DDSpanContext import datadog.trace.test.util.DDSpecification @@ -9,55 +10,53 @@ class PostProcessorChainTest extends DDSpecification { setup: def processor1 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + void processTags(TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { unsafeTags.put("key1", "processor1") unsafeTags.put("key2", "processor1") - return unsafeTags } } def processor2 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + void processTags(TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { unsafeTags.put("key1", "processor2") - return unsafeTags } } def chain = new PostProcessorChain(processor1, processor2) - def tags = ["key1": "root", "key3": "root"] + def tags = TagMap.fromMap(["key1": "root", "key3": "root"]) when: - def out = chain.processTags(tags, null, []) + chain.processTags(tags, null, []) then: - assert out == ["key1": "processor2", "key2": "processor1", "key3": "root"] + assert tags == ["key1": "processor2", "key2": "processor1", "key3": "root"] } def "processor can hide tags to next one()"() { setup: def processor1 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { - return ["my": "tag"] + void processTags(TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { + unsafeTags.clear() + unsafeTags.put("my", "tag") } } def processor2 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + void processTags(TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { if (unsafeTags.containsKey("test")) { unsafeTags.put("found", "true") } - return unsafeTags } } def chain = new PostProcessorChain(processor1, processor2) - def tags = ["test": "test"] + def tags = TagMap.fromMap(["test": "test"]) when: - def out = chain.processTags(tags, null, []) + chain.processTags(tags, null, []) then: - assert out == ["my": "tag"] + assert tags == ["my": "tag"] } } diff --git a/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy b/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy index d3420c2116b..bc690a73ac0 100644 --- a/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy +++ b/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy @@ -3,6 +3,7 @@ import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId import datadog.trace.api.IdGenerationStrategy import datadog.trace.api.ProcessTags +import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString import datadog.trace.core.CoreSpan import datadog.trace.core.Metadata @@ -156,7 +157,7 @@ class TraceGenerator { this.type = type this.measured = measured this.metadata = new Metadata(Thread.currentThread().getId(), - UTF8BytesString.create(Thread.currentThread().getName()), tags, baggage, UNSET, measured, topLevel, null, null, 0, + UTF8BytesString.create(Thread.currentThread().getName()), TagMap.fromMap(tags), baggage, UNSET, measured, topLevel, null, null, 0, ProcessTags.tagsForSerialization) } @@ -299,7 +300,7 @@ class TraceGenerator { return metadata.getBaggage() } - Map getTags() { + TagMap getTags() { return metadata.getTags() } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index dc1b70139d1..e1ecd3cea34 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -1179,6 +1179,7 @@ public static String getHostName() { private final boolean longRunningTraceEnabled; private final long longRunningTraceInitialFlushInterval; private final long longRunningTraceFlushInterval; + private final boolean cassandraKeyspaceStatementExtractionEnabled; private final boolean couchbaseInternalSpansEnabled; private final boolean elasticsearchBodyEnabled; @@ -1215,6 +1216,7 @@ public static String getHostName() { private final boolean jdkSocketEnabled; + private final boolean optimizedMapEnabled; private final int stackTraceLengthLimit; private final boolean rumEnabled; @@ -2715,6 +2717,9 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) this.jdkSocketEnabled = configProvider.getBoolean(JDK_SOCKET_ENABLED, true); + this.optimizedMapEnabled = + configProvider.getBoolean(GeneralConfig.OPTIMIZED_MAP_ENABLED, false); + int defaultStackTraceLengthLimit = instrumenterConfig.isCiVisibilityEnabled() ? 5000 // EVP limit @@ -4389,15 +4394,19 @@ public boolean isJdkSocketEnabled() { return jdkSocketEnabled; } + public boolean isOptimizedMapEnabled() { + return optimizedMapEnabled; + } + public int getStackTraceLengthLimit() { return stackTraceLengthLimit; } /** @return A map of tags to be applied only to the local application root span. */ - public Map getLocalRootSpanTags() { + public TagMap getLocalRootSpanTags() { final Map runtimeTags = getRuntimeTags(); - final Map result = new HashMap<>(runtimeTags.size() + 2); - result.putAll(runtimeTags); + + final TagMap result = TagMap.fromMap(runtimeTags); result.put(LANGUAGE_TAG_KEY, LANGUAGE_TAG_VALUE); result.put(SCHEMA_VERSION_TAG_KEY, SpanNaming.instance().version()); result.put(DDTags.PROFILING_ENABLED, isProfilingEnabled() ? 1 : 0); @@ -4418,7 +4427,7 @@ public Map getLocalRootSpanTags() { result.putAll(getProcessIdTag()); - return Collections.unmodifiableMap(result); + return result.freeze(); } public WellKnownTags getWellKnownTags() { diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java new file mode 100644 index 00000000000..574e59c9e4e --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -0,0 +1,3009 @@ +package datadog.trace.api; + +import datadog.trace.api.function.TriConsumer; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * A super simple hash map designed for... + * + *
    + *
  • fast copy from one map to another + *
  • compatibility with builder idioms + *
  • building small maps as fast as possible + *
  • storing primitives without boxing + *
  • minimal memory footprint + *
+ * + *

This is mainly accomplished by using immutable entry objects that can reference an object or a + * primitive. By using immutable entries, the entry objects can be shared between builders & maps + * freely. + * + *

This map lacks the ability to mutate an entry via @link {@link Entry#setValue(Object)}. + * Entries must be replaced by re-setting / re-putting the key which will create a new Entry object. + * + *

This map also lacks features designed for handling large long lived mutable maps... + * + *

    + *
  • bucket array expansion + *
  • adaptive collision + *
+ */ +public interface TagMap extends Map, Iterable { + /** Immutable empty TagMap - similar to {@link Collections#emptyMap()} */ + public static final TagMap EMPTY = TagMapFactory.INSTANCE.empty(); + + /** Creates a new mutable TagMap that contains the contents of map */ + public static TagMap fromMap(Map map) { + TagMap tagMap = TagMap.create(map.size()); + tagMap.putAll(map); + return tagMap; + } + + /** Creates a new immutable TagMap that contains the contents of map */ + public static TagMap fromMapImmutable(Map map) { + if (map.isEmpty()) { + return TagMap.EMPTY; + } else { + return fromMap(map).freeze(); + } + } + + static TagMap create() { + return TagMapFactory.INSTANCE.create(); + } + + static TagMap create(int size) { + return TagMapFactory.INSTANCE.create(size); + } + + /** Creates a new TagMap.Ledger */ + public static Ledger ledger() { + return new Ledger(); + } + + /** Creates a new TagMap.Ledger which handles size modifications before expansion */ + public static Ledger ledger(int size) { + return new Ledger(size); + } + + boolean isOptimized(); + + /** Inefficiently implemented for optimized TagMap */ + @Deprecated + Set keySet(); + + /** Inefficiently implemented for optimized TagMap - requires boxing primitives */ + @Deprecated + Collection values(); + + // @Deprecated -- not deprecated until OptimizedTagMap becomes the default + Set> entrySet(); + + /** + * Deprecated in favor of typed getters like... + *
    + *
  • {@link TagMap#getObject(String)} + *
  • {@link TagMap#getString(String)} + *
  • {@link TagMap#getBoolean(String) + *
  • ... + *
+ */ + @Deprecated + Object get(Object tag); + + /** Provides the corresponding entry value as an Object - boxing if necessary */ + Object getObject(String tag); + + /** Provides the corresponding entry value as a String - calling toString if necessary */ + String getString(String tag); + + boolean getBoolean(String tag); + + boolean getBooleanOrDefault(String tag, boolean defaultValue); + + int getInt(String tag); + + int getIntOrDefault(String tag, int defaultValue); + + long getLong(String tag); + + long getLongOrDefault(String tag, long defaultValue); + + float getFloat(String tag); + + float getFloatOrDefault(String tag, float defaultValue); + + double getDouble(String tag); + + double getDoubleOrDefault(String tag, double defaultValue); + + /** + * Provides the corresponding Entry object - preferable w/ optimized TagMap if the Entry needs to + * have its type checked + */ + Entry getEntry(String tag); + + /** + * Deprecated in favor of {@link TagMap#set} methods. set methods don't return the prior value and + * are implemented efficiently for both the legacy and optimized implementations of TagMap. + */ + @Deprecated + Object put(String tag, Object value); + + /** Sets value without returning prior value - optimal for legacy & optimized implementations */ + void set(String tag, Object value); + + /** + * Similar to {@link TagMap#set(String, Object)} but more efficient when working with + * CharSequences and Strings. Depending on this situation, this methods avoids having to do type + * resolution later on + */ + void set(String tag, CharSequence value); + + void set(String tag, boolean value); + + void set(String tag, int value); + + void set(String tag, long value); + + void set(String tag, float value); + + void set(String tag, double value); + + void set(Entry newEntry); + + /** sets the value while returning the prior Entry */ + Entry getAndSet(String tag, Object value); + + Entry getAndSet(String tag, CharSequence value); + + Entry getAndSet(String tag, boolean value); + + Entry getAndSet(String tag, int value); + + Entry getAndSet(String tag, long value); + + Entry getAndSet(String tag, float value); + + Entry getAndSet(String tag, double value); + + /** + * TagMap specific method that places an Entry directly into an optimized TagMap avoiding need to + * allocate a new Entry object + */ + Entry getAndSet(Entry newEntry); + + void putAll(Map map); + + /** + * Similar to {@link Map#putAll(Map)} but optimized to quickly copy from one TagMap to another + * + *

For optimized TagMaps, this method takes advantage of the consistent TagMap layout to + * quickly handle each bucket. And similar to {@link TagMap#(Entry)} this method shares Entry + * objects from the source TagMap + */ + void putAll(TagMap that); + + void fillMap(Map map); + + void fillStringMap(Map stringMap); + + /** + * Deprecated in favor of {@link TagMap#remove(String)} which returns a boolean and is efficiently + * implemented for both legacy and optimal TagMaps + */ + @Deprecated + Object remove(Object tag); + + /** + * Similar to {@link Map#remove(Object)} but doesn't return the prior value (orEntry). Preferred + * when prior value isn't needed - best for both legacy and optimal TagMaps + */ + boolean remove(String tag); + + /** + * Similar to {@link Map#remove(Object)} but returns the prior Entry object rather than the prior + * value. For optimized TagMap-s, this method is preferred because it avoids additional boxing. + */ + Entry getAndRemove(String tag); + + /** Returns a mutable copy of this TagMap */ + TagMap copy(); + + /** + * Returns an immutable copy of this TagMap This method is more efficient than + * map.copy().freeze() when called on an immutable TagMap + */ + TagMap immutableCopy(); + + /** + * Provides an Iterator over the Entry-s of the TagMap Equivalent to entrySet().iterator() + * , but with less allocation + */ + @Override + Iterator iterator(); + + Stream stream(); + + /** + * Visits each Entry in this TagMap This method is more efficient than {@link TagMap#iterator()} + */ + void forEach(Consumer consumer); + + /** + * Version of forEach that takes an extra context object that is passed as the first argument to + * the consumer + * + *

The intention is to use this method to avoid using a capturing lambda + */ + void forEach(T thisObj, BiConsumer consumer); + + /** + * Version of forEach that takes two extra context objects that are passed as the first two + * argument to the consumer + * + *

The intention is to use this method to avoid using a capturing lambda + */ + void forEach(T thisObj, U otherObj, TriConsumer consumer); + + /** Clears the TagMap */ + void clear(); + + /** Freeze the TagMap preventing further modification - returns this TagMap */ + TagMap freeze(); + + /** Indicates if this map is frozen */ + boolean isFrozen(); + + /** Checks if the TagMap is writable - if not throws {@link IllegalStateException} */ + void checkWriteAccess(); + + public abstract static class EntryChange { + public static final EntryRemoval newRemoval(String tag) { + return new EntryRemoval(tag); + } + + final String tag; + + EntryChange(String tag) { + this.tag = tag; + } + + public final String tag() { + return this.tag; + } + + public final boolean matches(String tag) { + return this.tag.equals(tag); + } + + public abstract boolean isRemoval(); + } + + public static final class EntryRemoval extends EntryChange { + EntryRemoval(String tag) { + super(tag); + } + + @Override + public final boolean isRemoval() { + return true; + } + } + + public static final class Entry extends EntryChange implements Map.Entry { + /* + * Special value used for Objects that haven't been type checked yet. + * These objects might be primitive box objects. + */ + public static final byte ANY = 0; + public static final byte OBJECT = 1; + + /* + * Non-numeric primitive types + */ + public static final byte BOOLEAN = 2; + public static final byte CHAR = 3; + + /* + * Numeric constants - deliberately arranged to allow for checking by using type >= BYTE + */ + public static final byte BYTE = 4; + public static final byte SHORT = 5; + public static final byte INT = 6; + public static final byte LONG = 7; + public static final byte FLOAT = 8; + public static final byte DOUBLE = 9; + + static final Entry newAnyEntry(Map.Entry entry) { + return newAnyEntry(entry.getKey(), entry.getValue()); + } + + static final Entry newAnyEntry(String tag, Object value) { + // DQH - To keep entry creation (e.g. map changes) as fast as possible, + // the entry construction is kept as simple as possible. + + // Prior versions of this code did type detection on value to + // recognize box types but that proved expensive. So now, + // the type is recorded as an ANY which is an indicator to do + // type detection later if need be. + return new Entry(tag, ANY, 0L, value); + } + + static final Entry newObjectEntry(String tag, Object value) { + return new Entry(tag, OBJECT, 0, value); + } + + static final Entry newBooleanEntry(String tag, boolean value) { + return new Entry(tag, BOOLEAN, boolean2Prim(value), Boolean.valueOf(value)); + } + + static final Entry newBooleanEntry(String tag, Boolean box) { + return new Entry(tag, BOOLEAN, boolean2Prim(box.booleanValue()), box); + } + + static final Entry newIntEntry(String tag, int value) { + return new Entry(tag, INT, int2Prim(value), null); + } + + static final Entry newIntEntry(String tag, Integer box) { + return new Entry(tag, INT, int2Prim(box.intValue()), box); + } + + static final Entry newLongEntry(String tag, long value) { + return new Entry(tag, LONG, long2Prim(value), null); + } + + static final Entry newLongEntry(String tag, Long box) { + return new Entry(tag, LONG, long2Prim(box.longValue()), box); + } + + static final Entry newFloatEntry(String tag, float value) { + return new Entry(tag, FLOAT, float2Prim(value), null); + } + + static final Entry newFloatEntry(String tag, Float box) { + return new Entry(tag, FLOAT, float2Prim(box.floatValue()), box); + } + + static final Entry newDoubleEntry(String tag, double value) { + return new Entry(tag, DOUBLE, double2Prim(value), null); + } + + static final Entry newDoubleEntry(String tag, Double box) { + return new Entry(tag, DOUBLE, double2Prim(box.doubleValue()), box); + } + + /* + * hash is stored in line for fast handling of Entry-s coming from another TagMap + * However, hash is lazily computed using the same trick as {@link java.lang.String}. + */ + int lazyTagHash; + + // To optimize construction of Entry around boxed primitives and Object entries, + // no type checks are done during construction. + // Any Object entries are initially marked as type ANY, prim set to 0, and the Object put into + // obj + // If an ANY entry is later type checked or request as a primitive, then the ANY will be + // resolved + // to the correct type. + + // From the outside perspective, this object remains functionally immutable. + // However, internally, it is important to remember that this type must be thread safe. + // That includes multiple threads racing to resolve an ANY entry at the same time. + + // Type and prim cannot use the same trick as hash because during ANY resolution the order of + // writes is important + volatile byte rawType; + volatile long rawPrim; + volatile Object rawObj; + + volatile String strCache = null; + + private Entry(String tag, byte type, long prim, Object obj) { + super(tag); + this.lazyTagHash = 0; // lazily computed + + this.rawType = type; + this.rawPrim = prim; + this.rawObj = obj; + } + + int hash() { + // If value of hash read in this thread is zero, then hash is computed. + // hash is not held as a volatile, since this computation can safely be repeated as any time + int hash = this.lazyTagHash; + if (hash != 0) return hash; + + hash = _hash(this.tag); + this.lazyTagHash = hash; + return hash; + } + + public final byte type() { + return this.resolveAny(); + } + + public final boolean is(byte type) { + byte curType = this.rawType; + if (curType == type) { + return true; + } else if (curType != ANY) { + return false; + } else { + return (this.resolveAny() == type); + } + } + + public final boolean isNumericPrimitive() { + byte curType = this.rawType; + if (_isNumericPrimitive(curType)) { + return true; + } else if (curType != ANY) { + return false; + } else { + return _isNumericPrimitive(this.resolveAny()); + } + } + + public final boolean isNumber() { + byte curType = this.rawType; + return _isNumericPrimitive(curType) || (this.rawObj instanceof Number); + } + + private static final boolean _isNumericPrimitive(byte type) { + return (type >= BYTE); + } + + private final byte resolveAny() { + byte curType = this.rawType; + if (curType != ANY) return curType; + + Object value = this.rawObj; + long prim; + byte resolvedType; + + if (value instanceof Boolean) { + Boolean boolValue = (Boolean) value; + prim = boolean2Prim(boolValue); + resolvedType = BOOLEAN; + } else if (value instanceof Integer) { + Integer intValue = (Integer) value; + prim = int2Prim(intValue); + resolvedType = INT; + } else if (value instanceof Long) { + Long longValue = (Long) value; + prim = long2Prim(longValue); + resolvedType = LONG; + } else if (value instanceof Float) { + Float floatValue = (Float) value; + prim = float2Prim(floatValue); + resolvedType = FLOAT; + } else if (value instanceof Double) { + Double doubleValue = (Double) value; + prim = double2Prim(doubleValue); + resolvedType = DOUBLE; + } else { + prim = 0; + resolvedType = OBJECT; + } + + this._setPrim(resolvedType, prim); + + return resolvedType; + } + + private void _setPrim(byte type, long prim) { + // Order is important here, the contract is that prim must be set properly *before* + // type is set to a non-object type + + this.rawPrim = prim; + this.rawType = type; + } + + public final boolean isObject() { + return this.is(OBJECT); + } + + public final boolean isRemoval() { + return false; + } + + public final Object objectValue() { + if (this.rawObj != null) { + return this.rawObj; + } + + // This code doesn't need to handle ANY-s. + // An entry that starts as an ANY will always have this.obj set + switch (this.rawType) { + case BOOLEAN: + this.rawObj = prim2Boolean(this.rawPrim); + break; + + case INT: + // Maybe use a wider cache that handles response code??? + this.rawObj = prim2Int(this.rawPrim); + break; + + case LONG: + this.rawObj = prim2Long(this.rawPrim); + break; + + case FLOAT: + this.rawObj = prim2Float(this.rawPrim); + break; + + case DOUBLE: + this.rawObj = prim2Double(this.rawPrim); + break; + + default: + // DQH - satisfy spot bugs + break; + } + + return this.rawObj; + } + + public final boolean booleanValue() { + byte type = this.rawType; + + if (type == BOOLEAN) { + return prim2Boolean(this.rawPrim); + } else if (type == ANY && this.rawObj instanceof Boolean) { + boolean boolValue = (Boolean) this.rawObj; + this._setPrim(BOOLEAN, boolean2Prim(boolValue)); + return boolValue; + } + + // resolution will set prim if necessary + byte resolvedType = this.resolveAny(); + long prim = this.rawPrim; + + switch (resolvedType) { + case INT: + return prim2Int(prim) != 0; + + case LONG: + return prim2Long(prim) != 0L; + + case FLOAT: + return prim2Float(prim) != 0F; + + case DOUBLE: + return prim2Double(prim) != 0D; + + case OBJECT: + return (this.rawObj != null); + } + + return false; + } + + public final int intValue() { + byte type = this.rawType; + + if (type == INT) { + return prim2Int(this.rawPrim); + } else if (type == ANY && this.rawObj instanceof Integer) { + int intValue = (Integer) this.rawObj; + this._setPrim(INT, int2Prim(intValue)); + return intValue; + } + + // resolution will set prim if necessary + byte resolvedType = this.resolveAny(); + long prim = this.rawPrim; + + switch (resolvedType) { + case BOOLEAN: + return prim2Boolean(prim) ? 1 : 0; + + case LONG: + return (int) prim2Long(prim); + + case FLOAT: + return (int) prim2Float(prim); + + case DOUBLE: + return (int) prim2Double(prim); + + case OBJECT: + return 0; + } + + return 0; + } + + public final long longValue() { + byte type = this.rawType; + + if (type == LONG) { + return prim2Long(this.rawPrim); + } else if (type == ANY && this.rawObj instanceof Long) { + long longValue = (Long) this.rawObj; + this._setPrim(LONG, long2Prim(longValue)); + return longValue; + } + + // resolution will set prim if necessary + byte resolvedType = this.resolveAny(); + long prim = this.rawPrim; + + switch (resolvedType) { + case BOOLEAN: + return prim2Boolean(prim) ? 1L : 0L; + + case INT: + return (long) prim2Int(prim); + + case FLOAT: + return (long) prim2Float(prim); + + case DOUBLE: + return (long) prim2Double(prim); + + case OBJECT: + return 0; + } + + return 0; + } + + public final float floatValue() { + byte type = this.rawType; + + if (type == FLOAT) { + return prim2Float(this.rawPrim); + } else if (type == ANY && this.rawObj instanceof Float) { + float floatValue = (Float) this.rawObj; + this._setPrim(FLOAT, float2Prim(floatValue)); + return floatValue; + } + + // resolution will set prim if necessary + byte resolvedType = this.resolveAny(); + long prim = this.rawPrim; + + switch (resolvedType) { + case BOOLEAN: + return prim2Boolean(prim) ? 1F : 0F; + + case INT: + return (float) prim2Int(prim); + + case LONG: + return (float) prim2Long(prim); + + case DOUBLE: + return (float) prim2Double(prim); + + case OBJECT: + return 0F; + } + + return 0F; + } + + public final double doubleValue() { + byte type = this.rawType; + + if (type == DOUBLE) { + return prim2Double(this.rawPrim); + } else if (type == ANY && this.rawObj instanceof Double) { + double doubleValue = (Double) this.rawObj; + this._setPrim(DOUBLE, double2Prim(doubleValue)); + return doubleValue; + } + + // resolution will set prim if necessary + byte resolvedType = this.resolveAny(); + long prim = this.rawPrim; + + switch (resolvedType) { + case BOOLEAN: + return prim2Boolean(prim) ? 1D : 0D; + + case INT: + return (double) prim2Int(prim); + + case LONG: + return (double) prim2Long(prim); + + case FLOAT: + return (double) prim2Float(prim); + + case OBJECT: + return 0D; + } + + return 0D; + } + + public final String stringValue() { + String strCache = this.strCache; + if (strCache != null) { + return strCache; + } + + String computeStr = this.computeStringValue(); + this.strCache = computeStr; + return computeStr; + } + + private final String computeStringValue() { + // Could do type resolution here, + // but decided to just fallback to this.obj.toString() for ANY case + switch (this.rawType) { + case BOOLEAN: + return Boolean.toString(prim2Boolean(this.rawPrim)); + + case INT: + return Integer.toString(prim2Int(this.rawPrim)); + + case LONG: + return Long.toString(prim2Long(this.rawPrim)); + + case FLOAT: + return Float.toString(prim2Float(this.rawPrim)); + + case DOUBLE: + return Double.toString(prim2Double(this.rawPrim)); + + case OBJECT: + case ANY: + return this.rawObj.toString(); + } + + return null; + } + + @Override + public final String toString() { + return this.tag() + '=' + this.stringValue(); + } + + /** Deprecated in favor of{@link Entry#tag()} */ + @Deprecated + @Override + public String getKey() { + return this.tag(); + } + + /** + * Deprecated in favor of typed getters like... + * + *

    + *
  • {@link Entry#objectValue()} + *
  • {@link Entry#stringValue()} + *
  • {@link Entry#booleanValue()} + *
  • ... + *
+ */ + @Deprecated + @Override + public Object getValue() { + return this.objectValue(); + } + + @Deprecated + @Override + public Object setValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public final int hashCode() { + return this.hash(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TagMap.Entry)) return false; + + TagMap.Entry that = (TagMap.Entry) obj; + return this.tag.equals(that.tag) && this.objectValue().equals(that.objectValue()); + } + + private static final long boolean2Prim(boolean value) { + return value ? 1L : 0L; + } + + private static final boolean prim2Boolean(long prim) { + return (prim != 0L); + } + + private static final long int2Prim(int value) { + return (long) value; + } + + private static final int prim2Int(long prim) { + return (int) prim; + } + + private static final long long2Prim(long value) { + return value; + } + + private static final long prim2Long(long prim) { + return prim; + } + + private static final long float2Prim(float value) { + return (long) Float.floatToIntBits(value); + } + + private static final float prim2Float(long prim) { + return Float.intBitsToFloat((int) prim); + } + + private static final long double2Prim(double value) { + return Double.doubleToRawLongBits(value); + } + + private static final double prim2Double(long prim) { + return Double.longBitsToDouble(prim); + } + + static final int _hash(String tag) { + int hash = tag.hashCode(); + return hash == 0 ? 0xDD06 : hash ^ (hash >>> 16); + } + } + + /* + * An in-order ledger of changes to be made to a TagMap. + * Ledger can also serves as a builder for TagMap-s via build & buildImmutable. + */ + public static final class Ledger implements Iterable { + EntryChange[] entryChanges; + int nextPos = 0; + boolean containsRemovals = false; + + private Ledger() { + this(8); + } + + private Ledger(int size) { + this.entryChanges = new EntryChange[size]; + } + + public final boolean isDefinitelyEmpty() { + return (this.nextPos == 0); + } + + /** + * Provides the estimated size of the map created by the ledger Doesn't account for overwritten + * entries or entry removal + * + * @return + */ + public final int estimateSize() { + return this.nextPos; + } + + public final boolean containsRemovals() { + return this.containsRemovals; + } + + public final Ledger set(String tag, Object value) { + return this.recordEntry(Entry.newAnyEntry(tag, value)); + } + + public final Ledger set(String tag, CharSequence value) { + return this.recordEntry(Entry.newObjectEntry(tag, value)); + } + + public final Ledger set(String tag, boolean value) { + return this.recordEntry(Entry.newBooleanEntry(tag, value)); + } + + public final Ledger set(String tag, int value) { + return this.recordEntry(Entry.newIntEntry(tag, value)); + } + + public final Ledger set(String tag, long value) { + return this.recordEntry(Entry.newLongEntry(tag, value)); + } + + public final Ledger set(String tag, float value) { + return this.recordEntry(Entry.newFloatEntry(tag, value)); + } + + public final Ledger set(String tag, double value) { + return this.recordEntry(Entry.newDoubleEntry(tag, value)); + } + + public final Ledger set(Entry entry) { + return this.recordEntry(entry); + } + + public final Ledger remove(String tag) { + return this.recordRemoval(EntryChange.newRemoval(tag)); + } + + private final Ledger recordEntry(Entry entry) { + this.recordChange(entry); + return this; + } + + private final Ledger recordRemoval(EntryRemoval entry) { + this.recordChange(entry); + this.containsRemovals = true; + + return this; + } + + private final void recordChange(EntryChange entryChange) { + if (this.nextPos >= this.entryChanges.length) { + this.entryChanges = Arrays.copyOf(this.entryChanges, this.entryChanges.length << 1); + } + + this.entryChanges[this.nextPos++] = entryChange; + } + + public final Ledger smartRemove(String tag) { + if (this.contains(tag)) { + this.remove(tag); + } + return this; + } + + private final boolean contains(String tag) { + EntryChange[] thisChanges = this.entryChanges; + + // min is to clamp, so bounds check elimination optimization works + int lenClamp = Math.min(this.nextPos, thisChanges.length); + for (int i = 0; i < lenClamp; ++i) { + if (thisChanges[i].matches(tag)) return true; + } + return false; + } + + /* + * Just for testing + */ + final Entry findLastEntry(String tag) { + EntryChange[] thisChanges = this.entryChanges; + + // min is to clamp, so ArrayBoundsCheckElimination optimization works + int clampLen = Math.min(this.nextPos, thisChanges.length) - 1; + for (int i = clampLen; i >= 0; --i) { + EntryChange thisChange = thisChanges[i]; + if (!thisChange.isRemoval() && thisChange.matches(tag)) return (Entry) thisChange; + } + return null; + } + + public final void reset() { + Arrays.fill(this.entryChanges, null); + this.nextPos = 0; + } + + @Override + public final Iterator iterator() { + return new IteratorImpl(this.entryChanges, this.nextPos); + } + + public TagMap build() { + TagMap map = TagMap.create(this.estimateSize()); + fill(map); + return map; + } + + TagMap build(TagMapFactory mapFactory) { + TagMap map = mapFactory.create(this.estimateSize()); + fill(map); + return map; + } + + void fill(TagMap map) { + EntryChange[] entryChanges = this.entryChanges; + int size = this.nextPos; + for (int i = 0; i < size && i < entryChanges.length; ++i) { + EntryChange change = entryChanges[i]; + + if (change.isRemoval()) { + map.remove(change.tag()); + } else { + map.set((Entry) change); + } + } + } + + TagMap buildImmutable(TagMapFactory mapFactory) { + if (this.nextPos == 0) { + return mapFactory.empty(); + } else { + return this.build(mapFactory).freeze(); + } + } + + public TagMap buildImmutable() { + if (this.nextPos == 0) { + return TagMap.EMPTY; + } else { + return this.build().freeze(); + } + } + + static final class IteratorImpl implements Iterator { + private final EntryChange[] entryChanges; + private final int size; + + private int pos; + + IteratorImpl(EntryChange[] entryChanges, int size) { + this.entryChanges = entryChanges; + this.size = size; + + this.pos = -1; + } + + @Override + public final boolean hasNext() { + return (this.pos + 1 < this.size); + } + + @Override + public EntryChange next() { + if (!this.hasNext()) throw new NoSuchElementException("no next"); + + return this.entryChanges[++this.pos]; + } + } + } +} + +/* + * Using a class, so class hierarchy analysis kicks in + * That will allow all of the calls to create methods to be devirtualized without a guard + */ +abstract class TagMapFactory { + public static final TagMapFactory INSTANCE = + createFactory(Config.get().isOptimizedMapEnabled()); + + static final TagMapFactory createFactory(boolean useOptimized) { + return useOptimized ? new OptimizedTagMapFactory() : new LegacyTagMapFactory(); + } + + public abstract MapT create(); + + public abstract MapT create(int size); + + public abstract MapT empty(); +} + +final class OptimizedTagMapFactory extends TagMapFactory { + @Override + public final OptimizedTagMap create() { + return new OptimizedTagMap(); + } + + @Override + public OptimizedTagMap create(int size) { + return new OptimizedTagMap(); + } + + @Override + public OptimizedTagMap empty() { + return OptimizedTagMap.EMPTY; + } +} + +final class LegacyTagMapFactory extends TagMapFactory { + @Override + public final LegacyTagMap create() { + return new LegacyTagMap(); + } + + @Override + public LegacyTagMap create(int size) { + return new LegacyTagMap(size); + } + + @Override + public LegacyTagMap empty() { + return LegacyTagMap.EMPTY; + } +} + +/* + * For memory efficiency, OptimizedTagMap uses a rather complicated bucket system. + *

+ * When there is only a single Entry in a particular bucket, the Entry is stored into the bucket directly. + *

+ * Because the Entry objects can be shared between multiple TagMaps, the Entry objects cannot + * directly form a linked list to handle collisions. + *

+ * Instead when multiple entries collide in the same bucket, a BucketGroup is formed to hold multiple entries. + * But a BucketGroup is only formed when a collision occurs to keep allocation low in the common case of no collisions. + *

+ * For efficiency, BucketGroups are a fixed size, so when a BucketGroup fills up another BucketGroup is formed + * to hold the additional Entry-s. And the BucketGroup-s are connected via a linked list instead of the Entry-s. + *

+ * This does introduce some inefficiencies when Entry-s are removed. + * The assumption is that removals are rare, so BucketGroups are never consolidated. + * However as a precaution if a BucketGroup becomes completely empty, then that BucketGroup will be + * removed from the collision chain. + */ +final class OptimizedTagMap implements TagMap { + // Using special constructor that creates a frozen view of an existing array + // Bucket calculation requires that array length is a power of 2 + // e.g. size 0 will not work, it results in ArrayIndexOutOfBoundsException, but size 1 does + static final OptimizedTagMap EMPTY = new OptimizedTagMap(new Object[1], 0); + + private final Object[] buckets; + private int size; + private boolean frozen; + + public OptimizedTagMap() { + // needs to be a power of 2 for bucket masking calculation to work as intended + this.buckets = new Object[1 << 4]; + this.size = 0; + this.frozen = false; + } + + /** Used for inexpensive immutable */ + private OptimizedTagMap(Object[] buckets, int size) { + this.buckets = buckets; + this.size = size; + this.frozen = true; + } + + @Override + public final boolean isOptimized() { + return true; + } + + @Override + public final int size() { + return this.size; + } + + @Override + public final boolean isEmpty() { + return (this.size == 0); + } + + @Deprecated + @Override + public final Object get(Object tag) { + if (!(tag instanceof String)) return null; + + return this.getObject((String) tag); + } + + /** Provides the corresponding entry value as an Object - boxing if necessary */ + public final Object getObject(String tag) { + Entry entry = this.getEntry(tag); + return entry == null ? null : entry.objectValue(); + } + + /** Provides the corresponding entry value as a String - calling toString if necessary */ + public final String getString(String tag) { + Entry entry = this.getEntry(tag); + return entry == null ? null : entry.stringValue(); + } + + public final boolean getBoolean(String tag) { + return this.getBooleanOrDefault(tag, false); + } + + public final boolean getBooleanOrDefault(String tag, boolean defaultValue) { + Entry entry = this.getEntry(tag); + return entry == null ? defaultValue : entry.booleanValue(); + } + + public final int getInt(String tag) { + return getIntOrDefault(tag, 0); + } + + public final int getIntOrDefault(String tag, int defaultValue) { + Entry entry = this.getEntry(tag); + return entry == null ? defaultValue : entry.intValue(); + } + + public final long getLong(String tag) { + return this.getLongOrDefault(tag, 0L); + } + + public final long getLongOrDefault(String tag, long defaultValue) { + Entry entry = this.getEntry(tag); + return entry == null ? defaultValue : entry.longValue(); + } + + public final float getFloat(String tag) { + return this.getFloatOrDefault(tag, 0F); + } + + public final float getFloatOrDefault(String tag, float defaultValue) { + Entry entry = this.getEntry(tag); + return entry == null ? defaultValue : entry.floatValue(); + } + + public final double getDouble(String tag) { + return this.getDoubleOrDefault(tag, 0D); + } + + public final double getDoubleOrDefault(String tag, double defaultValue) { + Entry entry = this.getEntry(tag); + return entry == null ? defaultValue : entry.doubleValue(); + } + + @Override + public boolean containsKey(Object key) { + if (!(key instanceof String)) return false; + + return (this.getEntry((String) key) != null); + } + + @Override + public boolean containsValue(Object value) { + // This could be optimized - but probably isn't called enough to be worth it + for (Entry entry : this) { + if (entry.objectValue().equals(value)) return true; + } + return false; + } + + @Override + public Set keySet() { + return new Keys(this); + } + + @Override + public Collection values() { + return new Values(this); + } + + @Override + public Set> entrySet() { + return new Entries(this); + } + + @Override + public final Entry getEntry(String tag) { + Object[] thisBuckets = this.buckets; + + int hash = TagMap.Entry._hash(tag); + int bucketIndex = hash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + if (bucket == null) { + return null; + } else if (bucket instanceof Entry) { + Entry tagEntry = (Entry) bucket; + if (tagEntry.matches(tag)) return tagEntry; + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + Entry tagEntry = lastGroup.findInChain(hash, tag); + if (tagEntry != null) return tagEntry; + } + return null; + } + + @Deprecated + @Override + public final Object put(String tag, Object value) { + TagMap.Entry entry = this.getAndSet(Entry.newAnyEntry(tag, value)); + return entry == null ? null : entry.objectValue(); + } + + @Override + public final void set(TagMap.Entry newEntry) { + this.getAndSet(newEntry); + } + + @Override + public final void set(String tag, Object value) { + this.getAndSet(Entry.newAnyEntry(tag, value)); + } + + @Override + public final void set(String tag, CharSequence value) { + this.getAndSet(Entry.newObjectEntry(tag, value)); + } + + @Override + public final void set(String tag, boolean value) { + this.getAndSet(Entry.newBooleanEntry(tag, value)); + } + + @Override + public final void set(String tag, int value) { + this.getAndSet(Entry.newIntEntry(tag, value)); + } + + @Override + public final void set(String tag, long value) { + this.getAndSet(Entry.newLongEntry(tag, value)); + } + + @Override + public final void set(String tag, float value) { + this.getAndSet(Entry.newFloatEntry(tag, value)); + } + + @Override + public final void set(String tag, double value) { + this.getAndSet(Entry.newDoubleEntry(tag, value)); + } + + @Override + public final Entry getAndSet(Entry newEntry) { + this.checkWriteAccess(); + + Object[] thisBuckets = this.buckets; + + int newHash = newEntry.hash(); + int bucketIndex = newHash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + if (bucket == null) { + thisBuckets[bucketIndex] = newEntry; + + this.size += 1; + return null; + } else if (bucket instanceof Entry) { + Entry existingEntry = (Entry) bucket; + if (existingEntry.matches(newEntry.tag)) { + thisBuckets[bucketIndex] = newEntry; + + // replaced existing entry - no size change + return existingEntry; + } else { + thisBuckets[bucketIndex] = + new BucketGroup(existingEntry.hash(), existingEntry, newHash, newEntry); + + this.size += 1; + return null; + } + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + BucketGroup containingGroup = lastGroup.findContainingGroupInChain(newHash, newEntry.tag); + if (containingGroup != null) { + // replaced existing entry - no size change + return containingGroup._replace(newHash, newEntry); + } + + if (!lastGroup.insertInChain(newHash, newEntry)) { + thisBuckets[bucketIndex] = new BucketGroup(newHash, newEntry, lastGroup); + } + this.size += 1; + return null; + } + + // unreachable + return null; + } + + @Override + public Entry getAndSet(String tag, Object value) { + return this.getAndSet(Entry.newAnyEntry(tag, value)); + } + + @Override + public Entry getAndSet(String tag, CharSequence value) { + return this.getAndSet(Entry.newObjectEntry(tag, value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, boolean value) { + return this.getAndSet(Entry.newBooleanEntry(tag, value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, int value) { + return this.getAndSet(Entry.newIntEntry(tag, value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, long value) { + return this.getAndSet(Entry.newLongEntry(tag, value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, float value) { + return this.getAndSet(Entry.newFloatEntry(tag, value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, double value) { + return this.getAndSet(Entry.newDoubleEntry(tag, value)); + } + + public final void putAll(Map map) { + this.checkWriteAccess(); + + if (map instanceof OptimizedTagMap) { + this.putAllOptimizedMap((OptimizedTagMap) map); + } else { + this.putAllUnoptimizedMap(map); + } + } + + private final void putAllUnoptimizedMap(Map that) { + for (Map.Entry entry : that.entrySet()) { + // use set which returns a prior Entry rather put which may box a prior primitive value + this.set(entry.getKey(), entry.getValue()); + } + } + + /** + * Similar to {@link Map#putAll(Map)} but optimized to quickly copy from one TagMap to another + * + *

For optimized TagMaps, this method takes advantage of the consistent TagMap layout to + * quickly handle each bucket. And similar to {@link TagMap#getAndSet(Entry)} this method shares + * Entry objects from the source TagMap + */ + public final void putAll(TagMap that) { + this.checkWriteAccess(); + + if (that instanceof OptimizedTagMap) { + this.putAllOptimizedMap((OptimizedTagMap) that); + } else { + this.putAllUnoptimizedMap(that); + } + } + + private final void putAllOptimizedMap(OptimizedTagMap that) { + if (this.size == 0) { + this.putAllIntoEmptyMap(that); + } else { + this.putAllMerge(that); + } + } + + private final void putAllMerge(OptimizedTagMap that) { + Object[] thisBuckets = this.buckets; + Object[] thatBuckets = that.buckets; + + // Since TagMap-s don't support expansion, buckets are perfectly aligned + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check + // elimination + for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { + Object thatBucket = thatBuckets[i]; + + // if nothing incoming, nothing to do + if (thatBucket == null) continue; + + Object thisBucket = thisBuckets[i]; + if (thisBucket == null) { + // This bucket is null, easy case + // Either copy over the sole entry or clone the BucketGroup chain + + if (thatBucket instanceof Entry) { + thisBuckets[i] = thatBucket; + this.size += 1; + } else if (thatBucket instanceof BucketGroup) { + BucketGroup thatGroup = (BucketGroup) thatBucket; + + BucketGroup thisNewGroup = thatGroup.cloneChain(); + thisBuckets[i] = thisNewGroup; + this.size += thisNewGroup.sizeInChain(); + } + } else if (thisBucket instanceof Entry) { + // This bucket is a single entry, medium complexity case + // If other side is an Entry - just merge the entries into a bucket + // If other side is a BucketGroup - then clone the group and insert the entry normally into + // the cloned group + + Entry thisEntry = (Entry) thisBucket; + int thisHash = thisEntry.hash(); + + if (thatBucket instanceof Entry) { + Entry thatEntry = (Entry) thatBucket; + int thatHash = thatEntry.hash(); + + if (thisHash == thatHash && thisEntry.matches(thatEntry.tag())) { + thisBuckets[i] = thatEntry; + // replacing entry, no size change + } else { + thisBuckets[i] = + new BucketGroup( + thisHash, thisEntry, + thatHash, thatEntry); + this.size += 1; + } + } else if (thatBucket instanceof BucketGroup) { + BucketGroup thatGroup = (BucketGroup) thatBucket; + + // Clone the other group, then place this entry into that group + BucketGroup thisNewGroup = thatGroup.cloneChain(); + int thisNewGroupSize = thisNewGroup.sizeInChain(); + + Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); + if (incomingEntry != null) { + // there's already an entry w/ the same tag from the incoming TagMap + // incoming entry clobbers the existing try, so we're done + thisBuckets[i] = thisNewGroup; + + // overlapping group - subtract one for clobbered existing entry + this.size += thisNewGroupSize - 1; + } else if (thisNewGroup.insertInChain(thisHash, thisEntry)) { + // able to add thisEntry into the existing groups + thisBuckets[i] = thisNewGroup; + + // non overlapping group - existing entry already accounted for in this.size + this.size += thisNewGroupSize; + } else { + // unable to add into the existing groups + thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup); + + // non overlapping group - existing entry already accounted for in this.size + this.size += thisNewGroupSize; + } + } + } else if (thisBucket instanceof BucketGroup) { + // This bucket is a BucketGroup, medium to hard case + // If the other side is an entry, just normal insertion procedure - no cloning required + BucketGroup thisGroup = (BucketGroup) thisBucket; + + if (thatBucket instanceof Entry) { + Entry thatEntry = (Entry) thatBucket; + int thatHash = thatEntry.hash(); + + if (thisGroup.replaceInChain(thatHash, thatEntry) != null) { + // replaced existing entry no size change + } else if (thisGroup.insertInChain(thatHash, thatEntry)) { + this.size += 1; + } else { + thisBuckets[i] = new BucketGroup(thatHash, thatEntry, thisGroup); + this.size += 1; + } + } else if (thatBucket instanceof BucketGroup) { + // Most complicated case - need to walk that bucket group chain and update this chain + BucketGroup thatGroup = (BucketGroup) thatBucket; + + // Taking the easy / expensive way out for updating size + int thisPrevGroupSize = thisGroup.sizeInChain(); + + BucketGroup thisNewGroup = thisGroup.replaceOrInsertAllInChain(thatGroup); + int thisNewGroupSize = thisNewGroup.sizeInChain(); + + thisBuckets[i] = thisNewGroup; + this.size += (thisNewGroupSize - thisPrevGroupSize); + } + } + } + } + + /* + * Specially optimized version of putAll for the common case of destination map being empty + */ + private final void putAllIntoEmptyMap(OptimizedTagMap that) { + Object[] thisBuckets = this.buckets; + Object[] thatBuckets = that.buckets; + + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check + // elimination + for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { + Object thatBucket = thatBuckets[i]; + + // faster to explicitly null check first, then do instanceof + if (thatBucket == null) { + // do nothing + } else if (thatBucket instanceof BucketGroup) { + // if it is a BucketGroup, then need to clone + BucketGroup thatGroup = (BucketGroup) thatBucket; + + thisBuckets[i] = thatGroup.cloneChain(); + } else { // if ( thatBucket instanceof Entry ) + thisBuckets[i] = thatBucket; + } + } + this.size = that.size; + } + + public final void fillMap(Map map) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + map.put(thisEntry.tag, thisEntry.objectValue()); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.fillMapFromChain(map); + } + } + } + + public final void fillStringMap(Map stringMap) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + stringMap.put(thisEntry.tag, thisEntry.stringValue()); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.fillStringMapFromChain(stringMap); + } + } + } + + @Override + public final Object remove(Object tag) { + if (!(tag instanceof String)) return null; + + Entry entry = this.getAndRemove((String) tag); + return entry == null ? null : entry.objectValue(); + } + + public final boolean remove(String tag) { + return (this.getAndRemove(tag) != null); + } + + @Override + public final Entry getAndRemove(String tag) { + this.checkWriteAccess(); + + Object[] thisBuckets = this.buckets; + + int hash = TagMap.Entry._hash(tag); + int bucketIndex = hash & (thisBuckets.length - 1); + + Object bucket = thisBuckets[bucketIndex]; + // null bucket case - do nothing + if (bucket instanceof Entry) { + Entry existingEntry = (Entry) bucket; + if (existingEntry.matches(tag)) { + thisBuckets[bucketIndex] = null; + + this.size -= 1; + return existingEntry; + } else { + return null; + } + } else if (bucket instanceof BucketGroup) { + BucketGroup lastGroup = (BucketGroup) bucket; + + BucketGroup containingGroup = lastGroup.findContainingGroupInChain(hash, tag); + if (containingGroup == null) { + return null; + } + + Entry existingEntry = containingGroup._remove(hash, tag); + if (containingGroup._isEmpty()) { + this.buckets[bucketIndex] = lastGroup.removeGroupInChain(containingGroup); + } + + this.size -= 1; + return existingEntry; + } + return null; + } + + @Override + public final TagMap copy() { + OptimizedTagMap copy = new OptimizedTagMap(); + copy.putAllIntoEmptyMap(this); + return copy; + } + + public final TagMap immutableCopy() { + if (this.frozen) { + return this; + } else { + return this.copy().freeze(); + } + } + + @Override + public final Iterator iterator() { + return new EntryIterator(this); + } + + @Override + public final Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + @Override + public final void forEach(Consumer consumer) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + consumer.accept(thisEntry); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.forEachInChain(consumer); + } + } + } + + @Override + public final void forEach(T thisObj, BiConsumer consumer) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + consumer.accept(thisObj, thisEntry); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.forEachInChain(thisObj, consumer); + } + } + } + + @Override + public final void forEach( + T thisObj, U otherObj, TriConsumer consumer) { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + + consumer.accept(thisObj, otherObj, thisEntry); + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + thisGroup.forEachInChain(thisObj, otherObj, consumer); + } + } + } + + public final void clear() { + this.checkWriteAccess(); + + Arrays.fill(this.buckets, null); + this.size = 0; + } + + public final OptimizedTagMap freeze() { + this.frozen = true; + + return this; + } + + public boolean isFrozen() { + return this.frozen; + } + + public final void checkWriteAccess() { + if (this.frozen) throw new IllegalStateException("TagMap frozen"); + } + + final void checkIntegrity() { + // Decided to use if ( cond ) throw new IllegalStateException rather than assert + // That was done to avoid the extra static initialization needed for an assertion + // While that's probably an unnecessary optimization, this method is only called in tests + + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object thisBucket = thisBuckets[i]; + + if (thisBucket instanceof Entry) { + Entry thisEntry = (Entry) thisBucket; + int thisHash = thisEntry.hash(); + + int expectedBucket = thisHash & (thisBuckets.length - 1); + if (expectedBucket != i) { + throw new IllegalStateException("incorrect bucket"); + } + } else if (thisBucket instanceof BucketGroup) { + BucketGroup thisGroup = (BucketGroup) thisBucket; + + for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry thisEntry = curGroup._entryAt(i); + if (thisEntry == null) continue; + + int thisHash = thisEntry.hash(); + assert curGroup._hashAt(i) == thisHash; + + int expectedBucket = thisHash & (thisBuckets.length - 1); + if (expectedBucket != i) { + throw new IllegalStateException("incorrect bucket"); + } + } + } + } + } + + if (this.size != this.computeSize()) { + throw new IllegalStateException("incorrect size"); + } + if (this.isEmpty() != this.checkIfEmpty()) { + throw new IllegalStateException("incorrect empty status"); + } + } + + final int computeSize() { + Object[] thisBuckets = this.buckets; + + int size = 0; + for (int i = 0; i < thisBuckets.length; ++i) { + Object curBucket = thisBuckets[i]; + + if (curBucket instanceof Entry) { + size += 1; + } else if (curBucket instanceof BucketGroup) { + BucketGroup curGroup = (BucketGroup) curBucket; + size += curGroup.sizeInChain(); + } + } + return size; + } + + final boolean checkIfEmpty() { + Object[] thisBuckets = this.buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object curBucket = thisBuckets[i]; + + if (curBucket instanceof Entry) { + return false; + } else if (curBucket instanceof BucketGroup) { + BucketGroup curGroup = (BucketGroup) curBucket; + if (!curGroup.isEmptyChain()) return false; + } + } + + return true; + } + + @Override + public Object compute( + String key, BiFunction remappingFunction) { + this.checkWriteAccess(); + + return TagMap.super.compute(key, remappingFunction); + } + + @Override + public Object computeIfAbsent( + String key, Function mappingFunction) { + this.checkWriteAccess(); + + return TagMap.super.computeIfAbsent(key, mappingFunction); + } + + @Override + public Object computeIfPresent( + String key, BiFunction remappingFunction) { + this.checkWriteAccess(); + + return TagMap.super.computeIfPresent(key, remappingFunction); + } + + @Override + public final String toString() { + return toPrettyString(); + } + + /** + * Standard toString implementation - output is similar to {@link java.util.HashMap#toString()} + */ + final String toPrettyString() { + boolean first = true; + + StringBuilder ledger = new StringBuilder(128); + ledger.append('{'); + for (Entry entry : this) { + if (first) { + first = false; + } else { + ledger.append(", "); + } + + ledger.append(entry.tag).append('=').append(entry.stringValue()); + } + ledger.append('}'); + return ledger.toString(); + } + + /** + * toString that more visibility into the internal structure of TagMap - primarily for deep + * debugging + */ + final String toInternalString() { + Object[] thisBuckets = this.buckets; + + StringBuilder ledger = new StringBuilder(128); + for (int i = 0; i < thisBuckets.length; ++i) { + ledger.append('[').append(i).append("] = "); + + Object thisBucket = thisBuckets[i]; + if (thisBucket == null) { + ledger.append("null"); + } else if (thisBucket instanceof Entry) { + ledger.append('{').append(thisBucket).append('}'); + } else if (thisBucket instanceof BucketGroup) { + for (BucketGroup curGroup = (BucketGroup) thisBucket; + curGroup != null; + curGroup = curGroup.prev) { + ledger.append(curGroup).append(" -> "); + } + } + ledger.append('\n'); + } + return ledger.toString(); + } + + abstract static class MapIterator implements Iterator { + private final Object[] buckets; + + private Entry nextEntry; + + private int bucketIndex = -1; + + private BucketGroup group = null; + private int groupIndex = 0; + + MapIterator(OptimizedTagMap map) { + this.buckets = map.buckets; + } + + @Override + public boolean hasNext() { + if (this.nextEntry != null) return true; + + while (this.bucketIndex < this.buckets.length) { + this.nextEntry = this.advance(); + if (this.nextEntry != null) return true; + } + + return false; + } + + Entry nextEntry() { + if (this.nextEntry != null) { + Entry nextEntry = this.nextEntry; + this.nextEntry = null; + return nextEntry; + } + + if (this.hasNext()) { + return this.nextEntry; + } else { + throw new NoSuchElementException(); + } + } + + private final Entry advance() { + while (this.bucketIndex < this.buckets.length) { + if (this.group != null) { + for (++this.groupIndex; this.groupIndex < BucketGroup.LEN; ++this.groupIndex) { + Entry tagEntry = this.group._entryAt(this.groupIndex); + if (tagEntry != null) return tagEntry; + } + + // done processing - that group, go to next group + this.group = this.group.prev; + this.groupIndex = -1; + } + + // if the group is null, then we've finished the current bucket - so advance the bucket + if (this.group == null) { + for (++this.bucketIndex; this.bucketIndex < this.buckets.length; ++this.bucketIndex) { + Object bucket = this.buckets[this.bucketIndex]; + + if (bucket instanceof Entry) { + return (Entry) bucket; + } else if (bucket instanceof BucketGroup) { + this.group = (BucketGroup) bucket; + this.groupIndex = -1; + + break; + } + } + } + } + + return null; + } + } + + static final class EntryIterator extends MapIterator { + EntryIterator(OptimizedTagMap map) { + super(map); + } + + @Override + public Entry next() { + return this.nextEntry(); + } + } + + /** + * BucketGroup is a compromise for performance over a linked list or array + * + *

    + *
  • linked list - would prevent TagEntry-s from being immutable and would limit sharing + * opportunities + *
  • arrays - wouldn't be able to store hashes close together + *
  • parallel arrays (one for hashes & another for entries) would require more allocation + *
+ */ + static final class BucketGroup { + static final int LEN = 4; + + /* + * To make search operations on BucketGroups fast, the hashes for each entry are held inside + * the BucketGroup. This avoids pointer chasing to inspect each Entry object. + *

+ * As a further optimization, the hashes are deliberately placed next to each other. + * The intention is that the hashes will all end up in the same cache line, so loading + * one hash effectively loads the others for free. + *

+ * A hash of zero indicates an available slot, the hashes passed to BucketGroup must be "adjusted" + * hashes which can never be zero. The zero handling is done by TagMap#_hash. + */ + int hash0 = 0; + int hash1 = 0; + int hash2 = 0; + int hash3 = 0; + + Entry entry0 = null; + Entry entry1 = null; + Entry entry2 = null; + Entry entry3 = null; + + BucketGroup prev = null; + + BucketGroup() {} + + /** New group with an entry pointing to existing BucketGroup */ + BucketGroup(int hash0, Entry entry0, BucketGroup prev) { + this.hash0 = hash0; + this.entry0 = entry0; + + this.prev = prev; + } + + /** New group composed of two entries */ + BucketGroup(int hash0, Entry entry0, int hash1, Entry entry1) { + this.hash0 = hash0; + this.entry0 = entry0; + + this.hash1 = hash1; + this.entry1 = entry1; + } + + /** New group composed of 4 entries - used for cloning */ + BucketGroup( + int hash0, + Entry entry0, + int hash1, + Entry entry1, + int hash2, + Entry entry2, + int hash3, + Entry entry3) { + this.hash0 = hash0; + this.entry0 = entry0; + + this.hash1 = hash1; + this.entry1 = entry1; + + this.hash2 = hash2; + this.entry2 = entry2; + + this.hash3 = hash3; + this.entry3 = entry3; + } + + Entry _entryAt(int index) { + switch (index) { + case 0: + return this.entry0; + + case 1: + return this.entry1; + + case 2: + return this.entry2; + + case 3: + return this.entry3; + + // Do not use default case, that creates a 5% cost on entry handling + } + + return null; + } + + int _hashAt(int index) { + switch (index) { + case 0: + return this.hash0; + + case 1: + return this.hash1; + + case 2: + return this.hash2; + + case 3: + return this.hash3; + + // Do not use default case, that creates a 5% cost on entry handling + } + + return 0; + } + + int sizeInChain() { + int size = 0; + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + size += curGroup._size(); + } + return size; + } + + int _size() { + return (this.hash0 == 0 ? 0 : 1) + + (this.hash1 == 0 ? 0 : 1) + + (this.hash2 == 0 ? 0 : 1) + + (this.hash3 == 0 ? 0 : 1); + } + + boolean isEmptyChain() { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + if (!curGroup._isEmpty()) return false; + } + return true; + } + + boolean _isEmpty() { + return (this.hash0 | this.hash1 | this.hash2 | this.hash3) == 0; + } + + BucketGroup findContainingGroupInChain(int hash, String tag) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + if (curGroup._find(hash, tag) != null) return curGroup; + } + return null; + } + + Entry findInChain(int hash, String tag) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + Entry curEntry = curGroup._find(hash, tag); + if (curEntry != null) return curEntry; + } + return null; + } + + Entry _find(int hash, String tag) { + // if ( this._mayContain(hash) ) return null; + + if (this.hash0 == hash && this.entry0.matches(tag)) { + return this.entry0; + } else if (this.hash1 == hash && this.entry1.matches(tag)) { + return this.entry1; + } else if (this.hash2 == hash && this.entry2.matches(tag)) { + return this.entry2; + } else if (this.hash3 == hash && this.entry3.matches(tag)) { + return this.entry3; + } + return null; + } + + BucketGroup replaceOrInsertAllInChain(BucketGroup thatHeadGroup) { + BucketGroup thisOrigHeadGroup = this; + BucketGroup thisNewestHeadGroup = thisOrigHeadGroup; + + for (BucketGroup thatCurGroup = thatHeadGroup; + thatCurGroup != null; + thatCurGroup = thatCurGroup.prev) { + // First phase - tries to replace or insert each entry in the existing bucket chain + // Only need to search the original groups for replacements + // The whole chain is eligible for insertions + boolean handled0 = + (thatCurGroup.hash0 == 0) + || (thisOrigHeadGroup.replaceInChain(thatCurGroup.hash0, thatCurGroup.entry0) + != null) + || thisNewestHeadGroup.insertInChain(thatCurGroup.hash0, thatCurGroup.entry0); + + boolean handled1 = + (thatCurGroup.hash1 == 0) + || (thisOrigHeadGroup.replaceInChain(thatCurGroup.hash1, thatCurGroup.entry1) + != null) + || thisNewestHeadGroup.insertInChain(thatCurGroup.hash1, thatCurGroup.entry1); + + boolean handled2 = + (thatCurGroup.hash2 == 0) + || (thisOrigHeadGroup.replaceInChain(thatCurGroup.hash2, thatCurGroup.entry2) + != null) + || thisNewestHeadGroup.insertInChain(thatCurGroup.hash2, thatCurGroup.entry2); + + boolean handled3 = + (thatCurGroup.hash3 == 0) + || (thisOrigHeadGroup.replaceInChain(thatCurGroup.hash3, thatCurGroup.entry3) + != null) + || thisNewestHeadGroup.insertInChain(thatCurGroup.hash3, thatCurGroup.entry3); + + // Second phase - takes any entries that weren't handled by phase 1 and puts them + // into a new BucketGroup. Since BucketGroups are fixed size, we know that the + // left over entries from one BucketGroup will fit in the new BucketGroup. + if (!handled0 || !handled1 || !handled2 || !handled3) { + // Rather than calling insert one time per entry + // Exploiting the fact that the new group is known to be empty + // And that BucketGroups are allowed to have holes in them (to allow for removal), + // so each unhandled entry from the source group is simply placed in + // the same slot in the new group + BucketGroup thisNewHashGroup = new BucketGroup(); + if (!handled0) { + thisNewHashGroup.hash0 = thatCurGroup.hash0; + thisNewHashGroup.entry0 = thatCurGroup.entry0; + } + if (!handled1) { + thisNewHashGroup.hash1 = thatCurGroup.hash1; + thisNewHashGroup.entry1 = thatCurGroup.entry1; + } + if (!handled2) { + thisNewHashGroup.hash2 = thatCurGroup.hash2; + thisNewHashGroup.entry2 = thatCurGroup.entry2; + } + if (!handled3) { + thisNewHashGroup.hash3 = thatCurGroup.hash3; + thisNewHashGroup.entry3 = thatCurGroup.entry3; + } + thisNewHashGroup.prev = thisNewestHeadGroup; + + thisNewestHeadGroup = thisNewHashGroup; + } + } + + return thisNewestHeadGroup; + } + + Entry replaceInChain(int hash, Entry entry) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + Entry prevEntry = curGroup._replace(hash, entry); + if (prevEntry != null) return prevEntry; + } + return null; + } + + Entry _replace(int hash, Entry entry) { + // if ( this._mayContain(hash) ) return null; + + // first check to see if the item is already present + Entry prevEntry = null; + if (this.hash0 == hash && this.entry0.matches(entry.tag)) { + prevEntry = this.entry0; + this.entry0 = entry; + } else if (this.hash1 == hash && this.entry1.matches(entry.tag)) { + prevEntry = this.entry1; + this.entry1 = entry; + } else if (this.hash2 == hash && this.entry2.matches(entry.tag)) { + prevEntry = this.entry2; + this.entry2 = entry; + } else if (this.hash3 == hash && this.entry3.matches(entry.tag)) { + prevEntry = this.entry3; + this.entry3 = entry; + } + + return prevEntry; + } + + boolean insertInChain(int hash, Entry entry) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + if (curGroup._insert(hash, entry)) return true; + } + return false; + } + + boolean _insert(int hash, Entry entry) { + boolean inserted = false; + if (this.hash0 == 0) { + this.hash0 = hash; + this.entry0 = entry; + + inserted = true; + } else if (this.hash1 == 0) { + this.hash1 = hash; + this.entry1 = entry; + + inserted = true; + } else if (this.hash2 == 0) { + this.hash2 = hash; + this.entry2 = entry; + + inserted = true; + } else if (this.hash3 == 0) { + this.hash3 = hash; + this.entry3 = entry; + + inserted = true; + } + return inserted; + } + + BucketGroup removeGroupInChain(BucketGroup removeGroup) { + BucketGroup firstGroup = this; + if (firstGroup == removeGroup) { + return firstGroup.prev; + } + + for (BucketGroup priorGroup = firstGroup, curGroup = priorGroup.prev; + curGroup != null; + priorGroup = curGroup, curGroup = priorGroup.prev) { + if (curGroup == removeGroup) { + priorGroup.prev = curGroup.prev; + } + } + return firstGroup; + } + + Entry _remove(int hash, String tag) { + Entry existingEntry = null; + if (this.hash0 == hash && this.entry0.matches(tag)) { + existingEntry = this.entry0; + + this.hash0 = 0; + this.entry0 = null; + } else if (this.hash1 == hash && this.entry1.matches(tag)) { + existingEntry = this.entry1; + + this.hash1 = 0; + this.entry1 = null; + } else if (this.hash2 == hash && this.entry2.matches(tag)) { + existingEntry = this.entry2; + + this.hash2 = 0; + this.entry2 = null; + } else if (this.hash3 == hash && this.entry3.matches(tag)) { + existingEntry = this.entry3; + + this.hash3 = 0; + this.entry3 = null; + } + return existingEntry; + } + + void forEachInChain(Consumer consumer) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + curGroup._forEach(consumer); + } + } + + void _forEach(Consumer consumer) { + if (this.entry0 != null) consumer.accept(this.entry0); + if (this.entry1 != null) consumer.accept(this.entry1); + if (this.entry2 != null) consumer.accept(this.entry2); + if (this.entry3 != null) consumer.accept(this.entry3); + } + + void forEachInChain(T thisObj, BiConsumer consumer) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + curGroup._forEach(thisObj, consumer); + } + } + + void _forEach(T thisObj, BiConsumer consumer) { + if (this.entry0 != null) consumer.accept(thisObj, this.entry0); + if (this.entry1 != null) consumer.accept(thisObj, this.entry1); + if (this.entry2 != null) consumer.accept(thisObj, this.entry2); + if (this.entry3 != null) consumer.accept(thisObj, this.entry3); + } + + void forEachInChain(T thisObj, U otherObj, TriConsumer consumer) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + curGroup._forEach(thisObj, otherObj, consumer); + } + } + + void _forEach(T thisObj, U otherObj, TriConsumer consumer) { + if (this.entry0 != null) consumer.accept(thisObj, otherObj, this.entry0); + if (this.entry1 != null) consumer.accept(thisObj, otherObj, this.entry1); + if (this.entry2 != null) consumer.accept(thisObj, otherObj, this.entry2); + if (this.entry3 != null) consumer.accept(thisObj, otherObj, this.entry3); + } + + void fillMapFromChain(Map map) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + curGroup._fillMap(map); + } + } + + void _fillMap(Map map) { + Entry entry0 = this.entry0; + if (entry0 != null) map.put(entry0.tag, entry0.objectValue()); + + Entry entry1 = this.entry1; + if (entry1 != null) map.put(entry1.tag, entry1.objectValue()); + + Entry entry2 = this.entry2; + if (entry2 != null) map.put(entry2.tag, entry2.objectValue()); + + Entry entry3 = this.entry3; + if (entry3 != null) map.put(entry3.tag, entry3.objectValue()); + } + + void fillStringMapFromChain(Map map) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + curGroup._fillStringMap(map); + } + } + + void _fillStringMap(Map map) { + Entry entry0 = this.entry0; + if (entry0 != null) map.put(entry0.tag, entry0.stringValue()); + + Entry entry1 = this.entry1; + if (entry1 != null) map.put(entry1.tag, entry1.stringValue()); + + Entry entry2 = this.entry2; + if (entry2 != null) map.put(entry2.tag, entry2.stringValue()); + + Entry entry3 = this.entry3; + if (entry3 != null) map.put(entry3.tag, entry3.stringValue()); + } + + BucketGroup cloneChain() { + BucketGroup thisClone = this._cloneEntries(); + + BucketGroup thisPriorClone = thisClone; + for (BucketGroup curGroup = this.prev; curGroup != null; curGroup = curGroup.prev) { + BucketGroup newClone = curGroup._cloneEntries(); + thisPriorClone.prev = newClone; + + thisPriorClone = newClone; + } + + return thisClone; + } + + BucketGroup _cloneEntries() { + return new BucketGroup( + this.hash0, this.entry0, + this.hash1, this.entry1, + this.hash2, this.entry2, + this.hash3, this.entry3); + } + + @Override + public String toString() { + StringBuilder ledger = new StringBuilder(32); + ledger.append('['); + for (int i = 0; i < BucketGroup.LEN; ++i) { + if (i != 0) ledger.append(", "); + + ledger.append(this._entryAt(i)); + } + ledger.append(']'); + return ledger.toString(); + } + } + + static final class Entries extends AbstractSet> { + private final OptimizedTagMap map; + + Entries(OptimizedTagMap map) { + this.map = map; + } + + @Override + public int size() { + return this.map.computeSize(); + } + + @Override + public boolean isEmpty() { + return this.map.checkIfEmpty(); + } + + @Override + public Iterator> iterator() { + @SuppressWarnings({"rawtypes", "unchecked"}) + Iterator> iter = (Iterator) this.map.iterator(); + return iter; + } + } + + static final class Keys extends AbstractSet { + final OptimizedTagMap map; + + Keys(OptimizedTagMap map) { + this.map = map; + } + + @Override + public int size() { + return this.map.computeSize(); + } + + @Override + public boolean isEmpty() { + return this.map.checkIfEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.map.containsKey(o); + } + + @Override + public Iterator iterator() { + return new KeysIterator(this.map); + } + } + + static final class KeysIterator extends MapIterator { + KeysIterator(OptimizedTagMap map) { + super(map); + } + + @Override + public String next() { + return this.nextEntry().tag(); + } + } + + static final class Values extends AbstractCollection { + final OptimizedTagMap map; + + Values(OptimizedTagMap map) { + this.map = map; + } + + @Override + public int size() { + return this.map.computeSize(); + } + + @Override + public boolean isEmpty() { + return this.map.checkIfEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.map.containsValue(o); + } + + @Override + public Iterator iterator() { + return new ValuesIterator(this.map); + } + } + + static final class ValuesIterator extends MapIterator { + ValuesIterator(OptimizedTagMap map) { + super(map); + } + + @Override + public Object next() { + return this.nextEntry().objectValue(); + } + } +} + +final class LegacyTagMap extends HashMap implements TagMap { + private static final long serialVersionUID = 77473435283123683L; + + static final LegacyTagMap EMPTY = new LegacyTagMap().freeze(); + + private boolean frozen = false; + + LegacyTagMap() { + super(); + } + + LegacyTagMap(int capacity) { + super(capacity); + } + + LegacyTagMap(LegacyTagMap that) { + super(that); + } + + @Override + public boolean isOptimized() { + return false; + } + + @Override + public void clear() { + this.checkWriteAccess(); + + super.clear(); + } + + public final LegacyTagMap freeze() { + this.frozen = true; + + return this; + } + + public boolean isFrozen() { + return this.frozen; + } + + public final void checkWriteAccess() { + if (this.frozen) throw new IllegalStateException("TagMap frozen"); + } + + @Override + public final TagMap copy() { + return new LegacyTagMap(this); + } + + @Override + public final void fillMap(Map map) { + map.putAll(this); + } + + @Override + public final void fillStringMap(Map stringMap) { + for (Map.Entry entry : this.entrySet()) { + stringMap.put(entry.getKey(), entry.getValue().toString()); + } + } + + @Override + public final void forEach(Consumer consumer) { + for (Map.Entry entry : this.entrySet()) { + consumer.accept(TagMap.Entry.newAnyEntry(entry)); + } + } + + @Override + public final void forEach( + T thisObj, BiConsumer consumer) { + for (Map.Entry entry : this.entrySet()) { + consumer.accept(thisObj, TagMap.Entry.newAnyEntry(entry)); + } + } + + @Override + public final void forEach( + T thisObj, U otherObj, TriConsumer consumer) { + for (Map.Entry entry : this.entrySet()) { + consumer.accept(thisObj, otherObj, TagMap.Entry.newAnyEntry(entry)); + } + } + + @Override + public final TagMap.Entry getAndSet(String tag, Object value) { + Object prior = this.put(tag, value); + return prior == null ? null : TagMap.Entry.newAnyEntry(tag, prior); + } + + @Override + public final TagMap.Entry getAndSet(String tag, CharSequence value) { + Object prior = this.put(tag, value); + return prior == null ? null : TagMap.Entry.newAnyEntry(tag, prior); + } + + @Override + public final TagMap.Entry getAndSet(String tag, boolean value) { + return this.getAndSet(tag, Boolean.valueOf(value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, double value) { + return this.getAndSet(tag, Double.valueOf(value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, float value) { + return this.getAndSet(tag, Float.valueOf(value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, int value) { + return this.getAndSet(tag, Integer.valueOf(value)); + } + + @Override + public final TagMap.Entry getAndSet(String tag, long value) { + return this.getAndSet(tag, Long.valueOf(value)); + } + + @Override + public final TagMap.Entry getAndSet(TagMap.Entry newEntry) { + return this.getAndSet(newEntry.tag(), newEntry.objectValue()); + } + + @Override + public final TagMap.Entry getAndRemove(String tag) { + Object prior = this.remove((Object) tag); + return prior == null ? null : TagMap.Entry.newAnyEntry(tag, prior); + } + + @Override + public final Object getObject(String tag) { + return this.get(tag); + } + + @Override + public final boolean getBoolean(String tag) { + return this.getBooleanOrDefault(tag, false); + } + + @Override + public final boolean getBooleanOrDefault(String tag, boolean defaultValue) { + Object result = this.get(tag); + if (result == null) { + return defaultValue; + } else if (result instanceof Boolean) { + return (Boolean) result; + } else if (result instanceof Number) { + Number number = (Number) result; + return (number.intValue() != 0); + } else { + // deliberately doesn't use defaultValue + return true; + } + } + + @Override + public double getDouble(String tag) { + return this.getDoubleOrDefault(tag, 0D); + } + + @Override + public final double getDoubleOrDefault(String tag, double defaultValue) { + Object value = this.get(tag); + if (value == null) { + return defaultValue; + } else if (value instanceof Number) { + return ((Number) value).doubleValue(); + } else if (value instanceof Boolean) { + return ((Boolean) value) ? 1D : 0D; + } else { + // deliberately doesn't use defaultValue + return 0D; + } + } + + @Override + public final long getLong(String tag) { + return this.getLongOrDefault(tag, 0L); + } + + public final long getLongOrDefault(String tag, long defaultValue) { + Object value = this.get(tag); + if (value == null) { + return defaultValue; + } else if (value instanceof Number) { + return ((Number) value).longValue(); + } else if (value instanceof Boolean) { + return ((Boolean) value) ? 1L : 0L; + } else { + // deliberately doesn't use defaultValue + return 0L; + } + } + + @Override + public final float getFloat(String tag) { + return this.getFloatOrDefault(tag, 0F); + } + + @Override + public final float getFloatOrDefault(String tag, float defaultValue) { + Object value = this.get(tag); + if (value == null) { + return defaultValue; + } else if (value instanceof Number) { + return ((Number) value).floatValue(); + } else if (value instanceof Boolean) { + return ((Boolean) value) ? 1F : 0F; + } else { + // deliberately doesn't use defaultValue + return 0F; + } + } + + @Override + public final int getInt(String tag) { + return this.getIntOrDefault(tag, 0); + } + + @Override + public final int getIntOrDefault(String tag, int defaultValue) { + Object value = this.get(tag); + if (value == null) { + return defaultValue; + } else if (value instanceof Number) { + return ((Number) value).intValue(); + } else if (value instanceof Boolean) { + return ((Boolean) value) ? 1 : 0; + } else { + // deliberately doesn't use defaultValue + return 0; + } + } + + @Override + public final String getString(String tag) { + Object value = this.get(tag); + return value == null ? null : value.toString(); + } + + @Override + public final TagMap.Entry getEntry(String tag) { + Object value = this.get(tag); + return value == null ? null : TagMap.Entry.newAnyEntry(tag, value); + } + + @Override + public void set(String tag, boolean value) { + this.put(tag, Boolean.valueOf(value)); + } + + @Override + public void set(String tag, CharSequence value) { + this.put(tag, (Object) value); + } + + @Override + public void set(String tag, double value) { + this.put(tag, Double.valueOf(value)); + } + + @Override + public void set(String tag, float value) { + this.put(tag, Float.valueOf(value)); + } + + @Override + public void set(String tag, int value) { + this.put(tag, Integer.valueOf(value)); + } + + @Override + public void set(String tag, long value) { + this.put(tag, Long.valueOf(value)); + } + + @Override + public void set(String tag, Object value) { + this.put(tag, value); + } + + @Override + public void set(TagMap.Entry newEntry) { + this.put(newEntry.tag(), newEntry.objectValue()); + } + + @Override + public Object put(String key, Object value) { + this.checkWriteAccess(); + + return super.put(key, value); + } + + @Override + public void putAll(Map m) { + this.checkWriteAccess(); + + super.putAll(m); + } + + @Override + public void putAll(TagMap that) { + this.putAll((Map) that); + } + + @Override + public Object remove(Object key) { + this.checkWriteAccess(); + + return super.remove(key); + } + + @Override + public boolean remove(Object key, Object value) { + this.checkWriteAccess(); + + return super.remove(key, value); + } + + @Override + public boolean remove(String tag) { + this.checkWriteAccess(); + + return (super.remove((Object) tag) != null); + } + + @Override + public Object compute( + String key, BiFunction remappingFunction) { + this.checkWriteAccess(); + + return super.compute(key, remappingFunction); + } + + @Override + public Object computeIfAbsent( + String key, Function mappingFunction) { + this.checkWriteAccess(); + + return super.computeIfAbsent(key, mappingFunction); + } + + @Override + public Object computeIfPresent( + String key, BiFunction remappingFunction) { + this.checkWriteAccess(); + + return super.computeIfPresent(key, remappingFunction); + } + + @Override + public TagMap immutableCopy() { + if (this.isEmpty()) { + return LegacyTagMap.EMPTY; + } else { + return this.copy().freeze(); + } + } + + @Override + public Iterator iterator() { + return new IteratorImpl(this); + } + + @Override + public Stream stream() { + return StreamSupport.stream(this.spliterator(), false); + } + + private final class IteratorImpl implements Iterator { + private Iterator> wrappedIter; + + IteratorImpl(LegacyTagMap legacyMap) { + this.wrappedIter = legacyMap.entrySet().iterator(); + } + + @Override + public final boolean hasNext() { + return this.wrappedIter.hasNext(); + } + + @Override + public final TagMap.Entry next() { + return TagMap.Entry.newAnyEntry(this.wrappedIter.next()); + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/gateway/IGSpanInfo.java b/internal-api/src/main/java/datadog/trace/api/gateway/IGSpanInfo.java index 60bca47f184..00c3b596af1 100644 --- a/internal-api/src/main/java/datadog/trace/api/gateway/IGSpanInfo.java +++ b/internal-api/src/main/java/datadog/trace/api/gateway/IGSpanInfo.java @@ -1,15 +1,15 @@ package datadog.trace.api.gateway; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; -import java.util.Map; public interface IGSpanInfo { DDTraceId getTraceId(); long getSpanId(); - Map getTags(); + TagMap getTags(); AgentSpan setTag(String key, boolean value); diff --git a/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java b/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java index 63cd5afd2c9..31b610887ee 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/NamingSchema.java @@ -1,6 +1,6 @@ package datadog.trace.api.naming; -import java.util.Map; +import datadog.trace.api.TagMap; import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -229,11 +229,10 @@ interface ForPeerService { /** * Calculate the tags to be added to a span to represent the peer service * - * @param unsafeTags the span tags. Map che be mutated - * @return the input tags + * @param unsafeTags the span tags. Map to be mutated */ @Nonnull - Map tags(@Nonnull Map unsafeTags); + void tags(@Nonnull TagMap unsafeTags); } interface ForServer { diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java b/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java index b68496cc1f9..3e76d657069 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v0/PeerServiceNamingV0.java @@ -1,8 +1,7 @@ package datadog.trace.api.naming.v0; +import datadog.trace.api.TagMap; import datadog.trace.api.naming.NamingSchema; -import java.util.Collections; -import java.util.Map; import javax.annotation.Nonnull; public class PeerServiceNamingV0 implements NamingSchema.ForPeerService { @@ -13,7 +12,5 @@ public boolean supports() { @Nonnull @Override - public Map tags(@Nonnull final Map unsafeTags) { - return Collections.emptyMap(); - } + public void tags(@Nonnull final TagMap unsafeTags) {} } diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java index 47ff1ad9d29..827a7489e09 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v1/PeerServiceNamingV1.java @@ -1,6 +1,7 @@ package datadog.trace.api.naming.v1; import datadog.trace.api.DDTags; +import datadog.trace.api.TagMap; import datadog.trace.api.naming.NamingSchema; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -52,8 +53,8 @@ public boolean supports() { return true; } - private void resolve(@Nonnull final Map unsafeTags) { - final Object component = unsafeTags.get(Tags.COMPONENT); + private void resolve(@Nonnull final TagMap unsafeTags) { + final Object component = unsafeTags.getObject(Tags.COMPONENT); // avoid issues with UTF8ByteString or others final String componentString = component == null ? null : component.toString(); final String override = overridesByComponent.get(componentString); @@ -70,15 +71,14 @@ private void resolve(@Nonnull final Map unsafeTags) { resolveBy(unsafeTags, DEFAULT_PRECURSORS); } - private boolean resolveBy( - @Nonnull final Map unsafeTags, @Nullable final String[] precursors) { + private boolean resolveBy(@Nonnull final TagMap unsafeTags, @Nullable final String[] precursors) { if (precursors == null) { return false; } Object value = null; String source = null; for (String precursor : precursors) { - value = unsafeTags.get(precursor); + value = unsafeTags.getObject(precursor); if (value != null) { // we have a match. Use the tag name for the source source = precursor; @@ -90,7 +90,7 @@ private boolean resolveBy( return true; } - private void set(@Nonnull final Map unsafeTags, Object value, String source) { + private void set(@Nonnull final TagMap unsafeTags, Object value, String source) { if (value != null) { unsafeTags.put(Tags.PEER_SERVICE, value); unsafeTags.put(DDTags.PEER_SERVICE_SOURCE, source); @@ -99,13 +99,12 @@ private void set(@Nonnull final Map unsafeTags, Object value, St @Nonnull @Override - public Map tags(@Nonnull final Map unsafeTags) { + public void tags(@Nonnull final TagMap unsafeTags) { // check span.kind eligibility - final Object kind = unsafeTags.get(Tags.SPAN_KIND); + final Object kind = unsafeTags.getObject(Tags.SPAN_KIND); if (Tags.SPAN_KIND_CLIENT.equals(kind) || Tags.SPAN_KIND_PRODUCER.equals(kind)) { // we can calculate the peer service now resolve(unsafeTags); } - return unsafeTags; } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index b09a48d2547..19876207b81 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -7,6 +7,7 @@ import datadog.context.ImplicitContextKeyed; import datadog.trace.api.DDSpanId; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.gateway.IGSpanInfo; import datadog.trace.api.gateway.RequestContext; @@ -91,6 +92,9 @@ default boolean isValid() { @Override AgentSpan setSpanType(final CharSequence type); + @Override + TagMap getTags(); + Object getTag(String key); @Override diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ExtractedSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ExtractedSpan.java index 59bf633b834..8c0013602c4 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ExtractedSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ExtractedSpan.java @@ -1,10 +1,10 @@ package datadog.trace.bootstrap.instrumentation.api; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.gateway.Flow.Action.RequestBlockingAction; import datadog.trace.api.gateway.RequestContext; -import java.util.Collections; import java.util.Map; /** @@ -108,19 +108,18 @@ public boolean isOutbound() { @Override public Object getTag(final String tag) { if (this.spanContext instanceof TagContext) { - return ((TagContext) this.spanContext).getTags().get(tag); + return ((TagContext) this.spanContext).getTags().getObject(tag); } return null; } @Override - public Map getTags() { + public TagMap getTags() { if (this.spanContext instanceof TagContext) { - Map tags = ((TagContext) this.spanContext).getTags(); - //noinspection unchecked,rawtypes - return (Map) tags; + return ((TagContext) this.spanContext).getTags(); + } else { + return TagMap.EMPTY; } - return Collections.emptyMap(); } @Override diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/NoopSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/NoopSpan.java index a4ea00f23de..472744fa4c2 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/NoopSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/NoopSpan.java @@ -1,14 +1,12 @@ package datadog.trace.bootstrap.instrumentation.api; -import static java.util.Collections.emptyMap; - import datadog.trace.api.DDSpanId; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.gateway.Flow.Action.RequestBlockingAction; import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.sampling.PrioritySampling; -import java.util.Map; class NoopSpan extends ImmutableSpan implements AgentSpan { static final NoopSpan INSTANCE = new NoopSpan(); @@ -81,8 +79,8 @@ public String getSpanType() { } @Override - public Map getTags() { - return emptyMap(); + public TagMap getTags() { + return TagMap.EMPTY; } @Override diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContext.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContext.java index f5185d6292c..9a22acaa8cb 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContext.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/TagContext.java @@ -5,6 +5,7 @@ import datadog.trace.api.DDSpanId; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.TracePropagationStyle; import datadog.trace.api.datastreams.PathwayContext; @@ -13,7 +14,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.TreeMap; /** * When calling extract, we allow for grabbing other configured headers as tags. Those tags are @@ -24,7 +24,7 @@ public class TagContext implements AgentSpanContext.Extracted { private static final HttpHeaders EMPTY_HTTP_HEADERS = new HttpHeaders(); private final CharSequence origin; - private Map tags; + private TagMap tags; private List terminatedContextLinks; private Object requestContextDataAppSec; private Object requestContextDataIast; @@ -41,13 +41,13 @@ public TagContext() { this(null, null); } - public TagContext(final CharSequence origin, final Map tags) { + public TagContext(final CharSequence origin, final TagMap tags) { this(origin, tags, null, null, PrioritySampling.UNSET, null, NONE, DDTraceId.ZERO); } public TagContext( final CharSequence origin, - final Map tags, + final TagMap tags, final HttpHeaders httpHeaders, final Map baggage, final int samplingPriority, @@ -164,15 +164,15 @@ public String getCustomIpHeader() { return httpHeaders.customIpHeader; } - public final Map getTags() { - return tags; + public final TagMap getTags() { + return (this.tags == null) ? TagMap.EMPTY : this.tags; } public void putTag(final String key, final String value) { - if (this.tags.isEmpty()) { - this.tags = new TreeMap<>(); + if (this.tags == null) { + this.tags = TagMap.create(4); } - this.tags.put(key, value); + this.tags.set(key, value); } @Override diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/ExtractedSpanTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/ExtractedSpanTest.groovy index ca1957ad6e0..7cdc25a22d9 100644 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/ExtractedSpanTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/ExtractedSpanTest.groovy @@ -1,12 +1,13 @@ package datadog.trace.bootstrap.instrumentation.api import datadog.trace.api.DDTraceId +import datadog.trace.api.TagMap import spock.lang.Specification class ExtractedSpanTest extends Specification { def 'test extracted span from partial tracing context'() { given: - def tags = ['tag-1': 'value-1', 'tag-2': 'value-2'] + def tags = TagMap.fromMap(['tag-1': 'value-1', 'tag-2': 'value-2']) def baggage = ['baggage-1': 'value-1', 'baggage-2': 'value-2'] def traceId = DDTraceId.from(12345) def context = new TagContext('origin', tags, null, baggage, 0, null, null, traceId) diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapBucketGroupTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapBucketGroupTest.java new file mode 100644 index 00000000000..ef1c01387b3 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapBucketGroupTest.java @@ -0,0 +1,382 @@ +package datadog.trace.api; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class TagMapBucketGroupTest { + @Test + public void newGroup() { + TagMap.Entry firstEntry = TagMap.Entry.newIntEntry("foo", 0xDD06); + TagMap.Entry secondEntry = TagMap.Entry.newObjectEntry("bar", "quux"); + + int firstHash = firstEntry.hash(); + int secondHash = secondEntry.hash(); + + OptimizedTagMap.BucketGroup group = + new OptimizedTagMap.BucketGroup( + firstHash, firstEntry, + secondHash, secondEntry); + + assertEquals(firstHash, group._hashAt(0)); + assertEquals(firstEntry, group._entryAt(0)); + + assertEquals(secondEntry.hash(), group._hashAt(1)); + assertEquals(secondEntry, group._entryAt(1)); + + assertFalse(group._isEmpty()); + assertFalse(group.isEmptyChain()); + + assertContainsDirectly(firstEntry, group); + assertContainsDirectly(secondEntry, group); + } + + @Test + public void _insert() { + TagMap.Entry firstEntry = TagMap.Entry.newIntEntry("foo", 0xDD06); + TagMap.Entry secondEntry = TagMap.Entry.newObjectEntry("bar", "quux"); + + int firstHash = firstEntry.hash(); + int secondHash = secondEntry.hash(); + + OptimizedTagMap.BucketGroup group = + new OptimizedTagMap.BucketGroup(firstHash, firstEntry, secondHash, secondEntry); + + TagMap.Entry newEntry = TagMap.Entry.newAnyEntry("baz", "lorem ipsum"); + int newHash = newEntry.hash(); + + assertTrue(group._insert(newHash, newEntry)); + + assertContainsDirectly(newEntry, group); + assertContainsDirectly(firstEntry, group); + assertContainsDirectly(secondEntry, group); + + TagMap.Entry newEntry2 = TagMap.Entry.newDoubleEntry("new", 3.1415926535D); + int newHash2 = newEntry2.hash(); + + assertTrue(group._insert(newHash2, newEntry2)); + + assertContainsDirectly(newEntry2, group); + assertContainsDirectly(newEntry, group); + assertContainsDirectly(firstEntry, group); + assertContainsDirectly(secondEntry, group); + + TagMap.Entry overflowEntry = TagMap.Entry.newDoubleEntry("overflow", 2.718281828D); + int overflowHash = overflowEntry.hash(); + assertFalse(group._insert(overflowHash, overflowEntry)); + + assertDoesntContainDirectly(overflowEntry, group); + } + + @Test + public void _replace() { + TagMap.Entry origEntry = TagMap.Entry.newIntEntry("replaceable", 0xDD06); + TagMap.Entry otherEntry = TagMap.Entry.newObjectEntry("bar", "quux"); + + int origHash = origEntry.hash(); + int otherHash = otherEntry.hash(); + + OptimizedTagMap.BucketGroup group = + new OptimizedTagMap.BucketGroup(origHash, origEntry, otherHash, otherEntry); + assertContainsDirectly(origEntry, group); + assertContainsDirectly(otherEntry, group); + + TagMap.Entry replacementEntry = TagMap.Entry.newBooleanEntry("replaceable", true); + int replacementHash = replacementEntry.hash(); + assertEquals(replacementHash, origHash); + + TagMap.Entry priorEntry = group._replace(origHash, replacementEntry); + assertSame(priorEntry, origEntry); + + assertContainsDirectly(replacementEntry, group); + assertDoesntContainDirectly(priorEntry, group); + + TagMap.Entry dneEntry = TagMap.Entry.newAnyEntry("dne", "not present"); + int dneHash = dneEntry.hash(); + + assertNull(group._replace(dneHash, dneEntry)); + assertDoesntContainDirectly(dneEntry, group); + } + + @Test + public void _remove() { + TagMap.Entry firstEntry = TagMap.Entry.newIntEntry("first", 0xDD06); + TagMap.Entry secondEntry = TagMap.Entry.newObjectEntry("second", "quux"); + + int firstHash = firstEntry.hash(); + int secondHash = secondEntry.hash(); + + OptimizedTagMap.BucketGroup group = + new OptimizedTagMap.BucketGroup( + firstHash, firstEntry, + secondHash, secondEntry); + + assertFalse(group._isEmpty()); + + assertContainsDirectly(firstEntry, group); + assertContainsDirectly(secondEntry, group); + + assertSame(firstEntry, group._remove(firstHash, "first")); + + assertDoesntContainDirectly(firstEntry, group); + assertContainsDirectly(secondEntry, group); + assertFalse(group._isEmpty()); + + assertSame(secondEntry, group._remove(secondHash, "second")); + assertDoesntContainDirectly(secondEntry, group); + + assertTrue(group._isEmpty()); + } + + @Test + public void groupChaining() { + int startingIndex = 10; + OptimizedTagMap.BucketGroup firstGroup = fullGroup(startingIndex); + + for (int offset = 0; offset < OptimizedTagMap.BucketGroup.LEN; ++offset) { + assertChainContainsTag(tag(startingIndex + offset), firstGroup); + } + + TagMap.Entry newEntry = TagMap.Entry.newObjectEntry("new", "new"); + int newHash = newEntry.hash(); + + // This is a test of the process used by TagMap#put + assertNull(firstGroup._replace(newHash, newEntry)); + assertFalse(firstGroup._insert(newHash, newEntry)); + assertDoesntContainDirectly(newEntry, firstGroup); + + OptimizedTagMap.BucketGroup newHeadGroup = + new OptimizedTagMap.BucketGroup(newHash, newEntry, firstGroup); + assertContainsDirectly(newEntry, newHeadGroup); + assertSame(firstGroup, newHeadGroup.prev); + + assertChainContainsTag("new", newHeadGroup); + for (int offset = 0; offset < OptimizedTagMap.BucketGroup.LEN; ++offset) { + assertChainContainsTag(tag(startingIndex + offset), newHeadGroup); + } + } + + @Test + public void removeInChain() { + OptimizedTagMap.BucketGroup firstGroup = fullGroup(10); + OptimizedTagMap.BucketGroup headGroup = fullGroup(20, firstGroup); + + for (int offset = 0; offset < OptimizedTagMap.BucketGroup.LEN; ++offset) { + assertChainContainsTag(tag(10, offset), headGroup); + assertChainContainsTag(tag(20, offset), headGroup); + } + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2); + + String firstRemovedTag = tag(10, 1); + int firstRemovedHash = TagMap.Entry._hash(firstRemovedTag); + + OptimizedTagMap.BucketGroup firstContainingGroup = + headGroup.findContainingGroupInChain(firstRemovedHash, firstRemovedTag); + assertSame(firstContainingGroup, firstGroup); + assertNotNull(firstContainingGroup._remove(firstRemovedHash, firstRemovedTag)); + + assertChainDoesntContainTag(firstRemovedTag, headGroup); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2 - 1); + + String secondRemovedTag = tag(20, 2); + int secondRemovedHash = TagMap.Entry._hash(secondRemovedTag); + + OptimizedTagMap.BucketGroup secondContainingGroup = + headGroup.findContainingGroupInChain(secondRemovedHash, secondRemovedTag); + assertSame(secondContainingGroup, headGroup); + assertNotNull(secondContainingGroup._remove(secondRemovedHash, secondRemovedTag)); + + assertChainDoesntContainTag(secondRemovedTag, headGroup); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2 - 2); + } + + @Test + public void replaceInChain() { + OptimizedTagMap.BucketGroup firstGroup = fullGroup(10); + OptimizedTagMap.BucketGroup headGroup = fullGroup(20, firstGroup); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2); + + TagMap.Entry firstReplacementEntry = TagMap.Entry.newObjectEntry(tag(10, 1), "replaced"); + assertNotNull(headGroup.replaceInChain(firstReplacementEntry.hash(), firstReplacementEntry)); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2); + + TagMap.Entry secondReplacementEntry = TagMap.Entry.newObjectEntry(tag(20, 2), "replaced"); + assertNotNull(headGroup.replaceInChain(secondReplacementEntry.hash(), secondReplacementEntry)); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2); + } + + @Test + public void insertInChain() { + // set-up a chain with some gaps in it + OptimizedTagMap.BucketGroup firstGroup = fullGroup(10); + OptimizedTagMap.BucketGroup headGroup = fullGroup(20, firstGroup); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2); + + String firstHoleTag = tag(10, 1); + int firstHoleHash = TagMap.Entry._hash(firstHoleTag); + firstGroup._remove(firstHoleHash, firstHoleTag); + + String secondHoleTag = tag(20, 2); + int secondHoleHash = TagMap.Entry._hash(secondHoleTag); + headGroup._remove(secondHoleHash, secondHoleTag); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2 - 2); + + String firstNewTag = "new-tag-0"; + TagMap.Entry firstNewEntry = TagMap.Entry.newObjectEntry(firstNewTag, "new"); + int firstNewHash = firstNewEntry.hash(); + + assertTrue(headGroup.insertInChain(firstNewHash, firstNewEntry)); + assertChainContainsTag(firstNewTag, headGroup); + + String secondNewTag = "new-tag-1"; + TagMap.Entry secondNewEntry = TagMap.Entry.newObjectEntry(secondNewTag, "new"); + int secondNewHash = secondNewEntry.hash(); + + assertTrue(headGroup.insertInChain(secondNewHash, secondNewEntry)); + assertChainContainsTag(secondNewTag, headGroup); + + String thirdNewTag = "new-tag-2"; + TagMap.Entry thirdNewEntry = TagMap.Entry.newObjectEntry(secondNewTag, "new"); + int thirdNewHash = secondNewEntry.hash(); + + assertFalse(headGroup.insertInChain(thirdNewHash, thirdNewEntry)); + assertChainDoesntContainTag(thirdNewTag, headGroup); + + assertEquals(headGroup.sizeInChain(), OptimizedTagMap.BucketGroup.LEN * 2); + } + + @Test + public void cloneChain() { + OptimizedTagMap.BucketGroup firstGroup = fullGroup(10); + OptimizedTagMap.BucketGroup secondGroup = fullGroup(20, firstGroup); + OptimizedTagMap.BucketGroup headGroup = fullGroup(30, secondGroup); + + OptimizedTagMap.BucketGroup clonedHeadGroup = headGroup.cloneChain(); + OptimizedTagMap.BucketGroup clonedSecondGroup = clonedHeadGroup.prev; + OptimizedTagMap.BucketGroup clonedFirstGroup = clonedSecondGroup.prev; + + assertGroupContentsStrictEquals(headGroup, clonedHeadGroup); + assertGroupContentsStrictEquals(secondGroup, clonedSecondGroup); + assertGroupContentsStrictEquals(firstGroup, clonedFirstGroup); + } + + @Test + public void removeGroupInChain() { + OptimizedTagMap.BucketGroup tailGroup = fullGroup(10); + OptimizedTagMap.BucketGroup secondGroup = fullGroup(20, tailGroup); + OptimizedTagMap.BucketGroup thirdGroup = fullGroup(30, secondGroup); + OptimizedTagMap.BucketGroup fourthGroup = fullGroup(40, thirdGroup); + OptimizedTagMap.BucketGroup headGroup = fullGroup(50, fourthGroup); + assertChain(headGroup, fourthGroup, thirdGroup, secondGroup, tailGroup); + + // need to test group removal - at head, middle, and tail of the chain + + // middle + assertSame(headGroup, headGroup.removeGroupInChain(thirdGroup)); + assertChain(headGroup, fourthGroup, secondGroup, tailGroup); + + // tail + assertSame(headGroup, headGroup.removeGroupInChain(tailGroup)); + assertChain(headGroup, fourthGroup, secondGroup); + + // head + assertSame(fourthGroup, headGroup.removeGroupInChain(headGroup)); + assertChain(fourthGroup, secondGroup); + } + + static final OptimizedTagMap.BucketGroup fullGroup(int startingIndex) { + TagMap.Entry firstEntry = TagMap.Entry.newObjectEntry(tag(startingIndex), value(startingIndex)); + TagMap.Entry secondEntry = + TagMap.Entry.newObjectEntry(tag(startingIndex + 1), value(startingIndex + 1)); + + OptimizedTagMap.BucketGroup group = + new OptimizedTagMap.BucketGroup( + firstEntry.hash(), firstEntry, secondEntry.hash(), secondEntry); + for (int offset = 2; offset < OptimizedTagMap.BucketGroup.LEN; ++offset) { + TagMap.Entry anotherEntry = + TagMap.Entry.newObjectEntry(tag(startingIndex + offset), value(startingIndex + offset)); + group._insert(anotherEntry.hash(), anotherEntry); + } + return group; + } + + static final OptimizedTagMap.BucketGroup fullGroup( + int startingIndex, OptimizedTagMap.BucketGroup prev) { + OptimizedTagMap.BucketGroup group = fullGroup(startingIndex); + group.prev = prev; + return group; + } + + static final String tag(int startingIndex, int offset) { + return tag(startingIndex + offset); + } + + static final String tag(int i) { + return "tag-" + i; + } + + static final String value(int startingIndex, int offset) { + return value(startingIndex + offset); + } + + static final String value(int i) { + return "value-i"; + } + + static void assertContainsDirectly(TagMap.Entry entry, OptimizedTagMap.BucketGroup group) { + int hash = entry.hash(); + String tag = entry.tag(); + + assertSame(entry, group._find(hash, tag)); + + assertSame(entry, group.findInChain(hash, tag)); + assertSame(group, group.findContainingGroupInChain(hash, tag)); + } + + static void assertDoesntContainDirectly(TagMap.Entry entry, OptimizedTagMap.BucketGroup group) { + for (int i = 0; i < OptimizedTagMap.BucketGroup.LEN; ++i) { + assertNotSame(entry, group._entryAt(i)); + } + } + + static void assertChainContainsTag(String tag, OptimizedTagMap.BucketGroup group) { + int hash = TagMap.Entry._hash(tag); + assertNotNull(group.findInChain(hash, tag)); + } + + static void assertChainDoesntContainTag(String tag, OptimizedTagMap.BucketGroup group) { + int hash = TagMap.Entry._hash(tag); + assertNull(group.findInChain(hash, tag)); + } + + static void assertGroupContentsStrictEquals( + OptimizedTagMap.BucketGroup expected, OptimizedTagMap.BucketGroup actual) { + for (int i = 0; i < OptimizedTagMap.BucketGroup.LEN; ++i) { + assertEquals(expected._hashAt(i), actual._hashAt(i)); + assertSame(expected._entryAt(i), actual._entryAt(i)); + } + } + + static void assertChain(OptimizedTagMap.BucketGroup... chain) { + OptimizedTagMap.BucketGroup cur; + int index; + for (cur = chain[0], index = 0; cur != null; cur = cur.prev, ++index) { + assertSame(chain[index], cur); + } + assertEquals(chain.length, index); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapEntryTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapEntryTest.java new file mode 100644 index 00000000000..95f429737aa --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapEntryTest.java @@ -0,0 +1,580 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.TagMap.Entry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.function.Function; +import java.util.function.Supplier; +import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Since TagMap.Entry is thread safe and has involves complicated multi-thread type resolution code, + * this test uses a different approach to stress ordering different combinations. + * + *

Each test produces a series of check-s encapsulated in a Check object. + * + *

Those checks are then shuffled to simulate different operation orderings - both in single + * threaded and multi-threaded scenarios. + * + * @author dougqh + */ +public class TagMapEntryTest { + @Test + public void objectEntry() { + test( + () -> TagMap.Entry.newObjectEntry("foo", "bar"), + TagMap.Entry.OBJECT, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue("bar", entry), + checkEquals("bar", entry::stringValue), + checkTrue(entry::isObject))); + } + + @Test + public void anyEntry_object() { + test( + () -> TagMap.Entry.newAnyEntry("foo", "bar"), + TagMap.Entry.ANY, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue("bar", entry), + checkTrue(entry::isObject), + checkKey("foo", entry), + checkValue("bar", entry))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void booleanEntry(boolean value) { + test( + () -> TagMap.Entry.newBooleanEntry("foo", value), + TagMap.Entry.BOOLEAN, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkFalse(entry::isNumericPrimitive), + checkType(TagMap.Entry.BOOLEAN, entry))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void booleanEntry_boxed(boolean value) { + test( + () -> TagMap.Entry.newBooleanEntry("foo", Boolean.valueOf(value)), + TagMap.Entry.BOOLEAN, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkFalse(entry::isNumericPrimitive), + checkType(TagMap.Entry.BOOLEAN, entry))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void anyEntry_boolean(boolean value) { + test( + () -> TagMap.Entry.newAnyEntry("foo", Boolean.valueOf(value)), + TagMap.Entry.ANY, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkFalse(entry::isNumericPrimitive), + checkType(TagMap.Entry.BOOLEAN, entry), + checkValue(value, entry))); + } + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -256, -128, -1, 0, 1, 128, 256, Integer.MAX_VALUE}) + public void intEntry(int value) { + test( + () -> TagMap.Entry.newIntEntry("foo", value), + TagMap.Entry.INT, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.INT, entry))); + } + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -256, -128, -1, 0, 1, 128, 256, Integer.MAX_VALUE}) + public void intEntry_boxed(int value) { + test( + () -> TagMap.Entry.newIntEntry("foo", Integer.valueOf(value)), + TagMap.Entry.INT, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.INT, entry))); + } + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -256, -128, -1, 0, 1, 128, 256, Integer.MAX_VALUE}) + public void anyEntry_int(int value) { + test( + () -> TagMap.Entry.newAnyEntry("foo", Integer.valueOf(value)), + TagMap.Entry.ANY, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.INT, entry), + checkValue(value, entry))); + } + + @ParameterizedTest + @ValueSource( + longs = { + Long.MIN_VALUE, + Integer.MIN_VALUE, + -1_048_576L, + -256L, + -128L, + -1L, + 0L, + 1L, + 128L, + 256L, + 1_048_576L, + Integer.MAX_VALUE, + Long.MAX_VALUE + }) + public void longEntry(long value) { + test( + () -> TagMap.Entry.newLongEntry("foo", value), + TagMap.Entry.LONG, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.LONG, entry))); + } + + @ParameterizedTest + @ValueSource( + longs = { + Long.MIN_VALUE, + Integer.MIN_VALUE, + -1_048_576L, + -256L, + -128L, + -1L, + 0L, + 1L, + 128L, + 256L, + 1_048_576L, + Integer.MAX_VALUE, + Long.MAX_VALUE + }) + public void longEntry_boxed(long value) { + test( + () -> TagMap.Entry.newLongEntry("foo", Long.valueOf(value)), + TagMap.Entry.LONG, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.LONG, entry))); + } + + @ParameterizedTest + @ValueSource( + longs = { + Long.MIN_VALUE, + Integer.MIN_VALUE, + -1_048_576L, + -256L, + -128L, + -1L, + 0L, + 1L, + 128L, + 256L, + 1_048_576L, + Integer.MAX_VALUE, + Long.MAX_VALUE + }) + public void anyEntry_long(long value) { + test( + () -> TagMap.Entry.newAnyEntry("foo", Long.valueOf(value)), + TagMap.Entry.ANY, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkTrue(() -> entry.is(TagMap.Entry.LONG)), + checkValue(value, entry))); + } + + @ParameterizedTest + @ValueSource(floats = {Float.MIN_VALUE, -1F, 0F, 1F, 2.171828F, 3.1415F, Float.MAX_VALUE}) + public void floatEntry(float value) { + test( + () -> TagMap.Entry.newFloatEntry("foo", value), + TagMap.Entry.FLOAT, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.FLOAT, entry))); + } + + @ParameterizedTest + @ValueSource(floats = {Float.MIN_VALUE, -1F, 0F, 1F, 2.171828F, 3.1415F, Float.MAX_VALUE}) + public void floatEntry_boxed(float value) { + test( + () -> TagMap.Entry.newFloatEntry("foo", Float.valueOf(value)), + TagMap.Entry.FLOAT, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.FLOAT, entry))); + } + + @ParameterizedTest + @ValueSource(floats = {Float.MIN_VALUE, -1F, 0F, 1F, 2.171828F, 3.1415F, Float.MAX_VALUE}) + public void anyEntry_float(float value) { + test( + () -> TagMap.Entry.newAnyEntry("foo", Float.valueOf(value)), + TagMap.Entry.ANY, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.FLOAT, entry))); + } + + @ParameterizedTest + @ValueSource( + doubles = {Double.MIN_VALUE, Float.MIN_VALUE, -1D, 0D, 1D, Math.E, Math.PI, Double.MAX_VALUE}) + public void doubleEntry(double value) { + test( + () -> TagMap.Entry.newDoubleEntry("foo", value), + TagMap.Entry.DOUBLE, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.DOUBLE, entry))); + } + + @ParameterizedTest + @ValueSource( + doubles = {Double.MIN_VALUE, Float.MIN_VALUE, -1D, 0D, 1D, Math.E, Math.PI, Double.MAX_VALUE}) + public void doubleEntry_boxed(double value) { + test( + () -> TagMap.Entry.newDoubleEntry("foo", Double.valueOf(value)), + TagMap.Entry.DOUBLE, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.DOUBLE, entry))); + } + + @ParameterizedTest + @ValueSource( + doubles = {Double.MIN_VALUE, Float.MIN_VALUE, -1D, 0D, 1D, Math.E, Math.PI, Double.MAX_VALUE}) + public void anyEntry_double(double value) { + test( + () -> TagMap.Entry.newAnyEntry("foo", Double.valueOf(value)), + TagMap.Entry.ANY, + (entry) -> + multiCheck( + checkKey("foo", entry), + checkValue(value, entry), + checkTrue(entry::isNumericPrimitive), + checkType(TagMap.Entry.DOUBLE, entry), + checkValue(value, entry))); + } + + @Test + public void removalChange() { + TagMap.EntryChange removalChange = TagMap.EntryChange.newRemoval("foo"); + assertTrue(removalChange.isRemoval()); + } + + static final int NUM_THREADS = 4; + static final ExecutorService EXECUTOR = + Executors.newFixedThreadPool( + NUM_THREADS, + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "multithreaded-test-runner"); + thread.setDaemon(true); + return thread; + } + }); + + static final void test( + Supplier entrySupplier, byte rawType, Function checks) { + // repeat the test several times to exercise different orderings in this thread + for (int i = 0; i < 10; ++i) { + testSingleThreaded(entrySupplier, rawType, checks); + } + + // same for multi-threaded + for (int i = 0; i < 5; ++i) { + testMultiThreaded(entrySupplier, rawType, checks); + } + } + + static final void testSingleThreaded( + Supplier entrySupplier, byte rawType, Function checkSupplier) { + Entry entry = entrySupplier.get(); + assertEquals(rawType, entry.rawType); + + Check checks = checkSupplier.apply(entry); + checks.check(); + } + + static final void testMultiThreaded( + Supplier entrySupplier, byte rawType, Function checkSupplier) { + Entry sharedEntry = entrySupplier.get(); + assertEquals(rawType, sharedEntry.rawType); + + Check checks = checkSupplier.apply(sharedEntry); + + List> callables = new ArrayList<>(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; ++i) { + // Different shuffle for each thread + Check shuffledChecks = checks.shuffle(); + + callables.add( + () -> { + shuffledChecks.check(); + + return null; + }); + } + + List> futures; + try { + futures = EXECUTOR.invokeAll(callables); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + + throw new IllegalStateException(e); + } + + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new IllegalStateException(cause); + } + } + } + } + + static final void assertChecks(Check check) { + check.check(); + } + + static final Check checkKey(String expected, TagMap.Entry entry) { + return multiCheck(checkEquals(expected, entry::tag), checkEquals(expected, entry::getKey)); + } + + static final Check checkValue(Object expected, TagMap.Entry entry) { + return multiCheck( + checkEquals(expected, entry::objectValue), + checkEquals(expected, entry::getValue), + checkEquals(expected.toString(), entry::stringValue)); + } + + static final Check checkValue(boolean expected, TagMap.Entry entry) { + return multiCheck( + checkEquals(expected, entry::booleanValue), + checkEquals(Boolean.valueOf(expected), entry::objectValue), + checkEquals(expected ? 1 : 0, entry::intValue), + checkEquals(expected ? 1L : 0L, entry::longValue), + checkEquals(expected ? 1D : 0D, entry::doubleValue), + checkEquals(expected ? 1F : 0F, entry::floatValue), + checkEquals(Boolean.toString(expected), entry::stringValue)); + } + + static final Check checkValue(int expected, TagMap.Entry entry) { + return multiCheck( + checkEquals(expected, entry::intValue), + checkEquals((long) expected, entry::longValue), + checkEquals((float) expected, entry::floatValue), + checkEquals((double) expected, entry::doubleValue), + checkEquals(Integer.valueOf(expected), entry::objectValue), + checkEquals(expected != 0, entry::booleanValue), + checkEquals(Integer.toString(expected), entry::stringValue)); + } + + static final Check checkValue(long expected, TagMap.Entry entry) { + return multiCheck( + checkEquals(expected, entry::longValue), + checkEquals((int) expected, entry::intValue), + checkEquals((float) expected, entry::floatValue), + checkEquals((double) expected, entry::doubleValue), + checkEquals(Long.valueOf(expected), entry::objectValue), + checkEquals(expected != 0L, entry::booleanValue), + checkEquals(Long.toString(expected), entry::stringValue)); + } + + static final Check checkValue(double expected, TagMap.Entry entry) { + return multiCheck( + checkEquals(expected, entry::doubleValue), + checkEquals((int) expected, entry::intValue), + checkEquals((long) expected, entry::longValue), + checkEquals((float) expected, entry::floatValue), + checkEquals(Double.valueOf(expected), entry::objectValue), + checkEquals(expected != 0D, entry::booleanValue), + checkEquals(Double.toString(expected), entry::stringValue)); + } + + static final Check checkValue(float expected, TagMap.Entry entry) { + return multiCheck( + checkEquals(expected, entry::floatValue), + checkEquals((int) expected, entry::intValue), + checkEquals((long) expected, entry::longValue), + checkEquals((double) expected, entry::doubleValue), + checkEquals(expected != 0F, entry::booleanValue), + checkEquals(Float.valueOf(expected), entry::objectValue), + checkEquals(Float.toString(expected), entry::stringValue)); + } + + static final Check checkType(byte entryType, TagMap.Entry entry) { + return () -> assertTrue(entry.is(entryType), "type is " + entryType); + } + + static final Check multiCheck(Check... checks) { + return new MultipartCheck(checks); + } + + static final Check checkFalse(Supplier actual) { + return () -> assertFalse(actual.get(), actual.toString()); + } + + static final Check checkTrue(Supplier actual) { + return () -> assertTrue(actual.get(), actual.toString()); + } + + static final Check checkEquals(float expected, Supplier actual) { + return () -> assertEquals(expected, actual.get(), actual.toString()); + } + + static final Check checkEquals(int expected, Supplier actual) { + return () -> assertEquals(expected, actual.get(), actual.toString()); + } + + static final Check checkEquals(double expected, Supplier actual) { + return () -> assertEquals(expected, actual.get(), actual.toString()); + } + + static final Check checkEquals(long expected, Supplier actual) { + return () -> assertEquals(expected, actual.get(), actual.toString()); + } + + static final Check checkEquals(boolean expected, Supplier actual) { + return () -> assertEquals(expected, actual.get(), actual.toString()); + } + + static final Check checkEquals(Object expected, Supplier actual) { + return () -> assertEquals(expected, actual.get(), actual.toString()); + } + + @FunctionalInterface + interface Check { + void check(); + + default Check shuffle() { + return this; + } + + default void flatten(List checkAccumulator) { + checkAccumulator.add(this); + } + } + + static final class MultipartCheck implements Check { + private final Check[] checks; + + MultipartCheck(Check... checks) { + this.checks = checks; + } + + private List shuffleChecks() { + List checkAccumulator = new ArrayList<>(); + for (Check check : this.checks) { + check.flatten(checkAccumulator); + } + + Collections.shuffle(checkAccumulator); + return checkAccumulator; + } + + @Override + public void check() { + for (Check check : this.shuffleChecks()) { + check.check(); + } + } + + @Override + public Check shuffle() { + List shuffled = this.shuffleChecks(); + + return new Check() { + @Override + public void check() { + for (Check check : shuffled) { + check.check(); + } + } + }; + } + + @Override + public void flatten(List checkAccumulator) { + for (Check check : this.checks) { + check.flatten(checkAccumulator); + } + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java new file mode 100644 index 00000000000..6937960f5d5 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapFuzzTest.java @@ -0,0 +1,1220 @@ +package datadog.trace.api; + +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.Test; + +public final class TagMapFuzzTest { + static final int NUM_KEYS = 128; + static final int MAX_NUM_ACTIONS = 32; + + @Test + void test() { + test(generateTest()); + } + + @Test + void testMerge() { + TestCase mapACase = generateTest(); + TestCase mapBCase = generateTest(); + + OptimizedTagMap tagMapA = test(mapACase); + OptimizedTagMap tagMapB = test(mapBCase); + + HashMap hashMapA = new HashMap<>(tagMapA); + HashMap hashMapB = new HashMap<>(tagMapB); + + tagMapA.putAll(tagMapB); + hashMapA.putAll(hashMapB); + + assertMapEquals(hashMapA, tagMapA); + } + + @Test + void priorFailingCase0() { + TagMap map = + makeTagMap( + remove("key-4"), + put("key-71", "values-443049055"), + put("key-2", "values-1227065898"), + put("key-25", "values-696891692"), + put("key-93", "values-763707175"), + put("key-23", "values--1514091210"), + put("key-16", "values--1388742686")); + + MapAction failingAction = + putAllTagMap( + "key-17", + "values--2085338893", + "key-51", + "values-960243765", + "key-33", + "values-1493544499", + "key-46", + "values-697926849", + "key-70", + "values--184054454", + "key-67", + "values-374577326", + "key-9", + "values--742453833", + "key-11", + "values-1606950841", + "key-119", + "values--1914593057", + "key-53", + "values-375236438", + "key-96", + "values--107185569", + "key-47", + "values--1276407408", + "key-125", + "values--1627172151", + "key-110", + "values--1227150283", + "key-15", + "values-380379920", + "key-42", + "values--632271048", + "key-99", + "values--650090786", + "key-8", + "values--1990889145", + "key-103", + "values-1815698254", + "key-120", + "values-279025031", + "key-93", + "values-589795963", + "key-12", + "values--935895941", + "key-105", + "values-94976227", + "key-85", + "values--424609970", + "key-78", + "values-1231948102", + "key-115", + "values-88670282", + "key-26", + "values-733903384", + "key-100", + "values-2102967487", + "key-74", + "values-958598087", + "key-104", + "values-264458254", + "key-125", + "values--1781797927", + "key-27", + "values--562810078", + "key-7", + "values--376776745", + "key-111", + "values-263564677", + "key-50", + "values--859673100", + "key-57", + "values-1585057281", + "key-48", + "values--617889787", + "key-98", + "values--1878108220", + "key-9", + "values--227223375", + "key-59", + "values-1577082288", + "key-94", + "values--268049040", + "key-0", + "values-1708355496", + "key-62", + "values--733451297", + "key-14", + "values-232732747", + "key-4", + "values--406605642", + "key-58", + "values-1772476833", + "key-8", + "values--1155025225", + "key-101", + "values-144480545", + "key-66", + "values-355117269", + "key-121", + "values-1858008722", + "key-33", + "values-1947754079", + "key-1", + "values--1475603838", + "key-125", + "values--2146772243", + "key-117", + "values-852022714", + "key-53", + "values--2039348506", + "key-65", + "values-2011228657", + "key-108", + "values-1581592518", + "key-17", + "values-2129571020", + "key-5", + "values-1106900841", + "key-80", + "values-1791757923", + "key-18", + "values--1992962227", + "key-2", + "values-328863878", + "key-110", + "values-1182949334", + "key-5", + "values-1049403346", + "key-107", + "values-1246502060", + "key-115", + "values-2053931423", + "key-19", + "values--1731179633", + "key-104", + "values--1090790550", + "key-67", + "values--1312759979", + "key-10", + "values-1411135", + "key-109", + "values--1784920248", + "key-20", + "values--827644780", + "key-55", + "values--1610270998", + "key-60", + "values-1287959520", + "key-31", + "values-1686541667", + "key-41", + "values-399844058", + "key-115", + "values-2045201464", + "key-78", + "values-358081227", + "key-57", + "values--1374149269", + "key-65", + "values-1871734555", + "key-124", + "values--211494558", + "key-119", + "values-1757597102", + "key-32", + "values--336988038", + "key-85", + "values-1415155858", + "key-44", + "values-1455425178", + "key-48", + "values--325658059", + "key-68", + "values--793590840", + "key-96", + "values--2010766492", + "key-40", + "values-2007171160", + "key-29", + "values-186945230", + "key-63", + "values-1741962849", + "key-26", + "values-948582805", + "key-31", + "values-47004766", + "key-90", + "values-1304302008", + "key-69", + "values-2120328211", + "key-111", + "values-2053321468", + "key-69", + "values--498524858", + "key-125", + "values--193004619", + "key-30", + "values--1142090845", + "key-15", + "values--1334900170", + "key-33", + "values-1011001500", + "key-55", + "values-452401605", + "key-18", + "values-1260118555", + "key-44", + "values--1109396459", + "key-2", + "values--555647718", + "key-61", + "values-1060742038", + "key-51", + "values--827099230", + "key-62", + "values--1443716296", + "key-16", + "values-534556355", + "key-81", + "values--787910427", + "key-20", + "values-1429697120", + "key-36", + "values--1775988293", + "key-66", + "values-624669635", + "key-25", + "values--684183265", + "key-26", + "values-293626449", + "key-91", + "values--1212867803", + "key-6", + "values-1778251481", + "key-83", + "values-1257370908", + "key-92", + "values--1120490028", + "key-111", + "values-9646496", + "key-90", + "values-1485206899"); + failingAction.apply(map); + failingAction.verify(map); + } + + @Test + void priorFailingCase1() { + TagMap map = makeTagMap(put("key-68", "values--37178328"), put("key-93", "values--2093086281")); + + MapAction failingAction = + putAllTagMap( + "key-36", + "values--1951535044", + "key-59", + "values--1045985660", + "key-68", + "values-1270827526", + "key-65", + "values-440073158", + "key-91", + "values-954365843", + "key-75", + "values-1014366449", + "key-117", + "values--1306617705", + "key-90", + "values-984567966", + "key-120", + "values--1802603599", + "key-56", + "values-319574488", + "key-78", + "values--711288173", + "key-103", + "values-694279462", + "key-84", + "values-1391260657", + "key-59", + "values--484807195", + "key-67", + "values-1675498322", + "key-91", + "values--227731796", + "key-105", + "values--1471022333", + "key-112", + "values--755617374", + "key-117", + "values--668324524", + "key-65", + "values-1165174761", + "key-13", + "values--1947081814", + "key-72", + "values-2032502631", + "key-106", + "values-256372025", + "key-71", + "values--995163162", + "key-92", + "values-972782926", + "key-116", + "values-25012447", + "key-23", + "values--979671053", + "key-94", + "values-367125724", + "key-48", + "values--2011523144", + "key-14", + "values-578926680", + "key-65", + "values-1325737627", + "key-89", + "values-1539092266", + "key-100", + "values--319629978", + "key-53", + "values-1125496255", + "key-2", + "values-1988036327", + "key-105", + "values--1333468536", + "key-37", + "values-351345678", + "key-4", + "values-683252782", + "key-62", + "values--1466612877", + "key-100", + "values-268100559", + "key-104", + "values-3517495", + "key-48", + "values--1588410835", + "key-42", + "values--180653405", + "key-118", + "values--1181647255", + "key-17", + "values-509279769", + "key-33", + "values-298668287", + "key-76", + "values-2062435628", + "key-18", + "values-287811864", + "key-46", + "values--1337930894", + "key-50", + "values-2089310564", + "key-24", + "values--1870293199", + "key-47", + "values--1155431370", + "key-81", + "values--1507929564", + "key-115", + "values-1149614815", + "key-57", + "values--334611395", + "key-86", + "values-146447703", + "key-107", + "values-938082683", + "key-38", + "values-338654203", + "key-40", + "values--376260149", + "key-20", + "values--860844060", + "key-20", + "values-2003129702", + "key-75", + "values--1787311067", + "key-39", + "values--1988768973", + "key-58", + "values--479797619", + "key-16", + "values-571033631", + "key-65", + "values--1867296166", + "key-56", + "values--2071960469", + "key-12", + "values-821930484", + "key-40", + "values--54692885", + "key-65", + "values-328817493", + "key-121", + "values-1276016318", + "key-33", + "values--2081652233", + "key-31", + "values-381335133", + "key-77", + "values-1486312656", + "key-48", + "values--1058365372", + "key-109", + "values--733344537", + "key-85", + "values-1236864082", + "key-35", + "values-2045087594", + "key-49", + "values-1990762822", + "key-38", + "values--1582706513", + "key-18", + "values--626997990", + "key-80", + "values--1995264473", + "key-126", + "values--558193472", + "key-83", + "values-415016167", + "key-53", + "values-1348674948", + "key-58", + "values-612738550", + "key-12", + "values-417676134", + "key-101", + "values--58098778", + "key-127", + "values-1658306930", + "key-17", + "values-985378289", + "key-68", + "values-686600535", + "key-36", + "values-365513638", + "key-87", + "values--1737233661", + "key-67", + "values--1840935230", + "key-8", + "values-540289596", + "key-11", + "values--2045114386", + "key-38", + "values--786598887", + "key-48", + "values-1877144385", + "key-5", + "values-65838542", + "key-18", + "values-263200779", + "key-120", + "values--1500947489", + "key-65", + "values-769990109", + "key-38", + "values-1886840000", + "key-29", + "values--48760205", + "key-61", + "values--1942966789"); + failingAction.apply(map); + failingAction.verify(map); + } + + @Test + void priorFailingCase2() { + TestCase testCase = + new TestCase( + remove("key-34"), + put("key-122", "values-1828753938"), + putAll( + "key-123", + "values--118789056", + "key-28", + "values--751841781", + "key-105", + "values-1663318183", + "key-63", + "values--2036414463", + "key-74", + "values-1584612783", + "key-118", + "values--414681411", + "key-67", + "values-1154668404", + "key-1", + "values--1755856616", + "key-89", + "values--344740102", + "key-110", + "values-1884649283", + "key-1", + "values--1420345075", + "key-22", + "values-1951712698", + "key-103", + "values-488559164", + "key-8", + "values-1180668912", + "key-44", + "values-290310046", + "key-105", + "values--303926067", + "key-26", + "values-910376351", + "key-59", + "values-1600204544", + "key-23", + "values-425861746", + "key-76", + "values--1045446587", + "key-21", + "values-453905226", + "key-1", + "values-286624672", + "key-69", + "values-934359656", + "key-57", + "values--1890465763", + "key-13", + "values--1949062639", + "key-68", + "values-242077328", + "key-42", + "values--1584075743", + "key-46", + "values--1306318288", + "key-31", + "values--848418043", + "key-71", + "values--1547961101", + "key-121", + "values--1493693636", + "key-24", + "values-330660358", + "key-24", + "values--1466871690", + "key-91", + "values--995064376", + "key-18", + "values-1615316779", + "key-124", + "values--296191510", + "key-52", + "values-740309054", + "key-8", + "values-1777392898", + "key-73", + "values-92831985", + "key-13", + "values--1711360891", + "key-114", + "values-1960346620", + "key-44", + "values--1599497099", + "key-107", + "values-668485357", + "key-116", + "values--1792788504"), + put("key-123", "values--1844485682"), + putAll( + "key-64", + "values--1694520036", + "key-17", + "values--469732912", + "key-79", + "values--1293521097", + "key-11", + "values--2000592955", + "key-98", + "values-517073723", + "key-28", + "values-1085152681", + "key-34", + "values-1943586726", + "key-3", + "values-216087991", + "key-97", + "values-222660872", + "key-41", + "values-90906196", + "key-63", + "values--934208984", + "key-57", + "values-327167184", + "key-111", + "values--1059115125", + "key-75", + "values--2031064209", + "key-8", + "values-1924310140", + "key-69", + "values--362514182", + "key-90", + "values-852043703", + "key-98", + "values--998302860", + "key-49", + "values-1658920804", + "key-106", + "values--227162298", + "key-25", + "values-493046373", + "key-52", + "values--555623542", + "key-77", + "values--717275660", + "key-31", + "values-1930766287", + "key-69", + "values--1367213079", + "key-38", + "values--1112081116", + "key-65", + "values--1916889923", + "key-96", + "values-157036191", + "key-127", + "values--302553995", + "key-38", + "values-485874872", + "key-110", + "values--855874569", + "key-39", + "values--390829775", + "key-7", + "values--452123269", + "key-63", + "values--527204905", + "key-101", + "values-166173307", + "key-126", + "values-1050454498", + "key-4", + "values--215188400", + "key-25", + "values-947961204", + "key-42", + "values-145803888", + "key-1", + "values--970532578", + "key-43", + "values--1675493776", + "key-29", + "values-1193328809", + "key-108", + "values-1302659140", + "key-120", + "values--1722764270", + "key-24", + "values--483238806", + "key-53", + "values-611589672", + "key-39", + "values--229429656", + "key-29", + "values--733337788", + "key-9", + "values-736222322", + "key-74", + "values--950770749", + "key-91", + "values-202817768", + "key-95", + "values-500260096", + "key-71", + "values--1798188865", + "key-12", + "values--1936098297", + "key-28", + "values--2116134632", + "key-21", + "values-799594067", + "key-68", + "values--333178107", + "key-50", + "values-445767791", + "key-88", + "values-1307699662", + "key-69", + "values--110615017", + "key-25", + "values-699603233", + "key-101", + "values--2093413536", + "key-91", + "values--2022040839", + "key-45", + "values-888546703", + "key-40", + "values--2140684954", + "key-1", + "values-371033654", + "key-68", + "values--20293415", + "key-59", + "values-697437101", + "key-43", + "values--1145022834", + "key-62", + "values--2125187195", + "key-15", + "values--1062944166", + "key-103", + "values--889634836", + "key-125", + "values-8694763", + "key-101", + "values--281475498", + "key-13", + "values-1972488719", + "key-32", + "values-1900833863", + "key-119", + "values--926978044", + "key-82", + "values-288820151", + "key-78", + "values--303310027", + "key-25", + "values--1284661437", + "key-47", + "values-1624726045", + "key-14", + "values-1658036950", + "key-65", + "values-1629683219", + "key-10", + "values-275264679", + "key-126", + "values--592085694", + "key-32", + "values-1844385705", + "key-85", + "values--1815321660", + "key-72", + "values-918231225", + "key-91", + "values-675699466", + "key-121", + "values--2008685332", + "key-61", + "values--1398921570", + "key-19", + "values-617817427", + "key-122", + "values--793708860", + "key-41", + "values--2027225350", + "key-41", + "values-1194206680", + "key-1", + "values-1116090448", + "key-49", + "values-1662444555", + "key-54", + "values-747436284", + "key-118", + "values--1367237858", + "key-65", + "values-133495093", + "key-73", + "values--1451855551", + "key-43", + "values--357794833", + "key-76", + "values-129403123", + "key-59", + "values--65688873", + "key-22", + "values-480031738", + "key-73", + "values--310815862", + "key-0", + "values--1734944386", + "key-56", + "values--540459893", + "key-38", + "values-1308912555", + "key-2", + "values--2073028093", + "key-14", + "values--693713438", + "key-76", + "values-295450436", + "key-113", + "values--2065146687", + "key-0", + "values-2076623027", + "key-17", + "values--1394046356", + "key-78", + "values--2014478659", + "key-5", + "values--665180960"), + put("key-124", "values-460160716"), + put("key-112", "values--1828904046"), + put("key-41", "values--904162962")); + + Map expected = makeMap(testCase); + OptimizedTagMap actual = makeTagMap(testCase); + + MapAction failingAction = remove("key-127"); + failingAction.apply(expected); + failingAction.verify(expected); + + failingAction.apply(actual); + failingAction.verify(actual); + + assertMapEquals(expected, actual); + } + + public static final TagMap test(MapAction... actions) { + return test(new TestCase(Arrays.asList(actions))); + } + + public static final Map makeMap(TestCase testCase) { + return makeMap(testCase.actions); + } + + public static final Map makeMap(MapAction... actions) { + return makeMap(Arrays.asList(actions)); + } + + public static final Map makeMap(List actions) { + Map map = new HashMap<>(); + for (MapAction action : actions) { + action.apply(map); + } + return map; + } + + public static final OptimizedTagMap makeTagMap(TestCase testCase) { + return makeTagMap(testCase.actions); + } + + public static final OptimizedTagMap makeTagMap(MapAction... actions) { + return makeTagMap(Arrays.asList(actions)); + } + + public static final OptimizedTagMap makeTagMap(List actions) { + OptimizedTagMap map = new OptimizedTagMap(); + for (MapAction action : actions) { + action.apply(map); + } + return map; + } + + public static final OptimizedTagMap test(TestCase test) { + List actions = test.actions(); + + Map hashMap = new HashMap<>(); + OptimizedTagMap tagMap = new OptimizedTagMap(); + + int actionIndex = 0; + try { + for (actionIndex = 0; actionIndex < actions.size(); ++actionIndex) { + MapAction action = actions.get(actionIndex); + + Object expected = action.apply(hashMap); + Object result = action.apply(tagMap); + + assertEquals(expected, result); + + action.verify(tagMap); + + assertMapEquals(hashMap, tagMap); + } + } catch (Error e) { + System.err.println(new TestCase(actions.subList(0, actionIndex + 1))); + + throw e; + } + return tagMap; + } + + public static final TestCase generateTest() { + return generateTest(ThreadLocalRandom.current().nextInt(MAX_NUM_ACTIONS)); + } + + public static final TestCase generateTest(int size) { + List actions = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + actions.add(randomAction()); + } + return new TestCase(actions); + } + + public static final MapAction randomAction() { + float actionSelector = ThreadLocalRandom.current().nextFloat(); + + if (actionSelector > 0.5) { + // 50% puts + return put(randomKey(), randomValue()); + } else if (actionSelector > 0.3) { + // 20% removes + return remove(randomKey()); + } else if (actionSelector > 0.2) { + // 10% putAll TagMap + return putAllTagMap(randomKeysAndValues()); + } else if (actionSelector > 0.02) { + // ~10% putAll HashMap + return putAll(randomKeysAndValues()); + } else { + return clear(); + } + } + + public static final MapAction put(String key, String value) { + return new Put(key, value); + } + + public static final MapAction putAll(String... keysAndValues) { + return new PutAll(keysAndValues); + } + + public static final MapAction putAllTagMap(String... keysAndValues) { + return new PutAllTagMap(keysAndValues); + } + + public static final MapAction clear() { + return Clear.INSTANCE; + } + + public static final MapAction remove(String key) { + return new Remove(key); + } + + static final void assertMapEquals(Map expected, OptimizedTagMap actual) { + // checks entries in both directions to make sure there's full intersection + + for (Map.Entry expectedEntry : expected.entrySet()) { + TagMap.Entry actualEntry = actual.getEntry(expectedEntry.getKey()); + assertNotNull(actualEntry); + assertEquals(expectedEntry.getValue(), actualEntry.getValue()); + } + + for (TagMap.Entry actualEntry : actual) { + Object expectedValue = expected.get(actualEntry.tag()); + assertEquals(expectedValue, actualEntry.objectValue()); + } + + actual.checkIntegrity(); + } + + static final String randomKey() { + return "key-" + ThreadLocalRandom.current().nextInt(NUM_KEYS); + } + + static final String randomValue() { + return "values-" + ThreadLocalRandom.current().nextInt(); + } + + static final String[] randomKeysAndValues() { + int numEntries = ThreadLocalRandom.current().nextInt(NUM_KEYS); + + String[] keysAndValues = new String[numEntries << 1]; + for (int i = 0; i < keysAndValues.length; i += 2) { + keysAndValues[i] = randomKey(); + keysAndValues[i + 1] = randomValue(); + } + return keysAndValues; + } + + static final String literal(String str) { + return "\"" + str + "\""; + } + + static final String literalVarArgs(String... strs) { + StringBuilder builder = new StringBuilder(); + for (String str : strs) { + if (builder.length() != 0) builder.append(','); + builder.append(literal(str)); + } + return builder.toString(); + } + + static final Map mapOf(String... keysAndValues) { + HashMap map = new HashMap<>(keysAndValues.length >> 1); + for (int i = 0; i < keysAndValues.length; i += 2) { + String key = keysAndValues[i]; + String value = keysAndValues[i + 1]; + + map.put(key, value); + } + return map; + } + + static final TagMap tagMapOf(String... keysAndValues) { + OptimizedTagMap map = new OptimizedTagMap(); + for (int i = 0; i < keysAndValues.length; i += 2) { + String key = keysAndValues[i]; + String value = keysAndValues[i + 1]; + + map.set(key, value); + } + map.checkIntegrity(); + + return map; + } + + static final class TestCase { + final List actions; + + TestCase(MapAction... actions) { + this.actions = Arrays.asList(actions); + } + + TestCase(List actions) { + this.actions = actions; + } + + public final List actions() { + return this.actions; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (MapAction action : this.actions) { + builder.append(action).append(',').append('\n'); + } + return builder.toString(); + } + } + + abstract static class MapAction { + public abstract Object apply(Map mapUnderTest); + + public abstract void verify(Map mapUnderTest); + + public abstract String toString(); + } + + static final class Put extends MapAction { + final String key; + final String value; + + Put(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public Object apply(Map mapUnderTest) { + return mapUnderTest.put(this.key, this.value); + } + + @Override + public void verify(Map mapUnderTest) { + assertEquals(this.value, mapUnderTest.get(this.key)); + } + + @Override + public String toString() { + return String.format("put(%s,%s)", literal(this.key), literal(this.value)); + } + } + + static final class PutAll extends MapAction { + final String[] keysAndValues; + final Map map; + + PutAll(String... keysAndValues) { + this.keysAndValues = keysAndValues; + this.map = mapOf(keysAndValues); + } + + @Override + public Object apply(Map mapUnderTest) { + mapUnderTest.putAll(this.map); + + return void.class; + } + + @Override + public void verify(Map mapUnderTest) { + for (Map.Entry entry : this.map.entrySet()) { + assertEquals(entry.getValue(), mapUnderTest.get(entry.getKey())); + } + } + + @Override + public String toString() { + return String.format("putAll(%s)", literalVarArgs(this.keysAndValues)); + } + } + + static final class PutAllTagMap extends MapAction { + final String[] keysAndValues; + final TagMap tagMap; + + PutAllTagMap(String... keysAndValues) { + this.keysAndValues = keysAndValues; + this.tagMap = tagMapOf(keysAndValues); + } + + @Override + public Object apply(Map mapUnderTest) { + mapUnderTest.putAll(this.tagMap); + + return void.class; + } + + @Override + public void verify(Map mapUnderTest) { + for (TagMap.Entry entry : this.tagMap) { + assertEquals(entry.objectValue(), mapUnderTest.get(entry.tag()), "key=" + entry.tag()); + } + } + + @Override + public String toString() { + return String.format("putAllTagMap(%s)", literalVarArgs(this.keysAndValues)); + } + } + + static final class Remove extends MapAction { + final String key; + + Remove(String key) { + this.key = key; + } + + @Override + public Object apply(Map mapUnderTest) { + return mapUnderTest.remove(this.key); + } + + @Override + public void verify(Map mapUnderTest) { + assertFalse(mapUnderTest.containsKey(this.key)); + } + + @Override + public String toString() { + return String.format("remove(%s)", literal(this.key)); + } + } + + static final class Clear extends MapAction { + static final Clear INSTANCE = new Clear(); + + private Clear() {} + + @Override + public Object apply(Map mapUnderTest) { + mapUnderTest.clear(); + + return void.class; + } + + @Override + public void verify(Map mapUnderTest) { + assertTrue(mapUnderTest.isEmpty()); + } + + @Override + public String toString() { + return "clear()"; + } + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapLedgerTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapLedgerTest.java new file mode 100644 index 00000000000..abd2787f03f --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapLedgerTest.java @@ -0,0 +1,272 @@ +package datadog.trace.api; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class TagMapLedgerTest { + static final int SIZE = 32; + + @Test + public void inOrder() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < SIZE; ++i) { + ledger.set(key(i), value(i)); + } + + assertEquals(SIZE, ledger.estimateSize()); + + int i = 0; + for (TagMap.EntryChange entryChange : ledger) { + TagMap.Entry entry = (TagMap.Entry) entryChange; + + assertEquals(key(i), entry.tag()); + assertEquals(value(i), entry.stringValue()); + + ++i; + } + } + + @Test + public void testTypes() { + TagMap.Ledger ledger = TagMap.ledger(); + ledger.set("bool", true); + ledger.set("int", 1); + ledger.set("long", 1L); + ledger.set("float", 1F); + ledger.set("double", 1D); + ledger.set("object", (Object) "string"); + ledger.set("string", "string"); + + assertEntryRawType(TagMap.Entry.BOOLEAN, ledger, "bool"); + assertEntryRawType(TagMap.Entry.INT, ledger, "int"); + assertEntryRawType(TagMap.Entry.LONG, ledger, "long"); + assertEntryRawType(TagMap.Entry.FLOAT, ledger, "float"); + assertEntryRawType(TagMap.Entry.DOUBLE, ledger, "double"); + assertEntryRawType(TagMap.Entry.ANY, ledger, "object"); + assertEntryRawType(TagMap.Entry.OBJECT, ledger, "string"); + } + + @Test + public void buildMutable() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < SIZE; ++i) { + ledger.set(key(i), value(i)); + } + + assertEquals(SIZE, ledger.estimateSize()); + + TagMap map = ledger.build(); + for (int i = 0; i < SIZE; ++i) { + assertEquals(value(i), map.getString(key(i))); + } + assertEquals(SIZE, map.size()); + + // just proving that the map is mutable + map.set(key(1000), value(1000)); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void buildMutable(TagMapType mapType) { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < SIZE; ++i) { + ledger.set(key(i), value(i)); + } + + assertEquals(SIZE, ledger.estimateSize()); + + TagMap map = ledger.build(mapType.factory); + for (int i = 0; i < SIZE; ++i) { + assertEquals(value(i), map.getString(key(i))); + } + assertEquals(SIZE, map.size()); + + // just proving that the map is mutable + map.set(key(1000), value(1000)); + } + + @Test + public void buildImmutable() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < SIZE; ++i) { + ledger.set(key(i), value(i)); + } + + assertEquals(SIZE, ledger.estimateSize()); + + TagMap map = ledger.buildImmutable(); + for (int i = 0; i < SIZE; ++i) { + assertEquals(value(i), map.getString(key(i))); + } + assertEquals(SIZE, map.size()); + + assertFrozen(map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void buildImmutable(TagMapType mapType) { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < SIZE; ++i) { + ledger.set(key(i), value(i)); + } + + assertEquals(SIZE, ledger.estimateSize()); + + TagMap map = ledger.buildImmutable(mapType.factory); + for (int i = 0; i < SIZE; ++i) { + assertEquals(value(i), map.getString(key(i))); + } + assertEquals(SIZE, map.size()); + + assertFrozen(map); + } + + @Test + public void build_empty() { + TagMap.Ledger ledger = TagMap.ledger(); + assertTrue(ledger.isDefinitelyEmpty()); + assertNotSame(TagMap.EMPTY, ledger.build()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void build_empty(TagMapType mapType) { + TagMap.Ledger ledger = TagMap.ledger(); + assertTrue(ledger.isDefinitelyEmpty()); + assertNotSame(mapType.empty(), ledger.build(mapType.factory)); + } + + @Test + public void buildImmutable_empty() { + TagMap.Ledger ledger = TagMap.ledger(); + assertTrue(ledger.isDefinitelyEmpty()); + assertSame(TagMap.EMPTY, ledger.buildImmutable()); + } + + @Test + public void isDefinitelyEmpty_emptyMap() { + TagMap.Ledger ledger = TagMap.ledger(); + ledger.set("foo", "bar"); + ledger.remove("foo"); + + assertFalse(ledger.isDefinitelyEmpty()); + TagMap map = ledger.build(); + assertTrue(map.isEmpty()); + } + + @Test + public void builderExpansion() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < 100; ++i) { + ledger.set(key(i), value(i)); + } + + TagMap map = ledger.build(); + for (int i = 0; i < 100; ++i) { + assertEquals(value(i), map.getString(key(i))); + } + } + + @Test + public void builderPresized() { + TagMap.Ledger ledger = TagMap.ledger(100); + for (int i = 0; i < 100; ++i) { + ledger.set(key(i), value(i)); + } + + TagMap map = ledger.build(); + for (int i = 0; i < 100; ++i) { + assertEquals(value(i), map.getString(key(i))); + } + } + + @Test + public void buildWithRemoves() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < SIZE; ++i) { + ledger.set(key(i), value(i)); + } + + for (int i = 0; i < SIZE; i += 2) { + ledger.remove(key(i)); + } + + TagMap map = ledger.build(); + for (int i = 0; i < SIZE; ++i) { + if ((i % 2) == 0) { + assertNull(map.getString(key(i))); + } else { + assertEquals(value(i), map.getString(key(i))); + } + } + } + + @Test + public void smartRemoval_existingCase() { + TagMap.Ledger ledger = TagMap.ledger(); + ledger.set("foo", "bar"); + ledger.smartRemove("foo"); + + assertTrue(ledger.containsRemovals()); + } + + @Test + public void smartRemoval_missingCase() { + TagMap.Ledger ledger = TagMap.ledger(); + ledger.smartRemove("foo"); + + assertFalse(ledger.containsRemovals()); + } + + @Test + public void reset() { + TagMap.Ledger ledger = TagMap.ledger(2); + + ledger.set(key(0), value(0)); + TagMap map0 = ledger.build(); + + ledger.reset(); + + ledger.set(key(1), value(1)); + TagMap map1 = ledger.build(); + + assertEquals(value(0), map0.getString(key(0))); + assertNull(map1.getString(key(0))); + + assertNull(map0.getString(key(1))); + assertEquals(value(1), map1.getString(key(1))); + } + + static final String key(int i) { + return "key-" + i; + } + + static final String value(int i) { + return "value-" + i; + } + + static final void assertEntryRawType(byte expectedType, TagMap.Ledger ledger, String tag) { + TagMap.Entry entry = ledger.findLastEntry(tag); + assertEquals(expectedType, entry.rawType); + } + + static final void assertFrozen(TagMap map) { + IllegalStateException ex = null; + try { + map.set("foo", "bar"); + } catch (IllegalStateException e) { + ex = e; + } + assertNotNull(ex); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTest.java b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java new file mode 100644 index 00000000000..7259eb84281 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java @@ -0,0 +1,896 @@ +package datadog.trace.api; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class TagMapTest { + // size is chosen to make sure to stress all types of collisions in the Map + static final int MANY_SIZE = 256; + + // static function tests - mostly exist to satisfy coverage checker + @Test + public void fromMap_emptyMap() { + Map emptyMap = Collections.emptyMap(); + + TagMap tagMap = TagMap.fromMap(emptyMap); + assertEquals(tagMap.size(), 0); + assertTrue(tagMap.isEmpty()); + + assertFalse(tagMap.isFrozen()); + } + + @Test + public void fromMap_nonEmptyMap() { + // mostly exists to satisfy coverage checker + HashMap origMap = new HashMap<>(); + origMap.put("foo", "bar"); + origMap.put("baz", "quux"); + + TagMap tagMap = TagMap.fromMap(origMap); + assertEquals(tagMap.size(), origMap.size()); + + assertEquals(tagMap.get("foo"), origMap.get("foo")); + assertEquals(tagMap.get("baz"), origMap.get("baz")); + + assertFalse(tagMap.isFrozen()); + } + + @Test + public void fromMapImmutable_empty() { + Map emptyMap = Collections.emptyMap(); + + TagMap tagMap = TagMap.fromMapImmutable(emptyMap); + assertEquals(tagMap.size(), 0); + assertTrue(tagMap.isEmpty()); + + assertTrue(tagMap.isFrozen()); + } + + @Test + public void fromMapImmutable_nonEmptyMap() { + // mostly exists to satisfy coverage checker + HashMap origMap = new HashMap<>(); + origMap.put("foo", "bar"); + origMap.put("baz", "quux"); + + TagMap tagMap = TagMap.fromMapImmutable(origMap); + assertEquals(tagMap.size(), origMap.size()); + + assertEquals(tagMap.get("foo"), origMap.get("foo")); + assertEquals(tagMap.get("baz"), origMap.get("baz")); + + assertTrue(tagMap.isFrozen()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void optimizedFactory(boolean optimized) { + TagMapFactory factory = TagMapFactory.createFactory(optimized); + + TagMap unsizedMap = factory.create(); + assertEquals(optimized, unsizedMap.isOptimized()); + + TagMap sizedMap = factory.create(32); + assertEquals(optimized, sizedMap.isOptimized()); + + TagMap emptyMap = factory.empty(); + assertEquals(optimized, emptyMap.isOptimized()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void map_put(TagMapType mapType) { + TagMap map = mapType.create(); + + Object prev = map.put("foo", "bar"); + assertNull(prev); + + assertEntry("foo", "bar", map); + + assertSize(1, map); + assertNotEmpty(map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void booleanEntry(TagMapType mapType) { + boolean first = false; + boolean second = true; + + TagMap map = mapType.create(); + map.set("bool", first); + + TagMap.Entry firstEntry = map.getEntry("bool"); + if (map.isOptimized()) { + assertEquals(TagMap.Entry.BOOLEAN, firstEntry.rawType); + } + + assertEquals(first, firstEntry.booleanValue()); + assertEquals(first, map.getBoolean("bool")); + + TagMap.Entry priorEntry = map.getAndSet("bool", second); + if (map.isOptimized()) { + assertSame(priorEntry, firstEntry); + } + assertEquals(first, priorEntry.booleanValue()); + + TagMap.Entry newEntry = map.getEntry("bool"); + assertEquals(second, newEntry.booleanValue()); + + assertEquals(false, map.getBoolean("unset")); + assertEquals(true, map.getBooleanOrDefault("unset", true)); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void numericZeroToBooleanCoercion(TagMapType mapType) { + TagMap map = + TagMap.ledger() + .set("int", 0) + .set("intObj", Integer.valueOf(0)) + .set("long", 0L) + .set("longObj", Long.valueOf(0L)) + .set("float", 0F) + .set("floatObj", Float.valueOf(0F)) + .set("double", 0D) + .set("doubleObj", Double.valueOf(0D)) + .build(mapType.factory); + + assertEquals(false, map.getBoolean("int")); + assertEquals(false, map.getBoolean("intObj")); + assertEquals(false, map.getBoolean("long")); + assertEquals(false, map.getBoolean("longObj")); + assertEquals(false, map.getBoolean("float")); + assertEquals(false, map.getBoolean("floatObj")); + assertEquals(false, map.getBoolean("double")); + assertEquals(false, map.getBoolean("doubleObj")); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void numericNonZeroToBooleanCoercion(TagMapType mapType) { + TagMap map = + TagMap.ledger() + .set("int", 1) + .set("intObj", Integer.valueOf(1)) + .set("long", 1L) + .set("longObj", Long.valueOf(1L)) + .set("float", 1F) + .set("floatObj", Float.valueOf(1F)) + .set("double", 1D) + .set("doubleObj", Double.valueOf(1D)) + .build(mapType.factory); + + assertEquals(true, map.getBoolean("int")); + assertEquals(true, map.getBoolean("intObj")); + assertEquals(true, map.getBoolean("long")); + assertEquals(true, map.getBoolean("longObj")); + assertEquals(true, map.getBoolean("float")); + assertEquals(true, map.getBoolean("floatObj")); + assertEquals(true, map.getBoolean("double")); + assertEquals(true, map.getBoolean("doubleObj")); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void objectToBooleanCoercion(TagMapType mapType) { + TagMap map = + TagMap.ledger() + .set("obj", new Object()) + .set("trueStr", "true") + .set("falseStr", "false") + .build(mapType.factory); + + assertEquals(true, map.getBoolean("obj")); + assertEquals(true, map.getBoolean("trueStr")); + assertEquals(true, map.getBoolean("falseStr")); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void booleanToNumericCoercion_true(TagMapType mapType) { + TagMap map = TagMap.ledger().set("true", true).build(mapType.factory); + + assertEquals(1, map.getInt("true")); + assertEquals(1L, map.getLong("true")); + assertEquals(1F, map.getFloat("true")); + assertEquals(1D, map.getDouble("true")); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void booleanToNumericCoercion_false(TagMapType mapType) { + TagMap map = TagMap.ledger().set("false", false).build(mapType.factory); + + assertEquals(0, map.getInt("false")); + assertEquals(0L, map.getLong("false")); + assertEquals(0F, map.getFloat("false")); + assertEquals(0D, map.getDouble("false")); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void emptyToPrimitiveCoercion(TagMapType mapType) { + TagMap map = mapType.empty(); + + assertEquals(false, map.getBoolean("dne")); + assertEquals(0, map.getInt("dne")); + assertEquals(0L, map.getLong("dne")); + assertEquals(0F, map.getFloat("dne")); + assertEquals(0D, map.getDouble("dne")); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void intEntry(TagMapType mapType) { + int first = 3142; + int second = 2718; + + TagMap map = mapType.create(); + map.set("int", first); + + TagMap.Entry firstEntry = map.getEntry("int"); + if (map.isOptimized()) { + assertEquals(TagMap.Entry.INT, firstEntry.rawType); + } + + assertEquals(first, firstEntry.intValue()); + assertEquals(first, map.getInt("int")); + + TagMap.Entry priorEntry = map.getAndSet("int", second); + if (map.isOptimized()) { + assertSame(priorEntry, firstEntry); + } + assertEquals(first, priorEntry.intValue()); + + TagMap.Entry newEntry = map.getEntry("int"); + assertEquals(second, newEntry.intValue()); + + assertEquals(0, map.getInt("unset")); + assertEquals(21, map.getIntOrDefault("unset", 21)); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void longEntry(TagMapType mapType) { + long first = 3142L; + long second = 2718L; + + TagMap map = mapType.create(); + map.set("long", first); + + TagMap.Entry firstEntry = map.getEntry("long"); + if (map.isOptimized()) { + assertEquals(TagMap.Entry.LONG, firstEntry.rawType); + } + + assertEquals(first, firstEntry.longValue()); + assertEquals(first, map.getLong("long")); + + TagMap.Entry priorEntry = map.getAndSet("long", second); + if (map.isOptimized()) { + assertSame(priorEntry, firstEntry); + } + assertEquals(first, priorEntry.longValue()); + + TagMap.Entry newEntry = map.getEntry("long"); + assertEquals(second, newEntry.longValue()); + + assertEquals(0L, map.getLong("unset")); + assertEquals(21L, map.getLongOrDefault("unset", 21L)); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void floatEntry(TagMapType mapType) { + float first = 3.14F; + float second = 2.718F; + + TagMap map = mapType.create(); + map.set("float", first); + + TagMap.Entry firstEntry = map.getEntry("float"); + if (map.isOptimized()) { + assertEquals(TagMap.Entry.FLOAT, firstEntry.rawType); + } + + assertEquals(first, firstEntry.floatValue()); + assertEquals(first, map.getFloat("float")); + + TagMap.Entry priorEntry = map.getAndSet("float", second); + if (map.isOptimized()) { + assertSame(priorEntry, firstEntry); + } + assertEquals(first, priorEntry.floatValue()); + + TagMap.Entry newEntry = map.getEntry("float"); + assertEquals(second, newEntry.floatValue()); + + assertEquals(0F, map.getFloat("unset")); + assertEquals(2.718F, map.getFloatOrDefault("unset", 2.718F)); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void doubleEntry(TagMapType mapType) { + double first = Math.PI; + double second = Math.E; + + TagMap map = mapType.create(); + map.set("double", Math.PI); + + TagMap.Entry firstEntry = map.getEntry("double"); + if (map.isOptimized()) { + assertEquals(TagMap.Entry.DOUBLE, firstEntry.rawType); + } + + assertEquals(first, firstEntry.doubleValue()); + assertEquals(first, map.getDouble("double")); + + TagMap.Entry priorEntry = map.getAndSet("double", second); + if (map.isOptimized()) { + assertSame(priorEntry, firstEntry); + } + assertEquals(first, priorEntry.doubleValue()); + + TagMap.Entry newEntry = map.getEntry("double"); + assertEquals(second, newEntry.doubleValue()); + + assertEquals(0D, map.getDouble("unset")); + assertEquals(2.718D, map.getDoubleOrDefault("unset", 2.718D)); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void empty(TagMapType mapType) { + TagMap empty = mapType.empty(); + assertFrozen(empty); + + assertNull(empty.getEntry("foo")); + assertSize(0, empty); + assertEmpty(empty); + } + + @ParameterizedTest + @EnumSource(TagMapTypePair.class) + public void putAll_empty(TagMapTypePair mapTypePair) { + // TagMap.EMPTY breaks the rules and uses a different size bucket array + // This test is just to verify that the commonly use putAll still works with EMPTY + TagMap newMap = mapTypePair.firstType.create(); + newMap.putAll(mapTypePair.secondType.empty()); + + assertSize(0, newMap); + assertEmpty(newMap); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void clear(TagMapType mapType) { + int size = randomSize(); + + TagMap map = createTagMap(mapType, size); + assertSize(size, map); + assertNotEmpty(map); + + map.clear(); + assertSize(0, map); + assertEmpty(map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void map_put_replacement(TagMapType mapType) { + TagMap map = mapType.create(); + Object prev1 = map.put("foo", "bar"); + assertNull(prev1); + + assertEntry("foo", "bar", map); + assertSize(1, map); + assertNotEmpty(map); + + Object prev2 = map.put("foo", "baz"); + assertSize(1, map); + assertEquals("bar", prev2); + + assertEntry("foo", "baz", map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void map_remove(TagMapType mapType) { + TagMap map = mapType.create(); + + Object prev1 = map.remove((Object) "foo"); + assertNull(prev1); + + map.put("foo", "bar"); + assertEntry("foo", "bar", map); + assertSize(1, map); + assertNotEmpty(map); + + Object prev2 = map.remove((Object) "foo"); + assertEquals("bar", prev2); + assertSize(0, map); + assertEmpty(map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void freeze(TagMapType mapType) { + TagMap map = mapType.create(); + map.put("foo", "bar"); + + assertEntry("foo", "bar", map); + + map.freeze(); + + assertFrozen( + () -> { + map.remove("foo"); + }); + + assertEntry("foo", "bar", map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void emptyMap(TagMapType mapType) { + TagMap map = mapType.empty(); + + assertSize(0, map); + assertEmpty(map); + + assertFrozen(map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void putMany(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + for (int i = 0; i < size; ++i) { + assertEntry(key(i), value(i), map); + } + + assertNotEmpty(map); + assertSize(size, map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void copyMany(TagMapType mapType) { + int size = randomSize(); + TagMap orig = createTagMap(mapType, size); + assertSize(size, orig); + + TagMap copy = orig.copy(); + orig.clear(); // doing this to make sure that copied isn't modified + + for (int i = 0; i < size; ++i) { + assertEntry(key(i), value(i), copy); + } + assertSize(size, copy); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void immutableCopy(TagMapType mapType) { + int size = randomSize(); + TagMap orig = createTagMap(mapType, size); + + TagMap immutableCopy = orig.immutableCopy(); + orig.clear(); // doing this to make sure that copied isn't modified + + for (int i = 0; i < size; ++i) { + assertEntry(key(i), value(i), immutableCopy); + } + assertSize(size, immutableCopy); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void replaceALot(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + for (int i = 0; i < size; ++i) { + int index = ThreadLocalRandom.current().nextInt(size); + + map.put(key(index), altValue(index)); + assertEquals(altValue(index), map.get(key(index))); + } + } + + @ParameterizedTest + @EnumSource(TagMapTypePair.class) + public void shareEntry(TagMapTypePair mapTypePair) { + TagMap orig = mapTypePair.firstType.create(); + orig.set("foo", "bar"); + + TagMap dest = mapTypePair.secondType.create(); + dest.set(orig.getEntry("foo")); + + assertEquals(orig.getEntry("foo"), dest.getEntry("foo")); + if (mapTypePair == TagMapTypePair.BOTH_OPTIMIZED) { + assertSame(orig.getEntry("foo"), dest.getEntry("foo")); + } + } + + @ParameterizedTest + @EnumSource(TagMapTypePair.class) + public void putAll_clobberAll(TagMapTypePair mapTypePair) { + int size = randomSize(); + TagMap orig = createTagMap(mapTypePair.firstType, size); + assertSize(size, orig); + + TagMap dest = mapTypePair.secondType.create(); + for (int i = size - 1; i >= 0; --i) { + dest.set(key(i), altValue(i)); + } + + // This should clobber all the values in dest + dest.putAll(orig); + + for (int i = 0; i < size; ++i) { + assertEntry(key(i), value(i), dest); + } + assertSize(size, dest); + } + + @ParameterizedTest + @EnumSource(TagMapTypePair.class) + public void putAll_clobberAndExtras(TagMapTypePair mapTypePair) { + int size = randomSize(); + TagMap orig = createTagMap(mapTypePair.firstType, size); + assertSize(size, orig); + + TagMap dest = mapTypePair.secondType.create(); + for (int i = size / 2 - 1; i >= 0; --i) { + dest.set(key(i), altValue(i)); + } + + // This should clobber all the values in dest + dest.putAll(orig); + + for (int i = 0; i < size; ++i) { + assertEntry(key(i), value(i), dest); + } + + assertSize(size, dest); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void removeMany(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + for (int i = 0; i < size; ++i) { + assertEntry(key(i), value(i), map); + } + + assertNotEmpty(map); + assertSize(size, map); + + for (int i = 0; i < size; ++i) { + Object removedValue = map.remove((Object) key(i)); + assertEquals(value(i), removedValue); + + // not doing exhaustive size checks + assertEquals(size - i - 1, map.size()); + } + + assertEmpty(map); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void fillMap(TagMapType mapType) { + int size = randomSize(); + TagMap map = mapType.create(); + for (int i = 0; i < size; ++i) { + map.set(key(i), i); + } + + HashMap hashMap = new HashMap<>(); + map.fillMap(hashMap); + + for (int i = 0; i < size; ++i) { + assertEquals(Integer.valueOf(i), hashMap.remove(key(i))); + } + assertTrue(hashMap.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void fillStringMap(TagMapType mapType) { + int size = randomSize(); + TagMap map = mapType.create(); + for (int i = 0; i < size; ++i) { + map.set(key(i), i); + } + + HashMap hashMap = new HashMap<>(); + map.fillStringMap(hashMap); + + for (int i = 0; i < size; ++i) { + assertEquals(Integer.toString(i), hashMap.remove(key(i))); + } + assertTrue(hashMap.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void iterator(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Set keys = new HashSet<>(); + for (TagMap.Entry entry : map) { + // makes sure that each key is visited once and only once + assertTrue(keys.add(entry.tag())); + } + + for (int i = 0; i < size; ++i) { + // make sure the key was present + assertTrue(keys.remove(key(i))); + } + + // no extraneous keys + assertTrue(keys.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void forEachConsumer(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Set keys = new HashSet<>(size); + map.forEach((entry) -> keys.add(entry.tag())); + + for (int i = 0; i < size; ++i) { + // make sure the key was present + assertTrue(keys.remove(key(i))); + } + + // no extraneous keys + assertTrue(keys.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void forEachBiConsumer(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Set keys = new HashSet<>(size); + map.forEach(keys, (k, entry) -> k.add(entry.tag())); + + for (int i = 0; i < size; ++i) { + // make sure the key was present + assertTrue(keys.remove(key(i))); + } + + // no extraneous keys + assertTrue(keys.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void forEachTriConsumer(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Set keys = new HashSet<>(size); + map.forEach(keys, "hi", (k, msg, entry) -> keys.add(entry.tag())); + + for (int i = 0; i < size; ++i) { + // make sure the key was present + assertTrue(keys.remove(key(i))); + } + + // no extraneous keys + assertTrue(keys.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void entrySet(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Set> actualEntries = map.entrySet(); + assertEquals(size, actualEntries.size()); + assertFalse(actualEntries.isEmpty()); + + Set expectedKeys = expectedKeys(size); + for (Map.Entry entry : actualEntries) { + assertTrue(expectedKeys.remove(entry.getKey())); + } + assertTrue(expectedKeys.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void keySet(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Set actualKeys = map.keySet(); + assertEquals(size, actualKeys.size()); + assertFalse(actualKeys.isEmpty()); + + Set expectedKeys = expectedKeys(size); + for (String key : actualKeys) { + assertTrue(expectedKeys.remove(key)); + } + assertTrue(expectedKeys.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void values(TagMapType mapType) { + int size = randomSize(); + TagMap map = createTagMap(mapType, size); + + Collection actualValues = map.values(); + assertEquals(size, actualValues.size()); + assertFalse(actualValues.isEmpty()); + + Set expectedValues = expectedValues(size); + for (Object value : map.values()) { + assertTrue(expectedValues.remove(value)); + } + assertTrue(expectedValues.isEmpty()); + } + + @ParameterizedTest + @EnumSource(TagMapType.class) + public void _toString(TagMapType mapType) { + int size = 4; + TagMap map = createTagMap(mapType, size); + assertEquals("{key-1=value-1, key-0=value-0, key-3=value-3, key-2=value-2}", map.toString()); + } + + static final int randomSize() { + return ThreadLocalRandom.current().nextInt(1, MANY_SIZE); + } + + static final TagMap createTagMap(TagMapType mapType) { + return createTagMap(mapType, randomSize()); + } + + static final TagMap createTagMap(TagMapType mapType, int size) { + TagMap map = mapType.create(); + for (int i = 0; i < size; ++i) { + map.set(key(i), value(i)); + } + return map; + } + + static final Set expectedKeys(int size) { + Set set = new HashSet(size); + for (int i = 0; i < size; ++i) { + set.add(key(i)); + } + return set; + } + + static final Set expectedValues(int size) { + Set set = new HashSet(size); + for (int i = 0; i < size; ++i) { + set.add(value(i)); + } + return set; + } + + static final String key(int i) { + return "key-" + i; + } + + static final String value(int i) { + return "value-" + i; + } + + static final String altValue(int i) { + return "alt-value-" + i; + } + + static final int count(Iterable iterable) { + return count(iterable.iterator()); + } + + static final int count(Iterator iter) { + int count; + for (count = 0; iter.hasNext(); ++count) { + iter.next(); + } + return count; + } + + static final void assertEntry(String key, String value, TagMap map) { + TagMap.Entry entry = map.getEntry(key); + assertNotNull(entry); + + assertEquals(key, entry.tag()); + assertEquals(key, entry.getKey()); + + assertEquals(value, entry.objectValue()); + assertTrue(entry.isObject()); + assertEquals(value, entry.getValue()); + + assertEquals(value, entry.stringValue()); + + assertTrue(map.containsKey(key)); + assertTrue(map.keySet().contains(key)); + + assertTrue(map.containsValue(value)); + assertTrue(map.values().contains(value)); + } + + static final void assertSize(int size, TagMap map) { + if (map instanceof OptimizedTagMap) { + assertEquals(size, ((OptimizedTagMap) map).computeSize()); + } + assertEquals(size, map.size()); + + assertEquals(size, count(map)); + assertEquals(size, map.keySet().size()); + assertEquals(size, map.values().size()); + assertEquals(size, count(map.keySet())); + assertEquals(size, count(map.values())); + } + + static final void assertNotEmpty(TagMap map) { + if (map instanceof OptimizedTagMap) { + assertFalse(((OptimizedTagMap) map).checkIfEmpty()); + } + assertFalse(map.isEmpty()); + } + + static final void assertEmpty(TagMap map) { + if (map instanceof OptimizedTagMap) { + assertTrue(((OptimizedTagMap) map).checkIfEmpty()); + } + assertTrue(map.isEmpty()); + } + + static final void assertFrozen(TagMap map) { + IllegalStateException ex = null; + try { + map.put("foo", "bar"); + } catch (IllegalStateException e) { + ex = e; + } + assertNotNull(ex); + } + + static final void assertFrozen(Runnable runnable) { + IllegalStateException ex = null; + try { + runnable.run(); + } catch (IllegalStateException e) { + ex = e; + } + assertNotNull(ex); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapType.java b/internal-api/src/test/java/datadog/trace/api/TagMapType.java new file mode 100644 index 00000000000..a5c07410c48 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapType.java @@ -0,0 +1,20 @@ +package datadog.trace.api; + +public enum TagMapType { + OPTIMIZED(new OptimizedTagMapFactory()), + LEGACY(new LegacyTagMapFactory()); + + final TagMapFactory factory; + + TagMapType(TagMapFactory factory) { + this.factory = factory; + } + + public final TagMap create() { + return factory.create(); + } + + public final TagMap empty() { + return factory.empty(); + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/TagMapTypePair.java b/internal-api/src/test/java/datadog/trace/api/TagMapTypePair.java new file mode 100644 index 00000000000..1b82df3d3a0 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTypePair.java @@ -0,0 +1,16 @@ +package datadog.trace.api; + +public enum TagMapTypePair { + BOTH_OPTIMIZED(TagMapType.OPTIMIZED, TagMapType.OPTIMIZED), + BOTH_LEGACY(TagMapType.LEGACY, TagMapType.LEGACY), + OPTIMIZED_LEGACY(TagMapType.OPTIMIZED, TagMapType.LEGACY), + LEGACY_OPTIMIZED(TagMapType.LEGACY, TagMapType.OPTIMIZED); + + public final TagMapType firstType; + public final TagMapType secondType; + + TagMapTypePair(TagMapType firstType, TagMapType secondType) { + this.firstType = firstType; + this.secondType = secondType; + } +}