Author: tboehme
Date: Sat Mar 26 23:50:11 2016
New Revision: 1736709
URL: http://svn.apache.org/viewvc?rev=1736709&view=rev
Log:
PDFBOX-3284: reduce memory footprint of COSDictionary by using a memory
efficient Map implementation
Added:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/util/SmallMap.java
(with props)
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDictionary.java
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDictionary.java
URL:
http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDictionary.java?rev=1736709&r1=1736708&r2=1736709&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDictionary.java
(original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/cos/COSDictionary.java
Sat Mar 26 23:50:11 2016
@@ -17,14 +17,19 @@
package org.apache.pdfbox.cos;
import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.util.DateConverter;
+import org.apache.pdfbox.util.SmallMap;
/**
* This class represents a dictionary where name/value pairs reside.
@@ -34,13 +39,15 @@ import org.apache.pdfbox.util.DateConver
*/
public class COSDictionary extends COSBase implements COSUpdateInfo
{
+
private static final String PATH_SEPARATOR = "/";
private boolean needToBeUpdated;
/**
* The name-value pairs of this dictionary. The pairs are kept in the
order they were added to the dictionary.
*/
- protected Map<COSName, COSBase> items = new LinkedHashMap<COSName,
COSBase>();
+// protected Map<COSName, COSBase> items = new LinkedHashMap<COSName,
COSBase>();
+ protected Map<COSName, COSBase> items = new SmallMap<COSName, COSBase>();
/**
* Constructor.
@@ -48,6 +55,7 @@ public class COSDictionary extends COSBa
public COSDictionary()
{
// default constructor
+ debugInstanceCount();
}
/**
@@ -58,8 +66,54 @@ public class COSDictionary extends COSBa
public COSDictionary(COSDictionary dict)
{
items.putAll(dict.items);
+
+ debugInstanceCount();
}
+ private static final boolean DO_DEBUG_INSTANCE_COUNT = true;
+ private static final List<WeakReference<COSDictionary>> DICT_INSTANCES =
+ DO_DEBUG_INSTANCE_COUNT ? new
ArrayList<WeakReference<COSDictionary>>() : null;
+
+ /**
+ * Only for memory debugging purposes (especially PDFBOX-3284): holds weak
+ * references to all instances and prints after each 10,000th instance a
+ * statistic across all instances showing how many instances we have per
+ * dictionary size (item count).
+ * This is to show that there can be a large number of COSDictionary
instances
+ * but each having only few items, thus using a {@link LinkedHashMap} is a
+ * waste of memory resources.
+ *
+ * <p>This method should be removed if further testing of COSDictionary
uses
+ * is not needed anymore.</p>
+ */
+ private final void debugInstanceCount()
+ {
+ if (DO_DEBUG_INSTANCE_COUNT)
+ {
+ synchronized (DICT_INSTANCES)
+ {
+ DICT_INSTANCES.add(new WeakReference<COSDictionary>(this));
+ // print statistics at each 10,000th instance
+ if (DICT_INSTANCES.size() % 10000 == 0)
+ {
+ int[] sizeCount = new int[100];
+ for (WeakReference<COSDictionary> dict : DICT_INSTANCES)
+ {
+ COSDictionary curDict = dict.get();
+ if (curDict != null)
+ {
+ int sizeIdx = curDict.size();
+ sizeCount[sizeIdx < sizeCount.length ? sizeIdx
+ : sizeCount.length - 1]++;
+ }
+ }
+ System.out.println("COSDictionary: dictionary size
occurrences: " + Arrays.toString(sizeCount));
+ }
+ }
+ }
+ }
+
+
/**
* @see java.util.Map#containsValue(java.lang.Object)
*
Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/util/SmallMap.java
URL:
http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/util/SmallMap.java?rev=1736709&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/util/SmallMap.java
(added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/util/SmallMap.java Sat
Mar 26 23:50:11 2016
@@ -0,0 +1,388 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.pdfbox.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Map implementation with a smallest possible memory usage.
+ * It should only be used for maps with small number of items
+ * (e.g. <30) since most operations have an O(n) complexity.
+ * Thus it should be used in cases with large number of map
+ * objects, each having only few items.
+ *
+ * <p><code>null</code> is not supported for keys or values.</p>
+ */
+public class SmallMap<K, V> implements Map<K, V>
+{
+ /**
+ * stores key-value pair as 2 objects; key first; in case of empty map
this might be <code>null</code>
+ */
+ private Object[] mapArr;
+
+ /** Creates empty map. */
+ public SmallMap()
+ {
+ }
+
+ /** Creates map filled with entries from provided map. */
+ public SmallMap(Map<? extends K, ? extends V> initMap)
+ {
+ putAll(initMap);
+ }
+
+ /**
+ * Returns index of key within map-array or <code>-1</code>
+ * if key is not found (or key is <code>null</code>).
+ */
+ private final int findKey(Object key)
+ {
+ if (isEmpty() || (key==null))
+ {
+ return -1;
+ }
+
+ for ( int aIdx = 0; aIdx < mapArr.length; aIdx+=2 )
+ {
+ if (key.equals(mapArr[aIdx]))
+ {
+ return aIdx;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns index of value within map-array or <code>-1</code>
+ * if value is not found (or value is <code>null</code>).
+ */
+ private final int findValue(Object value)
+ {
+ if (isEmpty() || (value==null))
+ {
+ return -1;
+ }
+
+ for ( int aIdx = 1; aIdx < mapArr.length; aIdx+=2 )
+ {
+ if (value.equals(mapArr[aIdx]))
+ {
+ return aIdx;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public int size()
+ {
+ return mapArr == null ? 0 : mapArr.length >> 1;
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return (mapArr == null) || (mapArr.length == 0);
+ }
+
+ @Override
+ public boolean containsKey(Object key)
+ {
+ return findKey(key) >= 0;
+ }
+
+ @Override
+ public boolean containsValue(Object value)
+ {
+ return findValue(value) >= 0;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public V get(Object key)
+ {
+ int kIdx = findKey(key);
+
+ return kIdx < 0 ? null : (V) mapArr[kIdx+1];
+ }
+
+ @Override
+ public V put(K key, V value)
+ {
+ if ((key == null) || (value == null))
+ {
+ throw new NullPointerException( "Key or value must not be null.");
+ }
+
+ if (mapArr == null)
+ {
+ mapArr = new Object[] { key, value };
+ return null;
+ }
+ else
+ {
+ int kIdx = findKey(key);
+
+ if (kIdx < 0)
+ {
+ // key unknown
+ int oldLen = mapArr.length;
+ Object[] newMapArr = new Object[oldLen+2];
+ System.arraycopy(mapArr, 0, newMapArr, 0, oldLen);
+ newMapArr[oldLen] = key;
+ newMapArr[oldLen+1] = value;
+ mapArr = newMapArr;
+ return null;
+ }
+ else
+ {
+ // key exists; replace value
+ @SuppressWarnings("unchecked")
+ V oldValue = (V) mapArr[kIdx+1];
+ mapArr[kIdx+1] = value;
+ return oldValue;
+ }
+ }
+ }
+
+ @Override
+ public V remove(Object key)
+ {
+ int kIdx = findKey(key);
+
+ if (kIdx < 0)
+ {
+ // not found
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ V oldValue = (V) mapArr[kIdx+1];
+ int oldLen = mapArr.length;
+
+ if (oldLen == 2)
+ {
+ // was last entry
+ mapArr = null;
+ }
+ else
+ {
+ Object[] newMapArr = new Object[oldLen-2];
+ System.arraycopy(mapArr, 0, newMapArr, 0, kIdx);
+ System.arraycopy(mapArr, kIdx+2, newMapArr, kIdx, oldLen - kIdx -
2);
+ mapArr = newMapArr;
+ }
+
+ return oldValue;
+ }
+
+ @Override
+ public void putAll(Map<? extends K, ? extends V> otherMap)
+ {
+ if ((mapArr == null) || (mapArr.length == 0))
+ {
+ // existing map is empty
+ mapArr = new Object[otherMap.size() << 1];
+ int aIdx = 0;
+ for (Entry<? extends K, ? extends V> entry : otherMap.entrySet())
+ {
+ if ((entry.getKey() == null) || (entry.getValue() == null))
+ {
+ throw new NullPointerException( "Key or value must not be
null.");
+ }
+
+ mapArr[aIdx++] = entry.getKey();
+ mapArr[aIdx++] = entry.getValue();
+ }
+ }
+ else
+ {
+ int oldLen = mapArr.length;
+ // first increase array size to hold all to put entries as if they
have unknown keys
+ // reduce after adding all to the required size
+ Object[] newMapArr = new Object[oldLen+(otherMap.size() << 1)];
+ System.arraycopy(mapArr, 0, newMapArr, 0, oldLen);
+
+ int newIdx = oldLen;
+ for (Entry<? extends K, ? extends V> entry : otherMap.entrySet())
+ {
+ if ((entry.getKey() == null) || (entry.getValue() == null))
+ {
+ throw new NullPointerException( "Key or value must not be
null.");
+ }
+
+ int existKeyIdx = findKey(entry.getKey());
+
+ if (existKeyIdx >= 0)
+ {
+ // existing key
+ newMapArr[existKeyIdx+1] = entry.getValue();
+ }
+ else
+ {
+ // new key
+ newMapArr[newIdx++] = entry.getKey();
+ newMapArr[newIdx++] = entry.getValue();
+ }
+ }
+
+ if (newIdx < newMapArr.length)
+ {
+ Object[] reducedMapArr = new Object[newIdx];
+ System.arraycopy(newMapArr, 0, reducedMapArr, 0, newIdx);
+ newMapArr = reducedMapArr;
+ }
+
+ mapArr = newMapArr;
+ }
+ }
+
+ @Override
+ public void clear()
+ {
+ mapArr = null;
+ }
+
+ /**
+ * Returns a set view of the keys contained in this map.
+ *
+ * <p>The current implementation does not allow changes to the
+ * returned key set (which would have to be reflected in the
+ * underlying map.</p>
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public Set<K> keySet()
+ {
+ if (isEmpty())
+ {
+ return Collections.emptySet();
+ }
+
+ Set<K> keys = new LinkedHashSet<K>();
+ for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2)
+ {
+ keys.add((K)mapArr[kIdx]);
+ }
+ return Collections.unmodifiableSet( keys );
+ }
+
+ /**
+ * Returns a collection of the values contained in this map.
+ *
+ * <p>The current implementation does not allow changes to the
+ * returned collection (which would have to be reflected in the
+ * underlying map.</p>
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection<V> values()
+ {
+ if (isEmpty())
+ {
+ return Collections.emptySet();
+ }
+
+ List<V> values = new ArrayList<V>(mapArr.length >> 1);
+ for (int vIdx = 1; vIdx < mapArr.length; vIdx+=2)
+ {
+ values.add((V)mapArr[vIdx]);
+ }
+ return Collections.unmodifiableList( values );
+ }
+
+ private class SmallMapEntry implements Entry<K, V>
+ {
+ private final int keyIdx;
+
+ public SmallMapEntry(int keyInMapIdx)
+ {
+ keyIdx = keyInMapIdx;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public K getKey()
+ {
+ return (K)mapArr[keyIdx];
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public V getValue()
+ {
+ return (V)mapArr[keyIdx+1];
+ }
+
+ @Override
+ public V setValue(V value)
+ {
+ if (value == null)
+ {
+ throw new NullPointerException( "Key or value must not be
null.");
+ }
+
+ V oldValue = getValue();
+ mapArr[keyIdx+1] = value;
+ return oldValue;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return getKey().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (!(obj instanceof SmallMap.SmallMapEntry))
+ {
+ return false;
+ }
+ @SuppressWarnings("unchecked")
+ SmallMapEntry other = (SmallMapEntry) obj;
+
+ return getKey().equals(other.getKey()) &&
getValue().equals(other.getValue());
+ }
+ }
+
+ @Override
+ public Set<java.util.Map.Entry<K, V>> entrySet()
+ {
+ if (isEmpty())
+ {
+ return Collections.emptySet();
+ }
+
+ Set<java.util.Map.Entry<K, V>> entries = new
LinkedHashSet<java.util.Map.Entry<K, V>>();
+ for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2)
+ {
+ entries.add(new SmallMapEntry(kIdx));
+ }
+ return Collections.unmodifiableSet( entries );
+ }
+
+}
Propchange:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/util/SmallMap.java
------------------------------------------------------------------------------
svn:mime-type = text/plain