diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ba7231e..2bf70bb 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -3,7 +3,7 @@ # separate terms of service, privacy policy, and support # documentation. # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle name: Java CI with Gradle diff --git a/build.gradle b/build.gradle index 87a71d8..fe8e47d 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ configurations.archives.artifacts.clear() allprojects { group = 'com.uber.m3' - version = '0.11.1' + version = '0.13.0' apply plugin: 'java' apply plugin: 'maven' diff --git a/core/benchmark-tests.txt b/core/benchmark-tests.txt index e3651a8..cf77b78 100644 --- a/core/benchmark-tests.txt +++ b/core/benchmark-tests.txt @@ -1,9 +1,35 @@ -Benchmark Mode Cnt Score Error Units -ScopeImplBenchmark.scopeReportingBenchmark thrpt 10 1385.711 ± 55.291 ops/ms -ScopeImplBenchmark.scopeReportingBenchmark:·async thrpt NaN --- -ScopeImplBenchmark.scopeReportingBenchmark:·gc.alloc.rate thrpt 10 ≈ 10⁻⁴ MB/sec -ScopeImplBenchmark.scopeReportingBenchmark:·gc.alloc.rate.norm thrpt 10 ≈ 10⁻⁴ B/op -ScopeImplBenchmark.scopeReportingBenchmark:·gc.count thrpt 10 ≈ 0 counts -ScopeImplBenchmark.scopeReportingBenchmark:·threads.alive thrpt 10 5.800 ± 0.637 threads -ScopeImplBenchmark.scopeReportingBenchmark:·threads.daemon thrpt 10 4.000 ± 0.001 threads -ScopeImplBenchmark.scopeReportingBenchmark:·threads.started thrpt 10 26.000 threads +Benchmark Mode Cnt Score Error Units +ScopeImplBenchmark.scopeReportingBenchmark thrpt 10 1345.606 ± 129.913 ops/ms +ScopeImplBenchmark.scopeReportingBenchmark:·async thrpt NaN --- +ScopeImplBenchmark.scopeReportingBenchmark:·gc.alloc.rate thrpt 10 ≈ 10⁻⁴ MB/sec +ScopeImplBenchmark.scopeReportingBenchmark:·gc.alloc.rate.norm thrpt 10 ≈ 10⁻⁴ B/op +ScopeImplBenchmark.scopeReportingBenchmark:·gc.count thrpt 10 ≈ 0 counts +ScopeImplBenchmark.scopeReportingBenchmark:·threads.alive thrpt 10 5.800 ± 0.637 threads +ScopeImplBenchmark.scopeReportingBenchmark:·threads.daemon thrpt 10 4.000 ± 0.001 threads +ScopeImplBenchmark.scopeReportingBenchmark:·threads.started thrpt 10 26.000 threads +ScopeImplBenchmark.scopeTaggedBenchmark thrpt 10 2440.881 ± 73.028 ops/ms +ScopeImplBenchmark.scopeTaggedBenchmark:·async thrpt NaN --- +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.alloc.rate thrpt 10 1947.506 ± 58.647 MB/sec +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.alloc.rate.norm thrpt 10 880.000 ± 0.001 B/op +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.churn.G1_Eden_Space thrpt 10 1940.575 ± 57.874 MB/sec +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.churn.G1_Eden_Space.norm thrpt 10 876.878 ± 5.246 B/op +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.churn.G1_Old_Gen thrpt 10 0.168 ± 0.001 MB/sec +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.churn.G1_Old_Gen.norm thrpt 10 0.076 ± 0.002 B/op +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.count thrpt 10 1736.000 counts +ScopeImplBenchmark.scopeTaggedBenchmark:·gc.time thrpt 10 1772.000 ms +ScopeImplBenchmark.scopeTaggedBenchmark:·threads.alive thrpt 10 5.800 ± 0.637 threads +ScopeImplBenchmark.scopeTaggedBenchmark:·threads.daemon thrpt 10 4.000 ± 0.001 threads +ScopeImplBenchmark.scopeTaggedBenchmark:·threads.started thrpt 10 26.000 threads +ScopeImplConcurrent.hotkeyLockContention thrpt 10 1.430 ± 0.218 ops/ms +ScopeImplConcurrent.hotkeyLockContention:·async thrpt NaN --- +ScopeImplConcurrent.hotkeyLockContention:·gc.alloc.rate thrpt 10 1244.444 ± 187.772 MB/sec +ScopeImplConcurrent.hotkeyLockContention:·gc.alloc.rate.norm thrpt 10 960144.037 ± 0.011 B/op +ScopeImplConcurrent.hotkeyLockContention:·gc.churn.G1_Eden_Space thrpt 10 1237.763 ± 186.894 MB/sec +ScopeImplConcurrent.hotkeyLockContention:·gc.churn.G1_Eden_Space.norm thrpt 10 954972.637 ± 4468.064 B/op +ScopeImplConcurrent.hotkeyLockContention:·gc.churn.G1_Old_Gen thrpt 10 0.166 ± 0.001 MB/sec +ScopeImplConcurrent.hotkeyLockContention:·gc.churn.G1_Old_Gen.norm thrpt 10 129.528 ± 18.631 B/op +ScopeImplConcurrent.hotkeyLockContention:·gc.count thrpt 10 1564.000 counts +ScopeImplConcurrent.hotkeyLockContention:·gc.time thrpt 10 1336.000 ms +ScopeImplConcurrent.hotkeyLockContention:·threads.alive thrpt 10 5.800 ± 0.637 threads +ScopeImplConcurrent.hotkeyLockContention:·threads.daemon thrpt 10 4.000 ± 0.001 threads +ScopeImplConcurrent.hotkeyLockContention:·threads.started thrpt 10 26.000 threads diff --git a/core/build.gradle b/core/build.gradle index d8d34dd..c13b98c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -21,6 +21,11 @@ description = 'Interfaces and utilities to report metrics to M3' apply from: 'jmhFixtures.gradle' +dependencies { + // https://mvnrepository.com/artifact/nl.jqno.equalsverifier/equalsverifier + testImplementation("nl.jqno.equalsverifier:equalsverifier:3.13") +} + sourceSets { jmh { java.srcDirs = ['src/jmh/java'] @@ -42,20 +47,26 @@ task runJmhTests(type: JavaExec, dependsOn: jmhClasses) { args '-rff', resultFile // Profile using GC, Threading profilers - args '-prof', 'gc' - args '-prof', 'hs_thr' + if (project.properties.get('busegc', 'true') == 'true') { + args '-prof', 'gc' + args '-prof', 'hs_thr' + + // Force GC after every iterations, to make sure that one iteration + // doesn't affect the other one + args '-gc', 'true' + } + // Profile using async-profiling // // NOTE: For this to work you need to make sure that async-profiler's library is either // - Available in LD_LIBRARY_PATH (Linux), DYLD_LIBRARY_PATH (Mac) // - Available in '-Djava.library.path' - // - Explicitly specified with 'async:libPath=' - args '-prof', 'async:event=cpu;direction=forward;output=flamegraph' - - // Force GC after every iterations, to make sure that one iteration - // doesn't affect the other one - args '-gc', 'true' + // - Explicitly specified in pprof arg, the value 'async:libPath=' + args '-prof', project.properties.get('bprof', 'async:event=cpu;direction=forward;output=flamegraph') + args '-bm', project.properties.get('bm', 'thrpt') + args '-t', project.properties.get('bthreads', '1') + args 'com.uber.m3.tally.' + project.properties.get('benchclass', '') } -classes.finalizedBy(jmhClasses) \ No newline at end of file +classes.finalizedBy(jmhClasses) diff --git a/core/gradle.properties b/core/gradle.properties new file mode 100644 index 0000000..9ec38ec --- /dev/null +++ b/core/gradle.properties @@ -0,0 +1,3 @@ +#bprof=async:event=cpu;direction=forward;output=flamegraph;dir=profile-results;libPath= +#benchclass=ScopeImplBenchmark.* +#output=benchmark-new.txt diff --git a/core/jmhFixtures.gradle b/core/jmhFixtures.gradle index 30c6bec..127030a 100644 --- a/core/jmhFixtures.gradle +++ b/core/jmhFixtures.gradle @@ -54,4 +54,8 @@ dependencies { jmhFixturesUsageCompile project(project.path) jmhFixturesCompile('org.openjdk.jmh:jmh-core:1.27') jmhFixturesCompile('org.openjdk.jmh:jmh-generator-annprocess:1.27') + + if (project.gradle.gradleVersion > '5') { + jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.27") + } } diff --git a/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java b/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java index 5844536..31ee6f4 100644 --- a/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java +++ b/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java @@ -30,6 +30,7 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.infra.Blackhole; import java.util.Random; import java.util.concurrent.TimeUnit; @@ -41,6 +42,8 @@ public class ScopeImplBenchmark { private static final DurationBuckets EXPONENTIAL_BUCKETS = DurationBuckets.linear(Duration.ofMillis(1), Duration.ofMillis(10), 128); + private static final ImmutableMap TAGS_STRING_MAP = ImmutableMap.of("tag1", "value1", "tag2", "value2", "tag3", "value3"); + private static final String[] COUNTER_NAMES = { "first-counter", "second-counter", @@ -70,6 +73,11 @@ public void scopeReportingBenchmark(BenchmarkState state) { state.scope.reportLoopIteration(); } + @Benchmark + public void scopeTaggedBenchmark(Blackhole blackhole, BenchmarkState state) { + blackhole.consume(state.scope.tagged(TAGS_STRING_MAP)); + } + @State(org.openjdk.jmh.annotations.Scope.Benchmark) public static class BenchmarkState { diff --git a/core/src/jmh/java/com/uber/m3/tally/ScopeImplConcurrent.java b/core/src/jmh/java/com/uber/m3/tally/ScopeImplConcurrent.java new file mode 100644 index 0000000..4e4411a --- /dev/null +++ b/core/src/jmh/java/com/uber/m3/tally/ScopeImplConcurrent.java @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import com.uber.m3.util.Duration; +import com.uber.m3.util.ImmutableMap; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Fork(value = 2, jvmArgsAppend = { "-server", "-XX:+UseG1GC" }) +public class ScopeImplConcurrent { + private static final List SCOPE_KEYS = + Stream.of(" ", "0", "@", "P").map(prefix -> new ScopeKey(prefix, null)).collect(Collectors.toList()); + + @Benchmark + public void hotkeyLockContention(Blackhole bh, BenchmarkState state) { + ImmutableMap common = new ImmutableMap.Builder().build(); + for (int i = 0; i < 10000; i++) { + + for (ScopeKey scopeKey : SCOPE_KEYS) { + Scope scope = state.scope.computeSubscopeIfAbsent("prefix", scopeKey, common); + assert scope != null; + bh.consume(scope); + } + } + } + + @State(org.openjdk.jmh.annotations.Scope.Benchmark) + public static class BenchmarkState { + + private ScopeImpl scope; + + @Setup + public void setup() { + this.scope = + (ScopeImpl) new RootScopeBuilder() + .reporter(new TestStatsReporter()) + .reportEvery(Duration.MAX_VALUE); + + for (ScopeKey scopeKey : SCOPE_KEYS) { + scope.computeSubscopeIfAbsent("prefix", scopeKey, new ImmutableMap.Builder().build()); + } + } + + @TearDown + public void teardown() { + scope.close(); + } + } +} diff --git a/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java b/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java new file mode 100644 index 0000000..9903a5e --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import com.uber.m3.util.Duration; + +import java.util.Map; + +/** + * NullStatsReporter is a noop implementation of StatsReporter. + */ +public class NullStatsReporter implements StatsReporter { + @Override + public Capabilities capabilities() { + return CapableOf.NONE; + } + + @Override + public void flush() { + + } + + @Override + public void close() { + + } + + @Override + public void reportCounter(String name, Map tags, long value) { + + } + + @Override + public void reportGauge(String name, Map tags, double value) { + + } + + @Override + public void reportTimer(String name, Map tags, Duration interval) { + + } + + @Override + public void reportHistogramValueSamples(String name, Map tags, Buckets buckets, double bucketLowerBound, double bucketUpperBound, long samples) { + + } + + @Override + public void reportHistogramDurationSamples(String name, Map tags, Buckets buckets, Duration bucketLowerBound, Duration bucketUpperBound, long samples) { + + } +} diff --git a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java index 8ac3c6b..09e8377 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java @@ -23,11 +23,9 @@ import com.uber.m3.util.ImmutableMap; import javax.annotation.Nullable; -import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; @@ -35,7 +33,7 @@ /** * Default {@link Scope} implementation. */ -class ScopeImpl implements Scope { +class ScopeImpl implements Scope, TestScope { private StatsReporter reporter; private String prefix; private String separator; @@ -90,7 +88,7 @@ public Timer timer(String name) { @Override public Histogram histogram(String name, @Nullable Buckets buckets) { return histograms.computeIfAbsent(name, ignored -> - // NOTE: This will called at most once + // NOTE: This will be called at most once new HistogramImpl( this, fullyQualifiedName(name), @@ -152,35 +150,8 @@ void report(StatsReporter reporter) { // Serializes a map to generate a key for a prefix/map combination // Non-generic EMPTY ImmutableMap will never contain any elements - @SuppressWarnings("unchecked") - static String keyForPrefixedStringMap(String prefix, ImmutableMap stringMap) { - if (prefix == null) { - prefix = ""; - } - - if (stringMap == null) { - stringMap = ImmutableMap.EMPTY; - } - - Set keySet = stringMap.keySet(); - String[] sortedKeys = keySet.toArray(new String[keySet.size()]); - Arrays.sort(sortedKeys); - - StringBuilder keyBuffer = new StringBuilder(prefix.length() + sortedKeys.length * 20); - keyBuffer.append(prefix); - keyBuffer.append("+"); - - for (int i = 0; i < sortedKeys.length; i++) { - keyBuffer.append(sortedKeys[i]); - keyBuffer.append("="); - keyBuffer.append(stringMap.get(sortedKeys[i])); - - if (i != sortedKeys.length - 1) { - keyBuffer.append(","); - } - } - - return keyBuffer.toString(); + static ScopeKey keyForPrefixedStringMap(String prefix, ImmutableMap stringMap) { + return new ScopeKey(prefix, stringMap); } String fullyQualifiedName(String name) { @@ -192,71 +163,79 @@ String fullyQualifiedName(String name) { } /** - * Returns a {@link Snapshot} of this {@link Scope}. + * Snapshot returns a copy of all values since the last report execution + * This is an expensive operation and should only be used for testing purposes. + * * @return a {@link Snapshot} of this {@link Scope} */ + @Override public Snapshot snapshot() { Snapshot snap = new SnapshotImpl(); for (ScopeImpl subscope : registry.subscopes.values()) { + ImmutableMap tags = new ImmutableMap.Builder() + .putAll(this.tags) + .putAll(subscope.tags) + .build(); + for (Map.Entry counter : subscope.counters.entrySet()) { String name = subscope.fullyQualifiedName(counter.getKey()); - String id = keyForPrefixedStringMap(name, tags); + ScopeKey scopeKey = keyForPrefixedStringMap(name, tags); snap.counters().put( - id, - new CounterSnapshotImpl( - name, - tags, - counter.getValue().snapshot() - ) + scopeKey, + new CounterSnapshotImpl( + name, + tags, + counter.getValue().snapshot() + ) ); } for (Map.Entry gauge : subscope.gauges.entrySet()) { String name = subscope.fullyQualifiedName(gauge.getKey()); - String id = keyForPrefixedStringMap(name, tags); + ScopeKey scopeKey = keyForPrefixedStringMap(name, tags); snap.gauges().put( - id, - new GaugeSnapshotImpl( - name, - tags, - gauge.getValue().snapshot() - ) + scopeKey, + new GaugeSnapshotImpl( + name, + tags, + gauge.getValue().snapshot() + ) ); } for (Map.Entry timer : subscope.timers.entrySet()) { String name = subscope.fullyQualifiedName(timer.getKey()); - String id = keyForPrefixedStringMap(name, tags); + ScopeKey scopeKey = keyForPrefixedStringMap(name, tags); snap.timers().put( - id, - new TimerSnapshotImpl( - name, - tags, - timer.getValue().snapshot() - ) + scopeKey, + new TimerSnapshotImpl( + name, + tags, + timer.getValue().snapshot() + ) ); } for (Map.Entry histogram : subscope.histograms.entrySet()) { String name = subscope.fullyQualifiedName(histogram.getKey()); - String id = keyForPrefixedStringMap(name, tags); + ScopeKey scopeKey = keyForPrefixedStringMap(name, tags); snap.histograms().put( - id, - new HistogramSnapshotImpl( - name, - tags, - histogram.getValue().snapshotValues(), - histogram.getValue().snapshotDurations() - ) + scopeKey, + new HistogramSnapshotImpl( + name, + tags, + histogram.getValue().snapshotValues(), + histogram.getValue().snapshotDurations() + ) ); } } @@ -278,7 +257,17 @@ private Scope subScopeHelper(String prefix, Map tags) { ImmutableMap mergedTags = mapBuilder.build(); - String key = keyForPrefixedStringMap(prefix, mergedTags); + ScopeKey key = keyForPrefixedStringMap(prefix, mergedTags); + + return computeSubscopeIfAbsent(prefix, key, mergedTags); + } + + // This method must only be called on unit tests or benchmarks + protected Scope computeSubscopeIfAbsent(String prefix, ScopeKey key, ImmutableMap mergedTags) { + Scope scope = registry.subscopes.get(key); + if (scope != null) { + return scope; + } return registry.subscopes.computeIfAbsent( key, @@ -332,6 +321,7 @@ private void reportUncaughtException(Exception uncaughtException) { } static class Registry { - Map subscopes = new ConcurrentHashMap<>(); + Map subscopes = new ConcurrentHashMap<>(); } + } diff --git a/core/src/main/java/com/uber/m3/tally/ScopeKey.java b/core/src/main/java/com/uber/m3/tally/ScopeKey.java new file mode 100644 index 0000000..cf2188e --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/ScopeKey.java @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import com.uber.m3.util.ImmutableMap; + +import java.util.Objects; + +/** + * ScopeKey encapsulates the data to uniquely identify the {@link Scope}. + * This object overrides {@link #equals(Object)} and {@link #hashCode()} methods, so it can be used in Hash based {@link java.util.Map} implementations, to retrieve the corresponding {@link Scope}. + */ +public final class ScopeKey { + private final String prefix; + private final ImmutableMap tags; + + public ScopeKey(String prefix, ImmutableMap tags) { + this.prefix = (prefix == null) ? "" : prefix; + this.tags = (tags == null) ? ImmutableMap.EMPTY : tags; + } + + @Override + public int hashCode() { + return Objects.hash(prefix, tags); + } + + @Override + public boolean equals(Object otherObj) { + if (this == otherObj) { + return true; + } + if (otherObj == null) { + return false; + } + if (getClass() != otherObj.getClass()) { + return false; + } + ScopeKey other = (ScopeKey) otherObj; + return Objects.equals(this.prefix, other.prefix) && Objects.equals(this.tags, other.tags); + } + +} diff --git a/core/src/main/java/com/uber/m3/tally/Snapshot.java b/core/src/main/java/com/uber/m3/tally/Snapshot.java index 3d1a2da..cc23b30 100644 --- a/core/src/main/java/com/uber/m3/tally/Snapshot.java +++ b/core/src/main/java/com/uber/m3/tally/Snapshot.java @@ -30,23 +30,23 @@ public interface Snapshot { * Returns a {@link CounterSnapshot} of all {@link Counter} summations since last report execution. * @return a {@link CounterSnapshot} of all {@link Counter} summations since last report execution */ - Map counters(); + Map counters(); /** * Returns a {@link GaugeSnapshot} of {@link Gauge} last values since last report execution. * @return a {@link GaugeSnapshot} of {@link Gauge} last values since last report execution */ - Map gauges(); + Map gauges(); /** * Returns a {@link TimerSnapshot} of {@link Timer} values since last report execution. * @return a {@link TimerSnapshot} of {@link Timer} values since last report execution */ - Map timers(); + Map timers(); /** * Returns a {@link HistogramSnapshot} of {@link Histogram} samples since last report execution. * @return a {@link HistogramSnapshot} of {@link Histogram} samples since last report execution */ - Map histograms(); + Map histograms(); } diff --git a/core/src/main/java/com/uber/m3/tally/SnapshotImpl.java b/core/src/main/java/com/uber/m3/tally/SnapshotImpl.java index 9387ac9..d4e98ce 100644 --- a/core/src/main/java/com/uber/m3/tally/SnapshotImpl.java +++ b/core/src/main/java/com/uber/m3/tally/SnapshotImpl.java @@ -27,28 +27,28 @@ * Default implementation of a {@link Snapshot}. */ class SnapshotImpl implements Snapshot { - Map counters = new ConcurrentHashMap<>(); - Map gauges = new ConcurrentHashMap<>(); - Map timers = new ConcurrentHashMap<>(); - Map histograms = new ConcurrentHashMap<>(); + Map counters = new ConcurrentHashMap<>(); + Map gauges = new ConcurrentHashMap<>(); + Map timers = new ConcurrentHashMap<>(); + Map histograms = new ConcurrentHashMap<>(); @Override - public Map counters() { + public Map counters() { return counters; } @Override - public Map gauges() { + public Map gauges() { return gauges; } @Override - public Map timers() { + public Map timers() { return timers; } @Override - public Map histograms() { + public Map histograms() { return histograms; } } diff --git a/core/src/main/java/com/uber/m3/tally/TestScope.java b/core/src/main/java/com/uber/m3/tally/TestScope.java new file mode 100644 index 0000000..af6c5b0 --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/TestScope.java @@ -0,0 +1,58 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import java.util.Map; + +/** + * TestScope is a metrics collector that has no reporting, ensuring that + * all emitted values have a given prefix or set of tags. + */ +public interface TestScope extends Scope { + + /** + * Creates a new TestScope that adds the ability to take snapshots of + * metrics emitted to it. + */ + static TestScope create() { + return new RootScopeBuilder() + .reporter(new NullStatsReporter()) + .build(); + } + + /** + * Creates a new TestScope with given prefix/tags that adds the ability to + * take snapshots of metrics emitted to it. + */ + static TestScope create(String prefix, Map tags) { + return new RootScopeBuilder() + .prefix(prefix) + .tags(tags) + .reporter(new NullStatsReporter()) + .build(); + } + + /** + * Snapshot returns a copy of all values since the last report execution + * This is an expensive operation and should only be used for testing purposes. + */ + Snapshot snapshot(); +} diff --git a/core/src/main/java/com/uber/m3/util/ImmutableMap.java b/core/src/main/java/com/uber/m3/util/ImmutableMap.java index 1fe4ec0..7aa7a54 100644 --- a/core/src/main/java/com/uber/m3/util/ImmutableMap.java +++ b/core/src/main/java/com/uber/m3/util/ImmutableMap.java @@ -223,6 +223,10 @@ public Builder put(K key, V value) { } public Builder putAll(Map otherMap) { + if (otherMap == null) { + return this; + } + map.putAll(otherMap); return this; diff --git a/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java b/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java new file mode 100644 index 0000000..1141b40 --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java @@ -0,0 +1,38 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +public class NullStatsReporterTest { + + @Test + public void capabilities() { + NullStatsReporter reporter = new NullStatsReporter(); + assertNotNull(reporter.capabilities()); + assertFalse(reporter.capabilities().reporting()); + assertFalse(reporter.capabilities().tagging()); + } +} + diff --git a/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java index d5dbd28..39a5e3a 100644 --- a/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java +++ b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java @@ -20,20 +20,20 @@ package com.uber.m3.tally; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.uber.m3.util.Duration; +import com.uber.m3.util.ImmutableMap; import java.lang.Thread.UncaughtExceptionHandler; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; - import org.junit.Test; -import com.uber.m3.util.Duration; -import com.uber.m3.util.ImmutableMap; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - public class ScopeImplTest { private static final double EPSILON = 1e-10; private static final int REPORT_INTERVAL_MILLIS = 10; @@ -170,14 +170,15 @@ public void subscopes() { } catch (InterruptedException e) { System.err.println("Interrupted while sleeping! Let's continue anyway..."); } + final Set> actualCounters = new HashSet<>(); + actualCounters.add(reporter.nextCounter()); + actualCounters.add(reporter.nextCounter()); - TestStatsReporter.MetricStruct counter = reporter.nextCounter(); - assertEquals("root_counter", counter.getName()); - assertEquals(tags, counter.getTags()); + final Set> expectedCounters = new HashSet<>(); + expectedCounters.add(new TestStatsReporter.MetricStruct<>("root_counter", tags, 20L)); + expectedCounters.add(new TestStatsReporter.MetricStruct<>("inner.sub_counter", tags, 25L)); - counter = reporter.nextCounter(); - assertEquals("inner.sub_counter", counter.getName()); - assertEquals(tags, counter.getTags()); + assertEquals(expectedCounters, actualCounters); TestStatsReporter.MetricStruct gauge = reporter.nextGauge(); assertEquals("inner.deeper.sub_sub_gauge", gauge.getName()); @@ -226,27 +227,34 @@ public void snapshot() { Snapshot snapshot = ((ScopeImpl) rootScope).snapshot(); - Map counters = snapshot.counters(); + Map counters = snapshot.counters(); assertEquals(1, counters.size()); - assertEquals("snapshot-counter", counters.get("snapshot-counter+").name()); - assertEquals(null, counters.get("snapshot-counter+").tags()); + CounterSnapshot counterSnapshotActual = counters.get(ScopeImpl.keyForPrefixedStringMap("snapshot-counter", null)); + assertEquals("snapshot-counter", counterSnapshotActual.name()); + assertEquals(ImmutableMap.EMPTY, counterSnapshotActual.tags()); - Map gauges = snapshot.gauges(); + Map gauges = snapshot.gauges(); assertEquals(3, gauges.size()); - assertEquals("snapshot-gauge", gauges.get("snapshot-gauge+").name()); - assertEquals(null, gauges.get("snapshot-gauge+").tags()); - assertEquals(120, gauges.get("snapshot-gauge+").value(), EPSILON); - assertEquals("snapshot-gauge2", gauges.get("snapshot-gauge2+").name()); - assertEquals(null, gauges.get("snapshot-gauge2+").tags()); - assertEquals(220, gauges.get("snapshot-gauge2+").value(), EPSILON); - assertEquals("snapshot-gauge3", gauges.get("snapshot-gauge3+").name()); - assertEquals(null, gauges.get("snapshot-gauge3+").tags()); - assertEquals(320, gauges.get("snapshot-gauge3+").value(), EPSILON); - - Map timers = snapshot.timers(); + GaugeSnapshot gaugeSnapshotActual = gauges.get(ScopeImpl.keyForPrefixedStringMap("snapshot-gauge", null)); + assertEquals("snapshot-gauge", gaugeSnapshotActual.name()); + assertEquals(ImmutableMap.EMPTY, gaugeSnapshotActual.tags()); + assertEquals(120, gaugeSnapshotActual.value(), EPSILON); + + GaugeSnapshot gaugeSnapshot2Actual = gauges.get(ScopeImpl.keyForPrefixedStringMap("snapshot-gauge2", null)); + assertEquals("snapshot-gauge2", gaugeSnapshot2Actual.name()); + assertEquals(ImmutableMap.EMPTY, gaugeSnapshot2Actual.tags()); + assertEquals(220, gaugeSnapshot2Actual.value(), EPSILON); + + GaugeSnapshot gaugeSnapshot3Actual = gauges.get(ScopeImpl.keyForPrefixedStringMap("snapshot-gauge3", null)); + assertEquals("snapshot-gauge3", gaugeSnapshot3Actual.name()); + assertEquals(ImmutableMap.EMPTY, gaugeSnapshot3Actual.tags()); + assertEquals(320, gaugeSnapshot3Actual.value(), EPSILON); + + Map timers = snapshot.timers(); assertEquals(1, timers.size()); - assertEquals("snapshot-timer", timers.get("snapshot-timer+").name()); - assertEquals(null, timers.get("snapshot-timer+").tags()); + TimerSnapshot timerSnapshotActual = timers.get(ScopeImpl.keyForPrefixedStringMap("snapshot-timer", null)); + assertEquals("snapshot-timer", timerSnapshotActual.name()); + assertEquals(ImmutableMap.EMPTY, timerSnapshotActual.tags()); } @Test(expected = IllegalArgumentException.class) diff --git a/core/src/test/java/com/uber/m3/tally/ScopeKeyTest.java b/core/src/test/java/com/uber/m3/tally/ScopeKeyTest.java new file mode 100644 index 0000000..b1ef3a2 --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/ScopeKeyTest.java @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +/** + * Tests for {@link com.uber.m3.tally.ScopeKey} + **/ +public class ScopeKeyTest { + @Test + public void testEqualsAndHashCode() { + EqualsVerifier.forClass(ScopeKey.class).verify(); + } +} diff --git a/core/src/test/java/com/uber/m3/tally/TestScopeTest.java b/core/src/test/java/com/uber/m3/tally/TestScopeTest.java new file mode 100644 index 0000000..ccfc3aa --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/TestScopeTest.java @@ -0,0 +1,110 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import com.uber.m3.util.ImmutableMap; +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class TestScopeTest { + + @Test + public void testCreate() { + TestScope testScope = TestScope.create(); + assertNotNull(testScope); + assertThat(testScope, instanceOf(Scope.class)); + + assertNotNull(testScope.capabilities()); + assertFalse(testScope.capabilities().reporting()); + assertFalse(testScope.capabilities().tagging()); + + ImmutableMap tags = ImmutableMap.of("key", "value"); + + testScope.tagged(tags).counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + + CounterSnapshot counterSnapshot = counters.get(new ScopeKey("counter", tags)); + assertNotNull(counterSnapshot); + + assertEquals("counter", counterSnapshot.name()); + assertEquals(tags, counterSnapshot.tags()); + assertEquals(1, counterSnapshot.value()); + } + + @Test + public void createWithPrefixAndTags() { + Map tags = ImmutableMap.of("key", "value"); + TestScope testScope = TestScope.create("prefix", tags); + testScope.tagged(ImmutableMap.of("other_key", "other_value")).counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + + ImmutableMap totalTags = ImmutableMap.of("key", "value", "other_key", "other_value"); + CounterSnapshot counterSnapshot = counters.get(new ScopeKey("prefix.counter", totalTags)); + + assertNotNull(counterSnapshot); + assertEquals("prefix.counter", counterSnapshot.name()); + assertEquals(totalTags, counterSnapshot.tags()); + assertEquals(1, counterSnapshot.value()); + } + + @Test + public void testCreateWithTagsAndSubscope() { + ImmutableMap tags = ImmutableMap.of("key", "value"); + TestScope testScope = TestScope.create("", tags); + + ImmutableMap subScopeTags = ImmutableMap.of("key", "other_value"); + testScope.tagged(subScopeTags).subScope("subscope").counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + + CounterSnapshot counterSnapshot = counters.get(new ScopeKey("subscope.counter", subScopeTags)); + assertNotNull(counterSnapshot); + + assertEquals("subscope.counter", counterSnapshot.name()); + assertEquals(subScopeTags, counterSnapshot.tags()); + assertEquals(1, counterSnapshot.value()); + } +} + diff --git a/core/src/test/java/com/uber/m3/tally/TestStatsReporter.java b/core/src/test/java/com/uber/m3/tally/TestStatsReporter.java index ee91e1a..b8a1fed 100644 --- a/core/src/test/java/com/uber/m3/tally/TestStatsReporter.java +++ b/core/src/test/java/com/uber/m3/tally/TestStatsReporter.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -137,9 +138,9 @@ public Buckets getBuckets() { } static class MetricStruct { - private String name; - private Map tags; - private T value; + private final String name; + private final Map tags; + private final T value; MetricStruct(String name, Map tags, T value) { this.name = name; @@ -158,5 +159,22 @@ Map getTags() { T getValue() { return value; } + + @Override + public boolean equals(Object otherObj) { + if (this == otherObj) { + return true; + } + if (otherObj == null || getClass() != otherObj.getClass()) { + return false; + } + MetricStruct other = (MetricStruct) otherObj; + return Objects.equals(name, other.name) && Objects.equals(tags, other.tags) && Objects.equals(value, other.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, tags, value); + } } }