com.netflix.discovery.util.DeserializerStringCache Maven / Gradle / Ivy
package com.netflix.discovery.util;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.PrimitiveIterator.OfInt;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectReader;
/**
* A non-locking alternative to {@link String#intern()} and {@link StringCache}
* that works with Jackson's DeserializationContext. Definitely NOT thread-safe,
* intended to avoid the costs associated with thread synchronization and
* short-lived heap allocations (e.g., Strings)
*
*/
public class DeserializerStringCache implements Function {
public enum CacheScope {
// Strings in this scope are freed on deserialization of each
// Application element
APPLICATION_SCOPE,
// Strings in this scope are freed when overall deserialization is
// completed
GLOBAL_SCOPE
}
private static final Logger logger = LoggerFactory.getLogger(DeserializerStringCache.class);
private static final boolean debugLogEnabled = logger.isDebugEnabled();
private static final String ATTR_STRING_CACHE = "deserInternCache";
private static final int LENGTH_LIMIT = 256;
private static final int LRU_LIMIT = 1024 * 40;
private final Map globalCache;
private final Map applicationCache;
private final int lengthLimit = LENGTH_LIMIT;
/**
* adds a new DeserializerStringCache to the passed-in ObjectReader
*
* @param reader
* @return a wrapped ObjectReader with the string cache attribute
*/
public static ObjectReader init(ObjectReader reader) {
return reader.withAttribute(ATTR_STRING_CACHE, new DeserializerStringCache(
new HashMap(2048), new LinkedHashMap(4096, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > LRU_LIMIT;
}
}));
}
/**
* adds an existing DeserializerStringCache from the DeserializationContext
* to an ObjectReader
*
* @param reader
* a new ObjectReader
* @param context
* an existing DeserializationContext containing a
* DeserializerStringCache
* @return a wrapped ObjectReader with the string cache attribute
*/
public static ObjectReader init(ObjectReader reader, DeserializationContext context) {
return withCache(context, cache -> {
if (cache == null)
throw new IllegalStateException();
return reader.withAttribute(ATTR_STRING_CACHE, cache);
});
}
/**
* extracts a DeserializerStringCache from the DeserializationContext
*
* @param context
* an existing DeserializationContext containing a
* DeserializerStringCache
* @return a wrapped ObjectReader with the string cache attribute
*/
public static DeserializerStringCache from(DeserializationContext context) {
return withCache(context, cache -> {
if (cache == null) {
cache = new DeserializerStringCache(new HashMap(),
new HashMap());
}
return cache;
});
}
/**
* clears app-scoped cache entries from the specified ObjectReader
*
* @param reader
*/
public static void clear(ObjectReader reader) {
clear(reader, CacheScope.APPLICATION_SCOPE);
}
/**
* clears cache entries in the given scope from the specified ObjectReader.
* Always clears app-scoped entries.
*
* @param reader
* @param scope
*/
public static void clear(ObjectReader reader, final CacheScope scope) {
withCache(reader, cache -> {
if (scope == CacheScope.GLOBAL_SCOPE) {
if (debugLogEnabled)
logger.debug("clearing global-level cache with size {}", cache.globalCache.size());
cache.globalCache.clear();
}
if (debugLogEnabled)
logger.debug("clearing app-level serialization cache with size {}", cache.applicationCache.size());
cache.applicationCache.clear();
return null;
});
}
/**
* clears app-scoped cache entries from the specified DeserializationContext
*
* @param context
*/
public static void clear(DeserializationContext context) {
clear(context, CacheScope.APPLICATION_SCOPE);
}
/**
* clears cache entries in the given scope from the specified
* DeserializationContext. Always clears app-scoped entries.
*
* @param context
* @param scope
*/
public static void clear(DeserializationContext context, CacheScope scope) {
withCache(context, cache -> {
if (scope == CacheScope.GLOBAL_SCOPE) {
if (debugLogEnabled)
logger.debug("clearing global-level serialization cache with size {}", cache.globalCache.size());
cache.globalCache.clear();
}
if (debugLogEnabled)
logger.debug("clearing app-level serialization cache with size {}", cache.applicationCache.size());
cache.applicationCache.clear();
return null;
});
}
private static T withCache(DeserializationContext context, Function consumer) {
DeserializerStringCache cache = (DeserializerStringCache) context.getAttribute(ATTR_STRING_CACHE);
return consumer.apply(cache);
}
private static T withCache(ObjectReader reader, Function consumer) {
DeserializerStringCache cache = (DeserializerStringCache) reader.getAttributes()
.getAttribute(ATTR_STRING_CACHE);
return consumer.apply(cache);
}
private DeserializerStringCache(Map globalCache, Map applicationCache) {
this.globalCache = globalCache;
this.applicationCache = applicationCache;
}
public ObjectReader initReader(ObjectReader reader) {
return reader.withAttribute(ATTR_STRING_CACHE, this);
}
/**
* returns a String read from the JsonParser argument's current position.
* The returned value may be interned at the app scope to reduce heap
* consumption
*
* @param jp
* @return a possibly interned String
* @throws IOException
*/
public String apply(final JsonParser jp) throws IOException {
return apply(jp, CacheScope.APPLICATION_SCOPE, null);
}
public String apply(final JsonParser jp, CacheScope cacheScope) throws IOException {
return apply(jp, cacheScope, null);
}
/**
* returns a String read from the JsonParser argument's current position.
* The returned value may be interned at the given cacheScope to reduce heap
* consumption
*
* @param jp
* @param cacheScope
* @return a possibly interned String
* @throws IOException
*/
public String apply(final JsonParser jp, CacheScope cacheScope, Supplier source) throws IOException {
return apply(CharBuffer.wrap(jp, source), cacheScope);
}
/**
* returns a String that may be interned at app-scope to reduce heap
* consumption
*
* @param charValue
* @return a possibly interned String
*/
public String apply(final CharBuffer charValue) {
return apply(charValue, CacheScope.APPLICATION_SCOPE);
}
/**
* returns a object of type T that may be interned at the specified scope to
* reduce heap consumption
*
* @param charValue
* @param cacheScope
* @param trabsform
* @return a possibly interned instance of T
*/
public String apply(CharBuffer charValue, CacheScope cacheScope) {
int keyLength = charValue.length();
if ((lengthLimit < 0 || keyLength <= lengthLimit)) {
Map cache = (cacheScope == CacheScope.GLOBAL_SCOPE) ? globalCache : applicationCache;
String value = cache.get(charValue);
if (value == null) {
value = charValue.consume((k, v) -> {
cache.put(k, v);
});
} else {
// System.out.println("cache hit");
}
return value;
}
return charValue.toString();
}
/**
* returns a String that may be interned at the app-scope to reduce heap
* consumption
*
* @param stringValue
* @return a possibly interned String
*/
@Override
public String apply(final String stringValue) {
return apply(stringValue, CacheScope.APPLICATION_SCOPE);
}
/**
* returns a String that may be interned at the given scope to reduce heap
* consumption
*
* @param stringValue
* @param cacheScope
* @return a possibly interned String
*/
public String apply(final String stringValue, CacheScope cacheScope) {
if (stringValue != null && (lengthLimit < 0 || stringValue.length() <= lengthLimit)) {
return (String) (cacheScope == CacheScope.GLOBAL_SCOPE ? globalCache : applicationCache)
.computeIfAbsent(CharBuffer.wrap(stringValue), s -> {
logger.trace(" (string) writing new interned value {} into {} cache scope", stringValue, cacheScope);
return stringValue;
});
}
return stringValue;
}
public int size() {
return globalCache.size() + applicationCache.size();
}
private interface CharBuffer {
public static CharBuffer wrap(JsonParser source, Supplier stringSource) throws IOException {
return new ArrayCharBuffer(source, stringSource);
}
public static CharBuffer wrap(JsonParser source) throws IOException {
return new ArrayCharBuffer(source);
}
public static CharBuffer wrap(String source) {
return new StringCharBuffer(source);
}
String consume(BiConsumer valueConsumer);
int length();
OfInt chars();
static class ArrayCharBuffer implements CharBuffer {
private char[] source;
private int offset;
private final int length;
private int hash;
private Supplier valueTransform;
ArrayCharBuffer(JsonParser source) throws IOException {
this.source = source.getTextCharacters();
this.offset = source.getTextOffset();
this.length = source.getTextLength();
}
ArrayCharBuffer(JsonParser source, Supplier valueTransform) throws IOException {
this(source);
this.valueTransform = valueTransform;
}
@Override
public int length() {
return length;
}
@Override
public int hashCode() {
if (hash == 0 && length != 0) {
hash = arrayHash(source, offset, length);
}
return hash;
}
@Override
public boolean equals(Object other) {
if (other instanceof CharBuffer) {
CharBuffer otherBuffer = (CharBuffer) other;
if (otherBuffer.length() == length) {
OfInt otherText = otherBuffer.chars();
for (int i = offset; i < length; i++) {
if (source[i] != otherText.nextInt()) {
return false;
}
}
return true;
}
}
return false;
}
@Override
public OfInt chars() {
return new OfInt() {
int index = offset;
int limit = index + length;
@Override
public boolean hasNext() {
return index < limit;
}
@Override
public int nextInt() {
return source[index++];
}
};
}
@Override
public String toString() {
return new String(this.source, offset, length);
}
@Override
public String consume(BiConsumer valueConsumer) {
String key = new String(this.source, offset, length);
String value = valueTransform == null ? key : valueTransform.get();
valueConsumer.accept(new StringCharBuffer(key), value);
return value;
}
private static int arrayHash(char[] a, int offset, int length) {
if (a == null)
return 0;
int result = 0;
int limit = offset + length;
for (int i = offset; i < limit; i++) {
result = 31 * result + a[i];
}
return result;
}
}
static class StringCharBuffer implements CharBuffer {
private String source;
StringCharBuffer(String source) {
this.source = source;
}
@Override
public int hashCode() {
return source.hashCode();
}
@Override
public boolean equals(Object other) {
if (other instanceof CharBuffer) {
CharBuffer otherBuffer = (CharBuffer) other;
int length = source.length();
if (otherBuffer.length() == length) {
OfInt otherText = otherBuffer.chars();
for (int i = 0; i < length; i++) {
if (source.charAt(i) != otherText.nextInt()) {
return false;
}
}
return true;
}
}
return false;
}
@Override
public int length() {
return source.length();
}
@Override
public String toString() {
return source;
}
@Override
public OfInt chars() {
return new OfInt() {
int index;
@Override
public boolean hasNext() {
return index < source.length();
}
@Override
public int nextInt() {
return source.charAt(index++);
}
};
}
@Override
public String consume(BiConsumer valueConsumer) {
valueConsumer.accept(this, source);
return source;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy