Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested metric registries don't cause a sortedmap creation #430

Merged
merged 26 commits into from
Sep 18, 2019

Conversation

j-baker
Copy link
Contributor

@j-baker j-baker commented Sep 18, 2019

This might seem like an unnecessary optimization, but profiling
internally implies this uses plenty of CPU and allocates gigabytes and
gigabytes and gigabytes (about 1.8GB of allocations to report metrics).

BLUF: With nested tags, our tooling spends all of its time recreating
nested sorted maps and rehashing, which is very expensive. That said,
breaking binary backcompat here is super expensive, as we know from past
experience.

This PR solves this issue. We no longer use the immutables generated
class, though we do use the builder. Instead, we use our own
implementation, which hashes consistently and uses two map
implementations to ensure that in the cases we see, equality, hash, and
extending with another tag are cheap.

This might seem like an unnecessary optimization, but profiling
internally implies this uses plenty of CPU and allocates gigabytes and
gigabytes and gigabytes (about 1.8GB of allocations to report metrics).

BLUF: With nested tags, our tooling spends all of its time recreating
nested sorted maps and rehashing, which is very expensive. That said,
breaking binary backcompat here is super expensive, as we know from past
experience.

This PR solves this issue. We no longer use the immutables generated
class, though we do use the builder. Instead, we use our own
implementation, which hashes consistently and uses two map
implementations to ensure that in the cases we see, equality, hash, and
extending with another tag are cheap.
@j-baker j-baker requested a review from schlosna as a code owner September 18, 2019 10:39
@changelog-app
Copy link

changelog-app bot commented Sep 18, 2019

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Nested metric registries cause fewer allocations.

Check the box to generate changelog(s)

  • Generate changelog entry

@carterkozak
Copy link
Contributor

Would you mind adding a representative JMH benchmark?

@j-baker
Copy link
Contributor Author

j-baker commented Sep 18, 2019

That seems like a great idea.

@CRogers
Copy link

CRogers commented Sep 18, 2019

Chatted to @j-baker on slack, would be good to see a profile on this (possibly in prod) before we merge this as it's used by every product.

@carterkozak
Copy link
Contributor

Could we do something with a TaggedMetricRegistry.forEach(BiConsumer<MetricName, Metric>) instead?

@j-baker
Copy link
Contributor Author

j-baker commented Sep 18, 2019

I ended up doing both (and came to the forEach conclusion as well). Here are benchmark results before and after:

Before                                                                    Mode  Cnt        Score         Error   Units
NestedMetricsBenchmark.benchmarkForEach                                      avgt   10   949386.155 ±  290036.990   ns/op
NestedMetricsBenchmark.benchmarkForEach:·gc.alloc.rate.norm                  avgt   10  2122032.414 ±       0.121    B/op
NestedMetricsBenchmark.benchmarkGetMetrics                                   avgt   10  1627004.644 ±  361734.975   ns/op
NestedMetricsBenchmark.benchmarkGetMetrics:·gc.alloc.rate.norm               avgt   10  2800648.214 ±      12.257    B/op

After                                                                    Mode  Cnt        Score        Error   Units
NestedMetricsBenchmark.benchmarkForEach                                      avgt   10   214789.191 ±  66852.954   ns/op
NestedMetricsBenchmark.benchmarkForEach:·gc.alloc.rate.norm                  avgt   10   322032.094 ±      0.028    B/op
NestedMetricsBenchmark.benchmarkGetMetrics                                   avgt   10   557286.570 ±  78861.278   ns/op
NestedMetricsBenchmark.benchmarkGetMetrics:·gc.alloc.rate.norm               avgt   10   997784.244 ±      0.039    B/op

as you can see, even in the case where we only have a single pre-existing tag, this is a considerable improvement.

@j-baker
Copy link
Contributor Author

j-baker commented Sep 18, 2019

This is also only to write 5k metrics - in practice we see many more.

@carterkozak
Copy link
Contributor

I think we should update the benchmark into a root TaggedMetricRegistry with N tagged metrics, a child wrapped DropwizardMetricSet(MetricRegistry) with N metrics, and a child TaggedMetricRegistry with N metrics (representing SharedTaggedMetricRegistries.getSingleton()).
That should scale in a way that represents production systems.

I'd prefer to put all our eggs in the forEachMetric basket and avoid optimizations on getMetrics to reduce complexity. If we provide a simple MetricName implementation which doesn't pre-compute hashCode, and avoid precomputing hashcode on ExtraEntrySortedMap, we should have strictly less work.

Might be worth special casing adding a single tag to a MetricName with no tags, and adding a simple MetricName implementation around a safeName with no tags for DropwizardTaggedMetricSet.

import net.jqwik.api.constraints.IntRange;
import net.jqwik.api.constraints.Size;

class ExtraEntrySortedMapTest {
Copy link

@CRogers CRogers Sep 18, 2019

Choose a reason for hiding this comment

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

I've added this property based test as I wanted to make sure the behaviour of ExtraEntrySortedMap was correct but also not write a ton of example based tests. This checks that every method call on ExtraEntrySortedMap produces the same result as the same method call on an equivalent guava ImmutableSortedMap, for thousands of arbitrary base maps and extra values. It uses jqwik.

private final SortedMap<K, V> delegate;
private final int hashCode;

PrehashedSortedMap(SortedMap<K, V> delegate) {
Copy link
Contributor

Choose a reason for hiding this comment

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

would suggest making immutability explicit in types

Suggested change
PrehashedSortedMap(SortedMap<K, V> delegate) {
PrehashedSortedMap(ImmutableSortedMap<K, V> delegate) {

Copy link
Contributor Author

@j-baker j-baker Sep 18, 2019

Choose a reason for hiding this comment

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

This would require casting in RealMetricName, which I'd like to avoid.

Copy link
Contributor

Choose a reason for hiding this comment

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

not sure I follow that as the only caller of this constructor is com.palantir.tritium.metrics.registry.RealMetricName#prehash which explicitly passes in an ImmutableSortedMap

        return new PrehashedSortedMap<>(ImmutableSortedMap.copyOfSorted(map));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops!

private int computeHashCode() {
int hash = 5381;
hash += (hash << 5) + safeName().hashCode();
hash += (hash << 5) + safeTags().hashCode();
Copy link
Contributor

Choose a reason for hiding this comment

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

just curious why djb2 hash algorithm?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's the one in the immutables generated code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

copy-pasted :P

@schlosna schlosna mentioned this pull request Sep 18, 2019
@@ -469,7 +469,7 @@
<module name="NestedTryDepth"/> <!-- Java Coding Guide: Try/catch blocks: never nested -->
<module name="NonEmptyAtclauseDescription"/> <!-- Java Style Guide: At-clauses -->
<module name="ParameterName"> <!-- Java Style Guide: Parameter names -->
<property name="format" value="^[a-z][a-zA-Z0-9]+$"/>
<property name="format" value="^_?[a-z][a-zA-Z0-9]+$"/>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

as updated by baseline

Copy link
Contributor

Choose a reason for hiding this comment

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

assume we will also pick this up next time the robots do their rounds in https://github.com/palantir/gradle-baseline/releases/tag/2.7.0 though I don't see a tritium excavator listed in palantir/gradle-baseline#833

@j-baker j-baker force-pushed the jbaker/faster_strings branch from 1c39f80 to 90456c8 Compare September 18, 2019 19:09
Copy link
Contributor

@schlosna schlosna left a comment

Choose a reason for hiding this comment

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

LGTM 👍 , thanks James! Defer to you all when you want to merge & cut a release

@@ -469,7 +469,7 @@
<module name="NestedTryDepth"/> <!-- Java Coding Guide: Try/catch blocks: never nested -->
<module name="NonEmptyAtclauseDescription"/> <!-- Java Style Guide: At-clauses -->
<module name="ParameterName"> <!-- Java Style Guide: Parameter names -->
<property name="format" value="^[a-z][a-zA-Z0-9]+$"/>
<property name="format" value="^_?[a-z][a-zA-Z0-9]+$"/>
Copy link
Contributor

Choose a reason for hiding this comment

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

assume we will also pick this up next time the robots do their rounds in https://github.com/palantir/gradle-baseline/releases/tag/2.7.0 though I don't see a tritium excavator listed in palantir/gradle-baseline#833

@svc-autorelease
Copy link
Collaborator

Released 0.15.6

@j-baker j-baker deleted the jbaker/faster_strings branch September 18, 2019 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants