PERFORMANCE: Much faster ConvertedMap

* Use IdentityHashMap since we intern all possible keys anyways and can look them up more efficiently than `String.intern()` from our PathCache
* Use the PathCache to never have to instantiate new `String` when converting a RubyHash

Fixes #8104
This commit is contained in:
Armin 2017-08-30 12:23:12 +02:00 committed by Armin Braun
parent 7f3df27fa8
commit e23780d042
3 changed files with 58 additions and 12 deletions

View file

@ -103,7 +103,7 @@ public final class Accessors {
private static Object setChild(final Object target, final String key, final Object value) {
if (target instanceof Map) {
((ConvertedMap) target).put(key, value);
((ConvertedMap) target).putInterned(key, value);
return value;
} else {
return setOnList(key, value, (ConvertedList) target);
@ -112,7 +112,7 @@ public final class Accessors {
private static Object createChild(final ConvertedMap target, final String key) {
final Object result = new ConvertedMap(1);
target.put(key, result);
target.putInterned(key, result);
return result;
}

View file

@ -2,14 +2,27 @@ package org.logstash;
import java.io.Serializable;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import org.jruby.RubyHash;
import org.jruby.RubyString;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
public final class ConvertedMap extends HashMap<String, Object> {
/**
* <p>This class is an internal API and behaves very different from a standard {@link Map}.</p>
* <p>The {@code get} method only has defined behaviour when used with an interned {@link String}
* as key.</p>
* <p>The {@code put} method will work with any {@link String} key but is only intended for use in
* situations where {@link ConvertedMap#putInterned(String, Object)} would require manually
* interning the {@link String} key. This is due to the fact that we use our internal
* {@link PathCache} to get an interned version of the given key instead of JDKs
* {@link String#intern()}, which is faster since it works from a much smaller and hotter cache
* in {@link PathCache} than using String interning directly.</p>
*/
public final class ConvertedMap extends IdentityHashMap<String, Object> {
private static final long serialVersionUID = -4651798808586901122L;
private static final long serialVersionUID = 1L;
private static final RubyHash.VisitorWithState<ConvertedMap> RUBY_HASH_VISITOR =
new RubyHash.VisitorWithState<ConvertedMap>() {
@ -17,18 +30,27 @@ public final class ConvertedMap extends HashMap<String, Object> {
public void visit(final ThreadContext context, final RubyHash self,
final IRubyObject key, final IRubyObject value,
final int index, final ConvertedMap state) {
state.put(key.toString(), Valuefier.convert(value));
if (key instanceof RubyString) {
state.putInterned(convertKey((RubyString) key), Valuefier.convert(value));
} else {
state.put(key.toString(), Valuefier.convert(value));
}
}
};
ConvertedMap(final int size) {
super((size << 2) / 3 + 2);
super(size);
}
public static ConvertedMap newFromMap(Map<Serializable, Object> o) {
ConvertedMap cm = new ConvertedMap(o.size());
for (final Map.Entry<Serializable, Object> entry : o.entrySet()) {
cm.put(entry.getKey().toString(), Valuefier.convert(entry.getValue()));
final Serializable found = entry.getKey();
if (found instanceof String) {
cm.put((String) found, Valuefier.convert(entry.getValue()));
} else {
cm.putInterned(convertKey((RubyString) found), entry.getValue());
}
}
return cm;
}
@ -43,6 +65,21 @@ public final class ConvertedMap extends HashMap<String, Object> {
return result;
}
@Override
public Object put(final String key, final Object value) {
return super.put(PathCache.cache(key).getKey(), value);
}
/**
* <p>Behaves like a standard {@link Map#put(Object, Object)} but without the return value.</p>
* <p>Only produces correct results if the given {@code key} is an interned {@link String}.</p>
* @param key Interned String
* @param value Value to put
*/
public void putInterned(final String key, final Object value) {
super.put(key, value);
}
public Object unconvert() {
final HashMap<String, Object> result = new HashMap<>(size());
for (final Map.Entry<String, Object> entry : entrySet()) {
@ -50,4 +87,13 @@ public final class ConvertedMap extends HashMap<String, Object> {
}
return result;
}
/**
* Converts a {@link RubyString} into a {@link String} that is guaranteed to be interned.
* @param key RubyString to convert
* @return Interned String
*/
private static String convertKey(final RubyString key) {
return PathCache.cache(key.getByteList()).getKey();
}
}

View file

@ -44,10 +44,10 @@ public final class Event implements Cloneable, Queueable {
{
this.metadata = new ConvertedMap(10);
this.data = new ConvertedMap(10);
this.data.put(VERSION, VERSION_ONE);
this.data.putInterned(VERSION, VERSION_ONE);
this.cancelled = false;
this.timestamp = new Timestamp();
this.data.put(TIMESTAMP, this.timestamp);
this.data.putInterned(TIMESTAMP, this.timestamp);
}
/**
@ -68,7 +68,7 @@ public final class Event implements Cloneable, Queueable {
public Event(ConvertedMap data) {
this.data = data;
if (!this.data.containsKey(VERSION)) {
this.data.put(VERSION, VERSION_ONE);
this.data.putInterned(VERSION, VERSION_ONE);
}
if (this.data.containsKey(METADATA)) {
@ -120,7 +120,7 @@ public final class Event implements Cloneable, Queueable {
public void setTimestamp(Timestamp t) {
this.timestamp = t;
this.data.put(TIMESTAMP, this.timestamp);
this.data.putInterned(TIMESTAMP, this.timestamp);
}
public Object getField(final String reference) {