]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/ParentNode.java
Performance improvements for ParentNode in scene graph
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / ParentNode.java
index 36efc8dc01252e0a79b870e1b667e76a9127eebd..e9e0b4be3e1f5beb2107b89733a1ac72ba04bc2d 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
- * in Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
 package org.simantics.scenegraph;
 
-import gnu.trove.map.hash.THashMap;\r
-\r
-import java.beans.PropertyChangeEvent;\r
-import java.beans.PropertyChangeListener;\r
-import java.util.Collection;\r
-import java.util.Collections;\r
-import java.util.Map;\r
-
-/**\r
- * Base class of all scene graph nodes which can have a set of sub-nodes\r
- * (children). This class only provides support for unordered children.\r
- * \r
- * @param <T>\r
- */\r
-public abstract class ParentNode<T extends INode> extends Node {\r
-\r
-    private static final long                  serialVersionUID     = 8519410262849626534L;\r
-\r
-    public static final String                 EXISTING             = "#EXISTING#";\r
-    public static final String                 UNLINK               = "#UNLINK#";\r
-    public static final String                 NULL                 = "#NULL#";\r
-\r
-    protected static final String[]            EMPTY_STRING_ARRAY   = {};\r
-\r
-    @SuppressWarnings("rawtypes")\r
-    private static final Map                   DISPOSED_CHILDREN    = Collections.emptyMap();\r
-\r
-    /**\r
-     * A value used for {@link #rootNodeCache} to indicate the node has been\r
-     * disposed and the root node cache is to be considered indefinitely\r
-     * invalid.\r
-     */\r
-    protected static final ParentNode<?> DISPOSED = new ParentNode<INode>() {\r
-        private static final long serialVersionUID = 6155494069158034123L;\r
-        @Override\r
-        public void asyncRemoveNode(INode node) {\r
-            throw new Error();\r
-        }\r
-    };\r
-\r
-    protected transient Map<String, T>         children             = createChildMap();\r
-
-    /**\r
-     * A cached value for the root node of this parent node. This makes it\r
-     * possible to optimize {@link #getRootNode()} so that not all invocations\r
-     * go through the whole node hierarchy to find the root. This helps in\r
-     * making {@link ILookupService} located in the root node perform better and\r
-     * therefore be more useful.\r
-     * \r
-     * @see #DISPOSED\r
-     */\r
-    protected transient volatile ParentNode<?> rootNodeCache;\r
-\r
-    protected Map<String, T> createChildMap() {\r
-       return new THashMap<String, T>(1);\r
-    }\r
-    \r
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import gnu.trove.map.hash.TLongObjectHashMap;
+
+/**
+ * Base class of all scene graph nodes which can have a set of sub-nodes
+ * (children). This class only provides support for unordered children.
+ * 
+ * @param <T>
+ */
+public abstract class ParentNode<T extends INode> extends Node {
+
+    private static final long                  serialVersionUID     = 8519410262849626534L;
+
+    public static final String                 EXISTING             = "#EXISTING#";
+    public static final String                 UNLINK               = "#UNLINK#";
+    public static final String                 NULL                 = "#NULL#";
+
+    /**
+     * A value used for {@link #rootNodeCache} to indicate the node has been
+     * disposed and the root node cache is to be considered indefinitely
+     * invalid.
+     */
+    protected static final ParentNode<?> DISPOSED = new ParentNode<INode>() {
+        private static final long serialVersionUID = 6155494069158034123L;
+        @Override
+        public void asyncRemoveNode(INode node) {
+            throw new Error();
+        }
+    };
+
+    protected transient Map<String, INode>       children      = createChildMap();
+    private transient TLongObjectHashMap<String> childrenIdMap = new TLongObjectHashMap<>();
+
+    /**
+     * A cached value for the root node of this parent node. This makes it
+     * possible to optimize {@link #getRootNode()} so that not all invocations
+     * go through the whole node hierarchy to find the root. This helps in
+     * making {@link ILookupService} located in the root node perform better and
+     * therefore be more useful.
+     * 
+     * @see #DISPOSED
+     */
+    protected transient volatile ParentNode<?> rootNodeCache;
+
+    protected Map<String, INode> createChildMap() {
+        return createChildMap(1);
+    }
+
+    protected Map<String, INode> createChildMap(int initialCapacity) {
+        // With JDK 1.8 HashMap is faster than Trove
+        return new HashMap<>(initialCapacity);
+    }
+
     public final <TC> TC addNode(Class<TC> a) {
         return addNode(java.util.UUID.randomUUID().toString(), a);
     }
-\r
-    @SuppressWarnings("unchecked")\r
-    public <TC extends INode> TC addNode(String id, TC child) {\r
-\r
-        child.setParent(this);\r
-        children.put(id, (T)child);\r
-\r
-        child.init();\r
-        childrenChanged();\r
-        return (TC)child;\r
-       \r
-    }\r
-\r
-    @SuppressWarnings("unchecked")\r
-    public <TC extends INode> TC attachNode(String id, TC child) {\r
-\r
-        child.setParent(this);\r
-        children.put(id, (T)child);\r
-\r
-        child.attach();\r
-        childrenChanged();\r
-        return (TC)child;\r
-       \r
-    }\r
-\r
-    @SuppressWarnings("unchecked")\r
-    public <TC extends INode> TC detachNode(String id) {\r
-        T child = children.remove(id);\r
-        if (child == null)\r
-            return null;\r
-        child.setParent(null);\r
-        childrenChanged();\r
-        return (TC) child;\r
-    }\r
-\r
+
+    public <TC extends INode> TC addNode(String id, TC child) {
+        return addNodeInternal(id, child, true, true);
+    }
+
+    private <TC extends INode> TC addNodeInternal(String id, TC child, boolean init, boolean addToChildren) {
+        child.setParent(this);
+        if (addToChildren)
+            children.put(id, child);
+
+        childrenIdMap.put(child.getId(), id);
+
+        if (init)
+            child.init();
+        childrenChanged();
+        return (TC)child;
+    }
+
+    public <TC extends INode> TC attachNode(String id, TC child) {
+        return addNodeInternal(id, child, false, true);
+    }
+
     @SuppressWarnings("unchecked")
+    public <TC extends INode> TC detachNode(String id) {
+        INode child = children.remove(id);
+        if (child == null)
+            return null;
+        childrenIdMap.remove(child.getId());
+        child.setParent(null);
+        childrenChanged();
+        return (TC) child;
+    }
+
     public <TC> TC addNode(String id, Class<TC> a) {
+        return addNodeInternal0(id, a, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    private <TC> TC addNodeInternal0(String id, Class<TC> a, boolean addToChildren) {
         // a must be extended from Node
         if(!Node.class.isAssignableFrom(a)) {
             throw new IllegalArgumentException(a + " is not extended from org.simantics.scenegraph.Node");
         }
-        INode child = null;\r
+        INode child = null;
         try {
             child = (Node) a.newInstance();
         } catch (InstantiationException e) {
@@ -120,122 +123,80 @@ public abstract class ParentNode<T extends INode> extends Node {
         } catch (IllegalAccessException e) {
             throw new NodeException("Node " + Node.getSimpleClassName(a) + " instantiation failed, see exception for details.", e);
         }
+        return (TC) addNodeInternal(id, child, true, addToChildren);
+    }
 
-        child.setParent(this);
-        children.put(id, (T)child);
-
-        child.init();
-        childrenChanged();
-        return (TC)child;
+    @SuppressWarnings("unchecked")
+    public final <TC extends INode> TC getOrAttachNode(String id, TC a) {
+        return (TC) children.computeIfAbsent(id, key -> {
+            return (T) addNodeInternal(id, a, false, false);
+        });
     }
-\r
-    @SuppressWarnings("unchecked")\r
-    public final <TC extends INode> TC getOrAttachNode(String id, TC a) {\r
-        synchronized (children) {\r
-            if (children.containsKey(id))\r
-                return (TC) children.get(id);\r
-        }\r
-        return attachNode(id, a);\r
-    }\r
     
     @SuppressWarnings("unchecked")
     public final <TC> TC getOrCreateNode(String id, Class<TC> a) {
-        synchronized (children) {\r
-            if (children.containsKey(id))\r
-                return (TC) children.get(id);\r
-        }\r
-        return addNode(id, a);
+        return (TC) children.computeIfAbsent(id, key -> {
+            return (T) addNodeInternal0(id, a, false);
+        });
     }
 
-    public final void removeNode(String id) {\r
-        INode child = null;\r
-        synchronized (children) {
-            child = children.remove(id);\r
-        }
-        if (child != null) {\r
-            if (child instanceof ParentNode<?>) {\r
-                ((ParentNode<?>) child).removeNodes();\r
-            }\r
-            child.cleanup();
-            child.setParent(null);\r
-            childrenChanged();
-            if (propertyChangeListener != null) {\r
-                propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
-            }
-        }
+    public final void removeNode(String id) {
+        INode child = children.remove(id);
+        if (child != null)
+            removeNodeInternal(child, true);
     }
 
     public final void removeNode(INode child) {
-        synchronized (children) {
-            String id = null;
-            // FIXME: damn slow, needs more data structure to be supported well
-            // One option would be to store the id<->node mappings in a BidiMap.
-            for (String tmp : children.keySet()) {
-                if (children.get(tmp).equals(child)) {
-                    id = tmp;
-                    break;
-                }
-            }
-            if(id == null) return;
-            children.remove(id);\r
-            childrenChanged();
-        }\r
-        if (child instanceof ParentNode<?>) {\r
-            ((ParentNode<?>) child).removeNodes();\r
-        }
-        child.cleanup();
-        child.setParent(null);\r
+        String key = childrenIdMap.get(child.getId());
+        removeNode(key);
+    }
 
-        if (propertyChangeListener != null) {
-            propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
-        }
-    }\r
-\r
-    /**\r
-     * This method removes and disposes all children of the node (and their\r
-     * children).\r
-     */\r
+    /**
+     * This method removes and disposes all children of the node (and their
+     * children).
+     */
     public final void removeNodes() {
-        synchronized (children) {\r
-            boolean changed = false;
-            String[] keys = children.keySet().toArray(EMPTY_STRING_ARRAY);
-            for (String key : keys) {\r
-                INode child = children.remove(key);\r
-                if (child != null) {\r
-                    changed = true;\r
-                    if (child instanceof ParentNode<?>) {\r
-                        ((ParentNode<?>) child).removeNodes();\r
-                    }
-                    child.cleanup();
-                    child.setParent(null);\r
-                    if (propertyChangeListener != null) {\r
-                        propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
-                    }
-                }
+        boolean changed = children.size() > 0;
+        children.forEach((id, child) -> removeNodeInternal(child, false));
+        children.clear();
+        childrenIdMap.clear();
+
+        if (changed)
+            childrenChanged();
+    }
+
+    private void removeNodeInternal(INode child, boolean triggerChildrenChanged) {
+        if (child != null) {
+            if (child instanceof ParentNode<?>) {
+                ((ParentNode<?>) child).removeNodes();
             }
-            if (changed)
+            child.cleanup();
+            child.setParent(null);
+            if (triggerChildrenChanged)
                 childrenChanged();
+            triggerPropertyChangeEvent(child);
         }
     }
 
-    /**\r
-     * Invoked every time the set of child changes for a {@link ParentNode}.\r
-     * Extending implementations may override to perform their own actions, such\r
-     * as invalidating possible caches.\r
-     */\r
+    /**
+     * Invoked every time the set of child changes for a {@link ParentNode}.
+     * Extending implementations may override to perform their own actions, such
+     * as invalidating possible caches.
+     */
     protected void childrenChanged() {
     }
 
     public abstract void asyncRemoveNode(INode node);
 
+    @SuppressWarnings("unchecked")
     public T getNode(String id) {
-        return children.get(id);
+        return (T) children.get(id);
     }
 
-    /**\r
-     * @return the IDs of the child nodes as a collection. Returns internal\r
-     *         state which must not be directly modified by client code!\r
-     */\r
+    /**
+     * @return the IDs of the child nodes as a collection. Returns internal
+     *         state which must not be directly modified by client code!
+     */
     public Collection<String> getNodeIds() {
         return children.keySet();
     }
@@ -243,12 +204,13 @@ public abstract class ParentNode<T extends INode> extends Node {
     /**
      * @return the collection of this node's children in an unspecified order
      */
+    @SuppressWarnings("unchecked")
     public Collection<T> getNodes() {
-        return children.values();\r
+        return (Collection<T>) children.values();
     }
 
     public int getNodeCount() {
-        return children.size();\r
+        return children.size();
     }
 
     /**
@@ -258,137 +220,118 @@ public abstract class ParentNode<T extends INode> extends Node {
      */
     public void setPropertyChangeListener(PropertyChangeListener propertyChangeListener) {
         this.propertyChangeListener = propertyChangeListener;
-        synchronized(children) {
-            for(T t : children.values()) {
-                INode child = t;
-                if(child instanceof ParentNode<?>) {
-                    ((ParentNode<?>)child).setPropertyChangeListener(propertyChangeListener);
-                } else {
-                    ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME
-                }
+        children.forEach((id, child) -> {
+            if(child instanceof ParentNode<?>) {
+                ((ParentNode<?>)child).setPropertyChangeListener(propertyChangeListener);
+            } else {
+                ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME
             }
-        }
+        });
     }
 
-    @SuppressWarnings("unchecked")\r
     @Override
     public void cleanup() {
-        retractMapping();\r
-        if (children != DISPOSED_CHILDREN) { \r
-            synchronized(children) {
-                for(T child : children.values()) {
-                    ((INode)child).cleanup();\r
-                    ((INode)child).setParent(null);
-                }
-                children.clear();\r
-                children = DISPOSED_CHILDREN;
-                childrenChanged();\r
-            }\r
-            rootNodeCache = DISPOSED;\r
+        retractMapping();
+        if (children != null) { 
+            children.forEach((id, child) -> {
+                child.cleanup();
+                child.setParent(null);
+            });
+            children.clear();
+            childrenIdMap.clear();
+            children = null;
+            childrenIdMap = null;
+            childrenChanged();
+            rootNodeCache = DISPOSED;
         }
-    }\r
-\r
+    }
+
     @Override
-    public void delete() {\r
-        if(parent == null) {\r
-            return;\r
-        }\r
-\r
-        synchronized (children) {\r
-            // 1. Add children under parent\r
-            parent.appendChildren(children);\r
-\r
-            // 2. Clear children\r
-            children.clear();\r
-        }\r
-\r
-        // 3. Remove this node from parent\r
-        parent.unlinkChild(this);\r
-\r
-        // 4. Cleanup, this node is now disposed\r
-        cleanup();\r
-    }\r
-\r
-    /**\r
-     * Helper method for delete()\r
-     * @param children\r
-     */\r
-    @SuppressWarnings("unchecked")\r
-    protected void appendChildren(Map<String, ?> children) {\r
-        synchronized(this.children) {\r
-            for(String id : children.keySet()) {\r
-                INode child = (INode)children.get(id);\r
-                this.children.put(id, (T)child); // Hopefully cast works\r
-                child.setParent(this);\r
-\r
-                // Send notify only if we are on server side (or standalone)\r
-                if (propertyChangeListener != null && location.equals(Location.LOCAL)) {\r
-                    propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", null, EXISTING)); // "children" is a special field name\r
-                }\r
-            }\r
-        }\r
-    }\r
-\r
-    @SuppressWarnings("unchecked")\r
-    protected void appendChild(String id, INode child) {\r
-        children.put(id, (T)child);\r
-        child.setParent(this);\r
-        // Send notify only if we are on server side (or standalone)\r
-        if (propertyChangeListener != null && location.equals(Location.LOCAL)) {\r
-            propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+(child).getId()+"]", null, EXISTING)); // "children" is a special field name\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Same as removeNode, but does not perform cleanup for the child\r
-     * @param child\r
-     */\r
-    protected void unlinkChild(INode child) {\r
-        synchronized(children) {\r
-            String id = null;\r
-            // FIXME: damn slow, needs more data structure to be supported well\r
-            // One option would be to store the id<->node mappings in a BidiMap.\r
-            for(String tmp : children.keySet()) {\r
-                if(children.get(tmp).equals(child)) {\r
-                    id = tmp;\r
-                    break;\r
-                }\r
-            }\r
-            if(id == null) return;\r
-            children.remove(id);\r
-            childrenChanged();\r
-        }\r
-\r
-        if(propertyChangeListener != null && location.equals(Location.LOCAL)) {\r
-            propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), UNLINK)); // "children" is a special field name\r
-        }\r
-    }\r
+    public void delete() {
+        if(parent == null) {
+            return;
+        }
+
+        // 1. Add children under parent
+        parent.appendChildren(children);
+
+        // 2. Clear child maps to prevent cleanup from deleting them in step 4. 
+        children.clear();
+        childrenIdMap.clear();
+
+        // 3. Remove this node from parent
+        parent.unlinkChild(this);
+
+        // 4. Cleanup, this node is now disposed
+        cleanup();
+    }
+
+    /**
+     * Helper method for delete()
+     * @param children
+     */
+    protected void appendChildren(Map<String, INode> children) {
+        children.forEach((key, value) -> {
+            appendChildInternal(key, value);
+        });
+    }
+
+    protected void appendChild(String id, INode child) {
+        appendChildInternal(id, child);
+    }
+    
+    private void appendChildInternal(String id, INode child) {
+        children.put(id, child);
+        childrenIdMap.put(child.getId(), id);
+        child.setParent(this);
+        triggerPropertyChangeEvent(child);
+    }
+
+    /**
+     * Same as removeNode, but does not perform cleanup for the child
+     * @param child
+     */
+    protected void unlinkChild(INode child) {
+        String id = childrenIdMap.remove(child.getId());
+        if(id != null) {
+            children.remove(id);
+            childrenChanged();
+            triggerPropertyChangeEvent(child);
+        }
+    }
+
+    private void triggerPropertyChangeEvent(INode child) {
+        // Send notify only if we are on server side (or standalone)
+        if (propertyChangeListener != null && location.equals(Location.LOCAL)) {
+            propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), UNLINK)); // "children" is a special field name
+        }
+    }
 
     @Override
     public String toString() {
         return super.toString() + " [#child=" + children.size() + "]";
     }
 
-    @Override\r
-    public ParentNode<?> getRootNode() {\r
-        // Note: using double-checked locking idiom with volatile keyword.\r
-        // Works with Java 1.5+\r
-        ParentNode<?> result = rootNodeCache;\r
-        if (result == DISPOSED)\r
-            return null;\r
-\r
-        if (result == null) {\r
-            synchronized (this) {\r
-                result = rootNodeCache;\r
-                if (result == null) {\r
-                    if (parent != null) {\r
-                        result = parent.getRootNode();\r
-                        rootNodeCache = result;\r
-                    }\r
-                }\r
-            }\r
-        }\r
-        return result;\r
-    }\r
-\r
+    @Override
+    public ParentNode<?> getRootNode() {
+        // Note: using double-checked locking idiom with volatile keyword.
+        // Works with Java 1.5+
+        ParentNode<?> result = rootNodeCache;
+        if (result == DISPOSED)
+            return null;
+
+        if (result == null) {
+            synchronized (this) {
+                result = rootNodeCache;
+                if (result == null) {
+                    if (parent != null) {
+                        result = parent.getRootNode();
+                        rootNodeCache = result;
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
 }