Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add TypeFactory cache instrumentation #2588

Merged
merged 5 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-2588.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: Add TypeFactory cache instrumentation
links:
- https://github.com/palantir/conjure-java-runtime/pull/2588
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.palantir.conjure.java.serialization;

import com.codahale.metrics.Meter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.type.TypeModifier;
Expand All @@ -25,11 +26,19 @@
import com.fasterxml.jackson.databind.util.LookupCache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import com.google.common.primitives.Ints;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.tritium.metrics.registry.SharedTaggedMetricRegistries;
import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;
import java.util.Locale;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.checkerframework.checker.index.qual.NonNegative;

final class CaffeineCachingTypeFactory extends TypeFactory {
private static final SafeLogger log = SafeLoggerFactory.get(CaffeineCachingTypeFactory.class);
Expand Down Expand Up @@ -108,6 +117,7 @@ private static final class CaffeineLookupCache implements LookupCache<Object, Ja
.maximumSize(1000)
// initial-size 128 up from 16 default as of 2.14.2
.initialCapacity(128)
.recordStats(InstrumentedStatsCounter.SUPPLIER)
.build();
}

Expand Down Expand Up @@ -142,4 +152,65 @@ public String toString() {
return "CaffeineLookupCache{" + cache + '}';
}
}

private static final class InstrumentedStatsCounter implements StatsCounter {
// Collecting metrics without broadening APIs to require a TaggedMetricRegistry
@SuppressWarnings("deprecation")
private static final StatsCounter INSTANCE =
new InstrumentedStatsCounter(SharedTaggedMetricRegistries.getSingleton());

private static final Supplier<StatsCounter> SUPPLIER = () -> INSTANCE;

private final Meter hits;
private final Meter misses;
// Eviction meters are based on RemovalCause ordinal
private final Meter[] evictions;

private InstrumentedStatsCounter(TaggedMetricRegistry registry) {
JsonDatabindTypefactoryCacheMetrics metrics = JsonDatabindTypefactoryCacheMetrics.of(registry);
this.hits = metrics.hit();
this.misses = metrics.miss();
RemovalCause[] causes = RemovalCause.values();
this.evictions = new Meter[causes.length];
for (int i = 0; i < causes.length; i++) {
evictions[i] = metrics.eviction(causes[i].name().toLowerCase(Locale.ROOT));
}
}

@Override
public void recordHits(@NonNegative int count) {
hits.mark(count);
}

@Override
public void recordMisses(@NonNegative int count) {
misses.mark(count);
}

@Override
public void recordLoadSuccess(@NonNegative long _loadTime) {
// nop
}

@Override
public void recordLoadFailure(@NonNegative long _loadTime) {
// nop
}

@Override
public void recordEviction(@NonNegative int _weight, RemovalCause cause) {
evictions[cause.ordinal()].mark();
}

@Override
public CacheStats snapshot() {
// Item weight is always 1, evictions count and weight are always identical.
// We don't measure load success/failure/timing information.
long evictionsCount = 0;
for (Meter meter : evictions) {
evictionsCount += meter.getCount();
}
return CacheStats.of(hits.getCount(), misses.getCount(), 0, 0, 0, evictionsCount, evictionsCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ public static <M extends ObjectMapper, B extends MapperBuilder<M, B>> B withDefa
* </ul>
*/
public static ObjectMapper withDefaultModules(ObjectMapper mapper) {
mapper.setTypeFactory(new CaffeineCachingTypeFactory());
return mapper.registerModule(new GuavaModule())
return mapper.setTypeFactory(new CaffeineCachingTypeFactory())
.registerModule(new GuavaModule())
.registerModule(new ShimJdk7Module())
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModules(ObjectMapperOptimizations.createModules())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
options:
javaPackage: com.palantir.conjure.java.serialization
javaVisibility: packagePrivate
namespaces:
json.parser:
docs: Metrics produced instrumented Jackson components.
metrics:
string.length:
type: histogram
tags:
- format
docs: Histogram describing the length of strings parsed from input.
json.databind.typefactory.cache:
docs: Metrics produced by the Jackson Databind TypeFactory cache.
metrics:
hit:
type: meter
docs: Rate at which cache lookups are successful.
miss:
type: meter
docs: Rate at which cache lookups miss and require computation.
eviction:
type: meter
docs: Rate at which cache entries are removed, tagged by the cause for removal.
tags: [reason]

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
Expand All @@ -30,6 +31,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.palantir.logsafe.Preconditions;
import com.palantir.tritium.metrics.registry.SharedTaggedMetricRegistries;
Expand Down Expand Up @@ -322,8 +324,7 @@ public void testOptionalIntOverflowDeserialization() {
@Test
public void testStringMetrics_json() throws IOException {
TaggedMetricRegistry registry = SharedTaggedMetricRegistries.getSingleton();
// Unregister all metrics
registry.forEachMetric((name, _value) -> registry.remove(name));
removeJsonParserMetrics(registry);
Histogram stringLength = JsonParserMetrics.of(registry).stringLength(JsonFactory.FORMAT_NAME_JSON);
assertThat(stringLength.getSnapshot().size()).isZero();
// Length must exceed the minimum threshold for metrics
Expand All @@ -337,8 +338,7 @@ public void testStringMetrics_json() throws IOException {
@Test
public void testStringMetricsNotRecordedWhenValuesAreSmall_json() throws IOException {
TaggedMetricRegistry registry = SharedTaggedMetricRegistries.getSingleton();
// Unregister all metrics
registry.forEachMetric((name, _value) -> registry.remove(name));
removeJsonParserMetrics(registry);
Histogram stringLength = JsonParserMetrics.of(registry).stringLength(JsonFactory.FORMAT_NAME_JSON);
assertThat(stringLength.getSnapshot().size()).isZero();
String expected = "Hello, World!";
Expand All @@ -350,8 +350,7 @@ public void testStringMetricsNotRecordedWhenValuesAreSmall_json() throws IOExcep
@Test
public void testStringMetrics_smile() throws IOException {
TaggedMetricRegistry registry = SharedTaggedMetricRegistries.getSingleton();
// Unregister all metrics
registry.forEachMetric((name, _value) -> registry.remove(name));
removeJsonParserMetrics(registry);
Histogram stringLength = JsonParserMetrics.of(registry).stringLength(SmileFactory.FORMAT_NAME_SMILE);
assertThat(stringLength.getSnapshot().size()).isZero();
// Length must exceed the minimum threshold for metrics
Expand All @@ -366,8 +365,7 @@ public void testStringMetrics_smile() throws IOException {
@Test
public void testStringMetricsNotRecordedWhenValuesAreSmall_smile() throws IOException {
TaggedMetricRegistry registry = SharedTaggedMetricRegistries.getSingleton();
// Unregister all metrics
registry.forEachMetric((name, _value) -> registry.remove(name));
removeJsonParserMetrics(registry);
Histogram stringLength = JsonParserMetrics.of(registry).stringLength(SmileFactory.FORMAT_NAME_SMILE);
assertThat(stringLength.getSnapshot().size()).isZero();
String expected = "Hello, World!";
Expand All @@ -377,6 +375,15 @@ public void testStringMetricsNotRecordedWhenValuesAreSmall_smile() throws IOExce
assertThat(stringLength.getSnapshot().size()).isZero();
}

private static void removeJsonParserMetrics(TaggedMetricRegistry registry) {
// Unregister relevant metrics
registry.forEachMetric((name, _value) -> {
if (name.safeName().startsWith("json.parser")) {
registry.remove(name);
}
});
}

@Test
public void testJsonFormatName() {
assertThat(ObjectMappers.newServerJsonMapper().getFactory().getFormatName())
Expand Down Expand Up @@ -421,6 +428,34 @@ private void testTypeFactory(ObjectMapper mapper) {
assertThat(mapper.getTypeFactory()).isInstanceOf(CaffeineCachingTypeFactory.class);
}

@Test
public void testTypeFactoryCacheMetrics() {
TaggedMetricRegistry registry = SharedTaggedMetricRegistries.getSingleton();
JsonDatabindTypefactoryCacheMetrics metrics = JsonDatabindTypefactoryCacheMetrics.of(registry);
Meter hit = metrics.hit();
Meter miss = metrics.miss();
TypeFactory typeFactory = ObjectMappers.newServerJsonMapper().getTypeFactory();

long hitBeforeFirst = hit.getCount();
long missBeforeFirst = miss.getCount();
typeFactory.constructType(SimpleSerializable.class);
assertThat(miss.getCount() - missBeforeFirst).isOne();
assertThat(hit.getCount() - hitBeforeFirst).isZero();
// After writing the same type again, we should observe hits and no additional misses.
long hitBeforeSecond = hit.getCount();
long missBeforeSecond = miss.getCount();
typeFactory.constructType(SimpleSerializable.class);
assertThat(miss.getCount() - missBeforeSecond).isZero();
assertThat(hit.getCount() - hitBeforeSecond).isOne();
}

static final class SimpleSerializable {
@JsonProperty("str")
public String toString() {
return "stringValue";
}
}

@Test
public void testMapKeysAreNotInterned() throws IOException {
testMapKeysAreNotInterned(ObjectMappers.newServerJsonMapper());
Expand Down