diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java index 41330ffbe4c..32289160c75 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecorator.java @@ -13,6 +13,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; +import datadog.trace.api.TagMap; import datadog.trace.api.TraceConfig; import datadog.trace.api.function.TriConsumer; import datadog.trace.api.function.TriFunction; @@ -408,7 +409,7 @@ public String getSpanType() { } @Override - public Map getTags() { + public TagMap getTags() { return serverSpan.getTags(); } 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 51d2b1ceec9..fdc4cdbd7ba 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 @@ -72,7 +72,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 9cb06e65b60..b44a0c3a19e 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 @@ -44,7 +44,6 @@ import datadog.trace.civisibility.test.ExecutionResults; import java.lang.reflect.Method; import java.util.Collection; -import java.util.Collections; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -101,8 +100,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 aaabb300cc0..721409ad94f 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 bc5f5ef305a..84c2858f654 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 e8cf47d988b..f08d86f227b 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.ConfigurationEndListener import datadog.remoteconfig.ConfigurationPoller import datadog.remoteconfig.Product import datadog.trace.api.Config +import datadog.trace.api.TagMap import datadog.trace.api.internal.TraceSegment import datadog.trace.api.gateway.Flow import datadog.trace.api.gateway.IGSpanInfo @@ -72,7 +73,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 df6bd6d24b3..503ae7edd8c 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 @@ -1,3 +1,4 @@ + package com.datadog.appsec.gateway import com.datadog.appsec.AppSecSystem @@ -9,6 +10,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.function.TriConsumer import datadog.trace.api.function.TriFunction import datadog.trace.api.gateway.BlockResponseFunction @@ -167,7 +169,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() @@ -206,7 +208,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') } @@ -987,7 +989,7 @@ class GatewayBridgeSpecification extends DDSpecification { getTraceSegment() >> traceSegment } final spanInfo = Mock(AgentSpan) { - getTags() >> ['http.route':'/'] + getTags() >> TagMap.fromMap(['http.route':'/']) } when: 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 cf0664b9403..b700b9d1337 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 @@ -100,5 +100,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"; + private GeneralConfig() {} } 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 99fca082ecd..c3689de1f33 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 @@ -32,6 +32,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.TracePropagationBehaviorExtract; import datadog.trace.api.config.GeneralConfig; @@ -108,7 +109,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; @@ -190,10 +190,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 boolean defaultSpanTagsNeedsIntercept; /** number of spans in a pending trace before they get flushed */ private final int partialFlushMinSpans; @@ -318,8 +322,8 @@ public static class CoreTracerBuilder { private HttpCodec.Injector injector; private HttpCodec.Extractor extractor; private ContinuableScopeManager scopeManager; - private Map localRootSpanTags; - private Map defaultSpanTags; + private TagMap localRootSpanTags; + private TagMap defaultSpanTags; private Map serviceNameMappings; private Map taggedHeaders; private Map baggageMapping; @@ -379,12 +383,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; } @@ -534,6 +548,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, @@ -545,8 +616,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, @@ -599,7 +670,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) @@ -737,9 +812,6 @@ private CoreTracer( Propagators.register(INFERRED_PROXY_CONCERN, new InferredProxyPropagator()); } - this.tagInterceptor = - null == tagInterceptor ? new TagInterceptor(new RuleFlags(config)) : tagInterceptor; - if (config.isCiVisibilityEnabled()) { if (config.isCiVisibilityTraceSanitationEnabled()) { addTraceInterceptor(CiVisibilityTraceInterceptor.INSTANCE); @@ -791,12 +863,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. */ @@ -885,7 +960,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 @@ -1309,13 +1384,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; @@ -1330,7 +1405,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; @@ -1371,7 +1448,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(); } @@ -1446,14 +1523,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 TagBuilder 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 + // builder + // 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; } @@ -1505,9 +1589,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; @@ -1516,7 +1601,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(); } @@ -1570,7 +1655,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; @@ -1585,7 +1672,7 @@ private DDSpanContext buildSpanContext() { requestContextDataIast = null; ciVisibilityContextData = null; } - propagationTags = propagationTagsFactory.empty(); + propagationTags = tracer.propagationTagsFactory.empty(); } else { long endToEndStartTime; @@ -1601,19 +1688,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; @@ -1623,6 +1710,7 @@ private DDSpanContext buildSpanContext() { TagContext tc = (TagContext) parentContext; traceConfig = (ConfigSnapshot) tc.getTraceConfig(); coreTags = tc.getTags(); + coreTagsNeedsIntercept = true; // may intercept isn't needed? origin = tc.getOrigin(); baggage = tc.getBaggage(); requestContextDataAppSec = tc.getRequestContextDataAppSec(); @@ -1631,6 +1719,7 @@ private DDSpanContext buildSpanContext() { } else { traceConfig = null; coreTags = null; + coreTagsNeedsIntercept = false; origin = null; baggage = null; requestContextDataAppSec = null; @@ -1638,9 +1727,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); @@ -1655,11 +1745,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; } @@ -1683,20 +1773,16 @@ 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 == coreTags ? 0 : coreTags.size()) - + (null == rootSpanTags ? 0 : rootSpanTags.size()) - + (null == contextualTags ? 0 : contextualTags.size()); + final int tagsSize = 0; if (builderRequestContextDataAppSec != null) { requestContextDataAppSec = builderRequestContextDataAppSec; @@ -1729,22 +1815,20 @@ private DDSpanContext buildSpanContext() { requestContextDataIast, ciVisibilityContextData, pathwayContext, - disableSamplingMechanismValidation, + tracer.disableSamplingMechanismValidation, propagationTags, - profilingContextIntegration, - injectBaggageAsTags, + tracer.profilingContextIntegration, + tracer.injectBaggageAsTags, isRemote); // 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); - if (contextualTags != null) { - context.setAllTags(contextualTags); - } + context.setAllTags(mergedTracerTags, mergedTracerTagsNeedsIntercept); + context.setAllTags(tagLedger); + context.setAllTags(coreTags, coreTagsNeedsIntercept); + context.setAllTags(rootSpanTags, rootSpanTagsNeedsIntercept); + context.setAllTags(contextualTags); return context; } } @@ -1769,7 +1853,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) { @@ -1785,11 +1870,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); } } } @@ -1797,9 +1886,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(); result.putAll(userSpanTags); if (null != config) { // static if (!config.getEnv().isEmpty()) { @@ -1822,6 +1911,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 78b364c1a17..fdfb9ad49aa 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; @@ -693,7 +694,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 1277b16aa7b..c6eaa0518e8 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 @@ -8,6 +8,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; @@ -96,7 +97,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; @@ -344,7 +345,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 @@ -751,16 +753,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()); + } } } } @@ -797,12 +856,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()); @@ -817,7 +878,7 @@ public Map getTags() { if (value != null) { tags.put(Tags.HTTP_URL, value.toString()); } - return Collections.unmodifiableMap(tags); + return tags.freeze(); } } @@ -849,11 +910,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; @@ -868,7 +929,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 9d48317022f..8c543624344 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 @@ -398,9 +399,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 3761944b6ba..c3398abeb43 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 @@ -3,6 +3,7 @@ package datadog.trace.core.datastreams import datadog.communication.ddagent.DDAgentFeaturesDiscovery import datadog.trace.api.Config import datadog.trace.api.DDTraceId +import datadog.trace.api.TagMap import datadog.trace.api.TraceConfig import datadog.trace.api.WellKnownTags import datadog.trace.api.datastreams.StatsPoint @@ -454,7 +455,7 @@ class DefaultPathwayContextTest extends DDCoreSpecification { 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, localTraceConfig, DATADOG) + def spanContext = new ExtractedContext(DDTraceId.ONE, 1, 0, null, 0, null, (TagMap)null, null, null, localTraceConfig, DATADOG) def baseContext = AgentSpan.fromSpanContext(spanContext).storeInto(root()) def propagator = dataStreams.propagator() @@ -549,7 +550,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() 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 9277e7df974..35d7c9acbc5 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -540,6 +540,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; @@ -576,6 +577,8 @@ public static String getHostName() { private final boolean jdkSocketEnabled; + private final boolean optimizedMapEnabled; + // Read order: System Properties -> Env Variables, [-> properties file], [-> default value] private Config() { this(ConfigProvider.createDefault()); @@ -2027,6 +2030,9 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) this.jdkSocketEnabled = configProvider.getBoolean(JDK_SOCKET_ENABLED, true); + this.optimizedMapEnabled = + configProvider.getBoolean(GeneralConfig.OPTIMIZED_MAP_ENABLED, false); + log.debug("New instance: {}", this); } @@ -3653,11 +3659,15 @@ public boolean isJdkSocketEnabled() { return jdkSocketEnabled; } + public boolean isOptimizedMapEnabled() { + return optimizedMapEnabled; + } + /** @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); @@ -3678,7 +3688,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..c64ed02afd5 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -0,0 +1,2979 @@ +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 + *
+ */ + +/* + * For memory efficiency, TagMap 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. + */ +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(); + + @Deprecated + Set keySet(); + + @Deprecated + Collection values(); + + // @Deprecated -- not deprecated until OptimizedTagMap becomes the default + Set> entrySet(); + + @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 + 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 + 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 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 + @Override + public String getKey() { + return this.tag(); + } + + @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 = + Config.get().isOptimizedMapEnabled() + ? 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; + } +} + +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 ? 0D : 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..11b06a580d7 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,19 +41,22 @@ 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, final TraceConfig traceConfig, final TracePropagationStyle propagationStyle, final DDTraceId traceId) { + + // if ( tags != null ) tags.checkWriteAccess(); + this.origin = origin; this.tags = tags; this.terminatedContextLinks = null; @@ -164,15 +167,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..d73d253c431 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/TagMapTest.java @@ -0,0 +1,724 @@ +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.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.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class TagMapTest { + // size is chosen to make sure to stress all types of collisions in the Map + static final int MANY_SIZE = 256; + + @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 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; + } +}