BUG-650: optimize MapAdaptor a tiny bit
[yangtools.git] / common / util / src / main / java / org / opendaylight / yangtools / util / MapAdaptor.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.util;
9
10 import com.google.common.base.Preconditions;
11 import com.romix.scala.collection.concurrent.TrieMap;
12 import java.util.Collections;
13 import java.util.HashMap;
14 import java.util.Map;
15 import java.util.Map.Entry;
16 import org.slf4j.Logger;
17 import org.slf4j.LoggerFactory;
18
19 /**
20  * A simple layer on top of maps, which performs snapshot mediation and optimization of
21  * what the underlying implementation is.
22  */
23 public final class MapAdaptor {
24     public static final int DEFAULT_COPY_MAX_ITEMS = 100;
25     public static final String COPY_MAX_ITEMS_MAX_PROP = "org.opendaylight.yangtools.util.mapadaptor.maxcopy";
26
27     public static final int DEFAULT_PERSIST_MIN_ITEMS = 50;
28     public static final String PERSIST_MIN_ITEMS_PROP = "org.opendaylight.yangtools.util.mapadaptor.minpersist";
29
30     private static final Logger LOG = LoggerFactory.getLogger(MapAdaptor.class);
31     private static final MapAdaptor DEFAULT_INSTANCE;
32
33     private final boolean useSingleton;
34     private final int persistMinItems;
35     private final int copyMaxItems;
36
37     static {
38         DEFAULT_INSTANCE = new MapAdaptor(true,
39                 getProperty(COPY_MAX_ITEMS_MAX_PROP, DEFAULT_COPY_MAX_ITEMS),
40                 getProperty(PERSIST_MIN_ITEMS_PROP, DEFAULT_PERSIST_MIN_ITEMS));
41         LOG.debug("Configured HashMap/TrieMap cutoff at {}/{} entries",
42                 DEFAULT_INSTANCE.persistMinItems, DEFAULT_INSTANCE.copyMaxItems);
43     }
44
45     private static final int getProperty(final String name, final int defaultValue) {
46         try {
47             final String p = System.getProperty(name);
48             if (p != null) {
49                 try {
50                     int pi = Integer.valueOf(p);
51                     if (pi <= 0) {
52                         LOG.warn("Ignoring illegal value of {}: has to be a positive number", name);
53                     } else {
54                         return pi;
55                     }
56                 } catch (NumberFormatException e) {
57                     LOG.warn("Ignoring non-numerical value of {}", name, e);
58                 }
59             }
60         } catch (Exception e) {
61             LOG.debug("Failed to get {}", name, e);
62         }
63         return defaultValue;
64     }
65
66     private MapAdaptor(final boolean useSingleton, final int copyMaxItems, final int persistMinItems) {
67         this.useSingleton = useSingleton;
68         this.copyMaxItems = copyMaxItems;
69         this.persistMinItems = persistMinItems;
70     }
71
72     /**
73      * Return the default-configured instance.
74      *
75      * @return the singleton global instance
76      */
77     public static MapAdaptor getDefaultInstance() {
78         return DEFAULT_INSTANCE;
79     }
80
81     public static MapAdaptor getInstance(final boolean useSingleton, final int copyMaxItems, final int persistMinItems) {
82         Preconditions.checkArgument(copyMaxItems >= 0, "copyMaxItems has to be a non-negative integer");
83         Preconditions.checkArgument(persistMinItems >= 0, "persistMinItems has to be a positive integer");
84         Preconditions.checkArgument(persistMinItems <= copyMaxItems, "persistMinItems must be less than or equal to copyMaxItems");
85         return new MapAdaptor(useSingleton, copyMaxItems, persistMinItems);
86     }
87
88     /**
89      * Input is treated is supposed to be left unmodified, result must be mutable.
90      *
91      * @param input
92      * @return
93      */
94     public <K, V> Map<K, V> takeSnapshot(final Map<K, V> input) {
95         if (input instanceof ReadOnlyTrieMap) {
96             return ((ReadOnlyTrieMap<K, V>)input).toReadWrite();
97         }
98
99         LOG.trace("Converting input {} to a HashMap", input);
100
101         // FIXME: be a bit smart about allocation based on observed size
102
103         final Map<K, V> ret = new HashMap<>(input);
104         LOG.trace("Read-write HashMap is {}", ret);
105         return ret;
106     }
107
108     /**
109      * Input will be thrown away, result will be retained for read-only access or
110      * {@link #takeSnapshot(Map)} purposes.
111      *
112      * @param input
113      * @return
114      */
115     public <K, V> Map<K, V> optimize(final Map<K, V> input) {
116         if (input instanceof ReadOnlyTrieMap) {
117             LOG.warn("Optimizing read-only map {}", input);
118         }
119
120         final int size = input.size();
121
122         /*
123          * No-brainer :)
124          */
125         if (size == 0) {
126             LOG.trace("Reducing input {} to an empty map", input);
127             return Collections.emptyMap();
128         }
129
130         /*
131          * We retain the persistent map as long as it holds at least
132          * persistMinItems
133          */
134         if (input instanceof ReadWriteTrieMap && size >= persistMinItems) {
135             return ((ReadWriteTrieMap<K, V>)input).toReadOnly();
136         }
137
138         /*
139          * If the user opted to use singleton maps, use them. Except for the case
140          * when persistMinItems dictates we should not move off of the persistent
141          * map.
142          */
143         if (useSingleton && size == 1) {
144             final Entry<K, V> e = input.entrySet().iterator().next();
145             final Map<K, V> ret = Collections.singletonMap(e.getKey(), e.getValue());
146             LOG.trace("Reducing input {} to singleton map {}", input, ret);
147             return ret;
148         }
149
150         if (size <= copyMaxItems) {
151             /*
152              * Favor access speed: use a HashMap and copy it on modification.
153              */
154             if (input instanceof HashMap) {
155                 return input;
156             }
157
158             LOG.trace("Copying input {} to a HashMap ({} entries)", input, size);
159             final Map<K, V> ret = new HashMap<>(input);
160             LOG.trace("Read-only HashMap is {}", ret);
161             return ret;
162         }
163
164         /*
165          * Favor isolation speed: use a TrieMap and perform snapshots
166          *
167          * This one is a bit tricky, as the TrieMap is concurrent and does not
168          * keep an uptodate size. Updating it requires a full walk -- which is
169          * O(N) and we want to avoid that. So we wrap it in an interceptor,
170          * which will maintain the size for us.
171          */
172         LOG.trace("Copying input {} to a TrieMap ({} entries)", input, size);
173         final TrieMap<K, V> map = TrieMap.empty();
174         map.putAll(input);
175         final Map<K, V> ret = new ReadOnlyTrieMap<>(map, size);
176         LOG.trace("Read-only TrieMap is {}", ret);
177         return ret;
178     }
179 }