Skip to content

Commit

Permalink
Optimize Jackson data decoder to create DataMaps with proper capacity.
Browse files Browse the repository at this point in the history
RB=1851797
G=si-java-reviewers,sf-reviewers
R=kramgopa,kbalasub
A=kbalasub
  • Loading branch information
countmdm committed Dec 4, 2019
1 parent 51abe56 commit c8d2716
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 29 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
28.0.11
-------
(RB=1851797)
Optimize Jackson data decoder to create DataMaps with proper capacity.

28.0.10
-------
(RB=1896607)
Expand Down
124 changes: 124 additions & 0 deletions data/src/main/java/com/linkedin/data/DataMapBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright (c) 2019 LinkedIn Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.linkedin.data;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;


/**
* This class exists for one purpose: to create a DataMap with correct (small) capacity
* when the number of elements in it is small. With Jackson codec the size of DataMap
* is not communicated upfront, so we cannot create a proper DataMap upfront. We also
* don't want to create every DataMap with initial small capacity, since filling out
* bigger DataMaps will result in repeated resizing/rehashing operations, which is
* expensive. Thus we use the class below to temporarily hold the contents of a DataMap
* currently being processed, and only up to the point when the resulting DataMap's
* capacity is smaller than the default value of 16.
*/
public class DataMapBuilder implements DataComplex {

private List<Object> _dataMapContents = new ArrayList<>(20);
private boolean _inUse;

public void addKVPair(String field, Object value)
{
assert _inUse;
_dataMapContents.add(field);
_dataMapContents.add(value);
}

/**
* Returns true when the accumulated contents size reaches the point (6 key-value pairs),
* after which the resulting DataMap, assuming the default load factor of 0.75, will be
* created with the standard capacity of 16.
*/
public boolean smallHashMapThresholdReached() { return _dataMapContents.size() >= 12; }

public DataMap convertToDataMap()
{
DataMap dataMap = new DataMap(optimumCapacityFromSize());
for (int i = 0; i < _dataMapContents.size(); i += 2)
{
dataMap.put((String) _dataMapContents.get(i), _dataMapContents.get(i+1));
}
_dataMapContents.clear();
_inUse = false;
return dataMap;
}

public boolean inUse() { return _inUse; }

public void setInUse(boolean v) { _inUse = v; }

private int optimumCapacityFromSize() {
return getOptimumHashMapCapacityFromSize(_dataMapContents.size());
}

/**
* If the proposed hash map size is such that there is a capacity that fits it exactly, for example
* size 6 and capacity 8, performs an exact int calculation and returns the capacity. Otherwise,
* uses an approximate formula with the float load factor, which usually returns a higher number.
* Assumes the default load factor of 0.75.
*/
public static int getOptimumHashMapCapacityFromSize(int size) {
return (size % 3 == 0) ? size * 4 / 3 : ((int) (size / 0.75) + 1);
}

// The methods below are present only to implement DataComplex interface. They should not be used.
@Override
public void makeReadOnly() { throw new UnsupportedOperationException(); }

@Override
public boolean isMadeReadOnly() { throw new UnsupportedOperationException(); }

@Override
public Collection<Object> values() { throw new UnsupportedOperationException(); }

@Override
public DataComplex clone() throws CloneNotSupportedException { throw new UnsupportedOperationException(); }

@Override
public void setReadOnly() { throw new UnsupportedOperationException(); }

@Override
public boolean isReadOnly() { throw new UnsupportedOperationException(); }

@Override
public void invalidate() { throw new UnsupportedOperationException(); }

@Override
public DataComplex copy() throws CloneNotSupportedException { throw new UnsupportedOperationException(); }

@Override
public int dataComplexHashCode() { throw new UnsupportedOperationException(); }

@Override
public void startInstrumentingAccess() { throw new UnsupportedOperationException(); }

@Override
public void stopInstrumentingAccess() { throw new UnsupportedOperationException(); }

@Override
public void clearInstrumentedData() { throw new UnsupportedOperationException(); }

@Override
public void collectInstrumentedData(StringBuilder keyPrefix, Map<String, Map<String, Object>> instrumentedData,
boolean collectAllData) { throw new UnsupportedOperationException(); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.linkedin.data.DataComplex;
import com.linkedin.data.DataList;
import com.linkedin.data.DataMap;
import com.linkedin.data.DataMapBuilder;
import com.linkedin.data.collections.CheckedUtil;
import com.linkedin.util.FastByteArrayOutputStream;
import java.io.Closeable;
Expand Down Expand Up @@ -341,6 +342,8 @@ private static class Parser
private Deque<Object> _nameStack = null;
private Map<Object, DataLocation> _locationMap = null;

private DataMapBuilder _currDataMapBuilder = new DataMapBuilder();

Parser()
{
this(false);
Expand Down Expand Up @@ -387,7 +390,7 @@ List<Object> parse(JsonParser parser, StringBuilder mesg, Map<Object, DataLocati
JsonToken token;
while ((token = _parser.nextToken()) != null)
{
parse(list, null, null, token);
parse(list, null, token);
}
_errorBuilder = null;

Expand All @@ -412,8 +415,7 @@ <T extends DataComplex> T parse(JsonParser parser, Class<T> expectType) throws I
throw new DataDecodingException("Object must start with start object token.");
}

final DataMap map = new DataMap();
parseDataMap(map);
final DataMap map = parseDataMap();
if (_errorBuilder != null)
{
map.addError(_errorBuilder.toString());
Expand All @@ -427,8 +429,7 @@ else if (expectType == DataList.class)
throw new DataDecodingException("Array must start with start object token.");
}

final DataList list = new DataList();
parseDataList(list);
final DataList list = parseDataList();
if (_errorBuilder != null)
{
//list.addError(_errorBuilder.toString());
Expand Down Expand Up @@ -457,7 +458,7 @@ private void saveDataLocation(Object o, DataLocation location)
}
}

private Object parse(DataList parentList, DataMap parentMap, String name, JsonToken token) throws IOException
private Object parse(DataComplex parent, String name, JsonToken token) throws IOException
{
if (token == null)
{
Expand All @@ -468,34 +469,34 @@ private Object parse(DataList parentList, DataMap parentMap, String name, JsonTo
switch (token)
{
case START_OBJECT:
DataMap childMap = new DataMap();
value = childMap;
updateParent(parentList, parentMap, name, childMap);
parseDataMap(childMap);
value = parseDataMap();
updateParent(parent, name, value);
break;
case START_ARRAY:
DataList childList = new DataList();
value = childList;
updateParent(parentList, parentMap, name, childList);
parseDataList(childList);
value = parseDataList();
updateParent(parent, name, value);
break;
default:
value = parsePrimitive(token);
if (value != null)
{
updateParent(parentList, parentMap, name, value);
updateParent(parent, name, value);
}
break;
}
saveDataLocation(value, location);
return value;
}

private void updateParent(DataList parentList, DataMap parentMap, String name, Object value)
private void updateParent(DataComplex parent, String name, Object value)
{
if (parentMap != null)
if (parent instanceof DataMapBuilder)
{
((DataMapBuilder) parent).addKVPair(name, value);
}
else if (parent instanceof DataMap)
{
Object replaced = CheckedUtil.putWithoutChecking(parentMap, name, value);
Object replaced = CheckedUtil.putWithoutChecking((DataMap) parent, name, value);
if (replaced != null)
{
if (_errorBuilder == null)
Expand All @@ -507,7 +508,7 @@ private void updateParent(DataList parentList, DataMap parentMap, String name, O
}
else
{
CheckedUtil.addWithoutChecking(parentList, value);
CheckedUtil.addWithoutChecking((DataList) parent, value);
}
}

Expand Down Expand Up @@ -570,8 +571,15 @@ private Object parsePrimitive(JsonToken token) throws IOException
return object;
}

private void parseDataMap(DataMap map) throws IOException
private DataMap parseDataMap() throws IOException
{
if (_currDataMapBuilder.inUse())
{
_currDataMapBuilder = new DataMapBuilder();
}
_currDataMapBuilder.setInUse(true);
DataMapBuilder mapBuilder = _currDataMapBuilder;
DataComplex map = _currDataMapBuilder;
while (_parser.nextToken() != JsonToken.END_OBJECT)
{
String key = _parser.getCurrentName();
Expand All @@ -580,16 +588,24 @@ private void parseDataMap(DataMap map) throws IOException
_nameStack.addLast(key);
}
JsonToken token = _parser.nextToken();
parse(null, map, key, token);
parse(map, key, token);
if (mapBuilder != null && mapBuilder.smallHashMapThresholdReached())
{
map = mapBuilder.convertToDataMap();
mapBuilder = null;
}
if (_debug)
{
_nameStack.removeLast();
}
}

return mapBuilder != null ? mapBuilder.convertToDataMap() : (DataMap) map;
}

private void parseDataList(DataList list) throws IOException
private DataList parseDataList() throws IOException
{
DataList list = new DataList();
JsonToken token;
int index = 0;
while ((token = _parser.nextToken()) != JsonToken.END_ARRAY)
Expand All @@ -599,12 +615,13 @@ private void parseDataList(DataList list) throws IOException
_nameStack.addLast(index);
index++;
}
parse(list, null, null, token);
parse(list, null, token);
if (_debug)
{
_nameStack.removeLast();
}
}
return list;
}

private void error(JsonToken token, JsonParser.NumberType type) throws IOException
Expand Down
Loading

0 comments on commit c8d2716

Please sign in to comment.