/** * Copyright 2008 Google Inc. * * 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 org.waveprotocol.wave.client.common.util; import com.google.gwt.core.client.JavaScriptObject; import org.waveprotocol.wave.model.util.ReadableIntMap.ProcV; import java.util.HashMap; import java.util.Map; /** * A super fast and memory efficient map (directly uses a js object as the map, * and requires only 1 object). Only allows int keys, thus taking advantage of * the "perfect hashing" of ints with respect to javascript. * * No one other than JsoIntMap should use this class. * TODO(danilatos): Move the clever logic into JsoIntMap and delete this file. * * NOTE(danilatos): Does not use hasOwnProperty semantics. So it's possible for * spurious entries to appear in the map if we're not careful. Easily fixable, * but would incur a slight performance hit (I'm now just handwaving). * * TODO(dan): Use a different version from the GWT team once it is available * * @author danilatos@google.com (Daniel Danilatos) * @param <T> Type of values in the map. Keys are always ints. */ final class IntMapJsoView<T> extends JsoMapBase { /** * A function that accepts an accumulated value, a key and the corresponding * item from the map and returns the new accumulated value. * * @see IntMapJsoView#reduce(Object, IntMapJsoView.Reduce) * @param <E> * int map's type parameter * @param <R> * The type of the value being accumulated */ public interface Reduce<E, R> { /** The function */ public R apply(R soFar, int key, E item); } /** Construct an empty IntMap */ public static native <T> IntMapJsoView<T> create() /*-{ return {}; }-*/; /** * Represent a java script native function that change the integer key into index key. * This is done because in chrome, it is 20 times faster to store string index than to store * numeric index if the number is > 2^15. * * The following time are recorded under chrome linux 4.0.266.0 * The recorderd time are in ms. * * iterations Numeric Index String index Hybrid time * 1024 1 2 0 * 2048 0 5 1 * 4096 1 14 1 * 8192 4 30 4 * 16384 8 63 9 * 32768 20 132 24 * 65536 266 264 148 * 131072 5551 513 404 * 262144 16719 1036 976 * * for numeric index < 2^15, it is 6 times faster to use numeric index * for string index > 2^15, it is 20 times faster to use string index * hybrid approach represents what we are doing here, i.e. use numeric index for value < 2^15 and * string index for value > 2^15 * * DESPITE THIS RESULT, it is very bad to use numeric index at all. Using the numeric index * slows down the javascript engine. We always uses String index in chrome. So DO NOT change * the code to do the hybrid approach. * * This function must be represented by JavaScriptObject and not normal java interfaces because * it's return data of different type depends on the input value. */ @SuppressWarnings("unused") // used in native method private static JavaScriptObject prefixer; @SuppressWarnings("unused") // used in native method private static JavaScriptObject evaler; static { setupPrefix(UserAgent.isChrome()); } private static native void setupPrefix(boolean usePrefix) /*-{ if (usePrefix) { @org.waveprotocol.wave.client.common.util.IntMapJsoView::prefixer = function(a) { return "a" + a; }; @org.waveprotocol.wave.client.common.util.IntMapJsoView::evaler = function(a) { return a[0] == "a" ? parseInt(a.substr(1, a.length)) : parseInt(a); } } else { @org.waveprotocol.wave.client.common.util.IntMapJsoView::prefixer = function(a) { return a; }; @org.waveprotocol.wave.client.common.util.IntMapJsoView::evaler = function(a) { return parseInt(a); } } }-*/; /** Construct a IntMap from a java Map */ public static <T> IntMapJsoView<T> fromMap(Map<Integer, T> map) { IntMapJsoView<T> intMap = create(); for (int key : map.keySet()) { intMap.put(key, map.get(key)); } return intMap; } protected IntMapJsoView() { } /** * @param key * @return true if a value indexed by the given key is in the map */ public native boolean has(int key) /*-{ var prefixer = @org.waveprotocol.wave.client.common.util.IntMapJsoView::prefixer; return this[prefixer(key)] !== undefined; }-*/; /** * @param key * @return The value with the given key, or null if not present */ public native T get(int key) /*-{ var prefixer = @org.waveprotocol.wave.client.common.util.IntMapJsoView::prefixer; return this[prefixer(key)]; }-*/; /** * Put the value in the map at the given key. Note: Does not return the old * value. * * @param key * @param value */ public native void put(int key, T value) /*-{ var prefixer = @org.waveprotocol.wave.client.common.util.IntMapJsoView::prefixer; this[prefixer(key)] = value; }-*/; /** * Remove the value with the given key from the map. Note: does not return the * old value. * * @param key */ public native void remove(int key) /*-{ var prefixer = @org.waveprotocol.wave.client.common.util.IntMapJsoView::prefixer; delete this[prefixer(key)]; }-*/; /** * Same as {@link #remove(int)}, but returns what was previously there, if * anything. * * @param key * @return what was previously there or null */ public final T removeAndReturn(int key) { T val = get(key); remove(key); return val; } /** * Ruby/prototype.js style iterating idiom, using a callbak. Equivalent to a * for-each loop. TODO(danilatos): Implement break and through a la * prototype.js if needed. * * @param proc */ public final native void each(ProcV<? super T> proc) /*-{ var evaler = @org.waveprotocol.wave.client.common.util.IntMapJsoView::evaler; for (var k in this) { proc. @org.waveprotocol.wave.model.util.ReadableIntMap.ProcV::apply(ILjava/lang/Object;) (evaler(k), this[k]); } }-*/; public final native T someValue() /*-{ for (var k in this) { return this[k] } return null; }-*/; /** * Same as ruby/prototype reduce. Same as functional foldl. Apply a function * to an accumulator and key/value in the map. The function returns the new * accumulated value. TODO(danilatos): Implement break and through a la * prototype.js if needed. * * @param initial * @param proc * @return The accumulated value * @param <R> * The accumulating type */ public final native <R> R reduce(R initial, Reduce<T, R> proc) /*-{ var reduction = initial; var evaler = @org.waveprotocol.wave.client.common.util.IntMapJsoView::evaler; for (var k in this) { reduction = proc. @org.waveprotocol.wave.client.common.util.IntMapJsoView.Reduce::apply(Ljava/lang/Object;ILjava/lang/Object;) (reduction, evaler(k), this[k]); } return reduction; }-*/; /** * Convert to a java Map */ public final Map<Integer, T> toMap() { return addToMap(new HashMap<Integer, T>()); } /** * Add all values to a java map. * * @param map * The map to add values to * @return The same map, for convenience. */ public final Map<Integer, T> addToMap(final Map<Integer, T> map) { each(new ProcV<T>() { public void apply(int key, T item) { map.put(key, item); } }); return map; } /** * Add all values to a IntMap. * * @param map * The map to add values to * @return The same map, for convenience. */ public final IntMapJsoView<T> addToMap(final IntMapJsoView<T> map) { each(new ProcV<T>() { public void apply(int key, T item) { map.put(key, item); } }); return map; } }