Bug 576333 - [performance] optimize EclipseContext.localValues
EclipseContext.isSetLocally is a hotspot during open java editor.
Avoid synchronizedMap by using a ConcurrentHashMap for
EclipseContext.localValues.
Since ConcurrentHashMap does not allow null values it is wrapped. Null
values are substituted by a neutral value.
Speeds up EclipseContext.localValues by factor ~ 4.
Change-Id: Ie79da1a9cfc9b4d7b8bce8a4d590cbc5329183ec
Signed-off-by: Joerg Kubitz <jkubitz-eclipse@gmx.de>
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.runtime/+/185955
Tested-by: Platform Bot <platform-bot@eclipse.org>
diff --git a/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/ConcurrentNeutralValueMap.java b/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/ConcurrentNeutralValueMap.java
new file mode 100644
index 0000000..f588fac
--- /dev/null
+++ b/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/ConcurrentNeutralValueMap.java
@@ -0,0 +1,221 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Joerg Kubitz.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Joerg Kubitz - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.e4.core.internal.contexts;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
+
+/**
+ * A wrapper around ConcurrentHashMap which allows null values.
+ *
+ * For null values a neutral Element is internally used.
+ *
+ * Faster then a synchronized Map.
+ */
+public class ConcurrentNeutralValueMap<K, V> {// implements subset of Map<K, V>
+ final ConcurrentHashMap<K, V> delegate = new ConcurrentHashMap<>();
+ /** a value that is used for null elements **/
+ final private V neutralValue;
+
+ /**
+ * @param neutralValue a Element that does not equal any other Value
+ * @see #neutralObject()
+ */
+ public ConcurrentNeutralValueMap(V neutralValue) {
+ this.neutralValue = neutralValue;
+ }
+
+ private V wrapValue(V value) {
+ return value == null ? neutralValue : value;
+ }
+
+ private V unwrapValue(V v) {
+ return v == neutralValue ? null : v;
+ }
+
+ public V get(Object key) {
+ return unwrapValue(delegate.get(key));
+ }
+
+ private static final class NullValue {
+ @Override
+ public String toString() {
+ return "null"; //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return 0x55555555;
+ }
+ }
+
+ /** returns a neutral Element that does not equals any other **/
+ public static Object neutralObject() {
+ return new NullValue();
+ }
+
+ /*
+ * Method overloads from java.lang.Object:
+ */
+
+ @Override
+ public int hashCode() {
+ return delegate.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return delegate.equals(obj);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ /*
+ * Method overloads from java.util.Map:
+ */
+
+ /**
+ * Like {@link java.util.Map#put} but we do not return a value.
+ *
+ * @see java.util.Map#put(Object, Object)
+ * @see ConcurrentHashMap#put(Object, Object)
+ * @see #putAndGetOld(Object, Object)
+ **/
+ public void put(K key, V value) {
+ delegate.put(key, wrapValue(value));
+ }
+
+ /**
+ * Like {@link java.util.Map#put} but we do not return a wrapped value.
+ *
+ * @see java.util.Map#put(Object, Object)
+ * @see Value
+ **/
+ public Value<V> putAndGetOld(K key, V value) {
+ return new Wrapped(delegate.put(key, wrapValue(value)));
+ }
+
+ /**
+ * @see java.util.Map#remove(Object)
+ * @see ConcurrentHashMap#remove(Object)
+ **/
+ public V remove(Object key) {
+ return unwrapValue(delegate.remove(key));
+ }
+
+ /**
+ * @see java.util.Map#size()
+ **/
+ public int size() {
+ return delegate.size();
+ }
+
+ /**
+ * @see java.util.Map#isEmpty()
+ **/
+ public boolean isEmpty() {
+ return delegate.isEmpty();
+ }
+
+ /**
+ * @see java.util.Map#clear()
+ **/
+ public void clear() {
+ delegate.clear();
+ }
+
+ /**
+ * Use is discouraged as the outcome might change concurrently
+ *
+ * @see java.util.Map#containsKey(Object)
+ * @see ConcurrentHashMap#containsKey(Object)
+ **/
+ public boolean containsKey(Object key) {
+ return delegate.containsKey(key);
+ }
+
+ /*
+ * Some helpful methods from ConcurrentHashMap which do not return V:
+ */
+
+ /** @see ConcurrentHashMap#forEach(BiConsumer) **/
+ public void forEach(BiConsumer<? super K, ? super V> action) {
+ delegate.forEach( //
+ (k, v) -> action.accept(k, unwrapValue(v) //
+ ));
+ }
+
+ /*
+ * Some methods from ConcurrentHashMap which return V. returning null is
+ * discouraged because it is not clear if that was a value or not:
+ */
+
+ /**
+ * Like {@link ConcurrentHashMap#putIfAbsent(Object, Object)} but we do not
+ * return anything.
+ *
+ * @see ConcurrentHashMap#putIfAbsent(Object, Object)
+ **/
+ public void putIfAbsent(K key, V value) {
+ delegate.putIfAbsent(key, wrapValue(value));
+ }
+
+ public interface Value<V> {
+ /**
+ * @return the result of map.contains(key)
+ */
+ boolean isPresent();
+
+ /**
+ * @return the result of map.get(key)
+ */
+ V unwraped();
+ }
+
+ private final class Wrapped implements Value<V> {
+ V wrappedValue;
+
+ private Wrapped(V wrappedValue) {
+ this.wrappedValue = wrappedValue;
+ }
+
+ @Override
+ public boolean isPresent() {
+ return wrappedValue != null;
+ }
+
+ @Override
+ public V unwraped() {
+ return unwrapValue(wrappedValue);
+ }
+ }
+
+ /**
+ * @return a value that is wrapped into a Value Object.
+ * @see Value#isPresent()
+ * @see Value#unwraped()
+ */
+ public Value<V> getValue(K key) {
+ return new Wrapped(delegate.get(key));
+ }
+
+}
diff --git a/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/EclipseContext.java b/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/EclipseContext.java
index 5a0f644..6b92681 100644
--- a/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/EclipseContext.java
+++ b/bundles/org.eclipse.e4.core.contexts/src/org/eclipse/e4/core/internal/contexts/EclipseContext.java
@@ -33,6 +33,7 @@
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.contexts.RunAndTrack;
import org.eclipse.e4.core.di.IInjector;
+import org.eclipse.e4.core.internal.contexts.ConcurrentNeutralValueMap.Value;
import org.eclipse.e4.core.internal.contexts.osgi.ContextDebugHelper;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
@@ -91,7 +92,8 @@
private WeakGroupedListenerList weakListeners = new WeakGroupedListenerList();
private Map<String, ValueComputation> localValueComputations = Collections.synchronizedMap(new HashMap<>());
- final protected Map<String, Object> localValues = Collections.synchronizedMap(new HashMap<>());
+ final protected ConcurrentNeutralValueMap<String, Object> localValues = // null values allowed
+ new ConcurrentNeutralValueMap<>(ConcurrentNeutralValueMap.neutralObject());
private Set<String> modifiable;
@@ -256,8 +258,9 @@
Object result = null;
// 1. try for local value
- if (localValues.containsKey(name)) {
- result = localValues.get(name);
+ Value<Object> value = localValues.getValue(name);
+ if (value.isPresent()) {
+ result = value.unwraped();
if (result == null)
return null;
} else
@@ -320,9 +323,10 @@
}
protected boolean isLocalEquals(String name, Object newValue) {
- if (!localValues.containsKey(name))
+ Value<Object> value = localValues.getValue(name);
+ if (!value.isPresent())
return false;
- return (localValues.get(name) == newValue);
+ return (value.unwraped() == newValue);
}
private boolean isSetLocally(String name) {
@@ -371,8 +375,9 @@
setParent((IEclipseContext) value);
return;
}
- boolean containsKey = localValues.containsKey(name);
- Object oldValue = localValues.put(name, value);
+ Value<Object> old = localValues.putAndGetOld(name, value);
+ boolean containsKey = old.isPresent();
+ Object oldValue = old.unwraped();
if (!containsKey || oldValue != value) {
Set<Scheduled> scheduled = new LinkedHashSet<>();
invalidate(name, ContextChangeEvent.ADDED, oldValue, value, scheduled);
@@ -407,7 +412,7 @@
String tmp = "Variable " + name + " is not modifiable in the context " + this; //$NON-NLS-1$ //$NON-NLS-2$
throw new IllegalArgumentException(tmp);
}
- Object oldValue = localValues.put(name, value);
+ Object oldValue = localValues.putAndGetOld(name, value).unwraped();
if (oldValue != value)
invalidate(name, ContextChangeEvent.ADDED, oldValue, value, scheduled);
return true;
@@ -470,9 +475,7 @@
if (modifiable == null)
modifiable = new HashSet<>(3);
modifiable.add(name);
- if (localValues.containsKey(name))
- return;
- localValues.put(name, null);
+ localValues.putIfAbsent(name, null);
}
private boolean checkModifiable(String name) {
@@ -709,23 +712,22 @@
// This method is for debug only, do not use externally
public Map<String, Object> localData() {
Map<String, Object> result = new HashMap<>(localValues.size());
- for (Map.Entry<String, Object> entry : localValues.entrySet()) {
- if (entry.getValue() instanceof IContextFunction) {
- continue;
+ localValues.forEach((k, v) -> {
+ if (!(v instanceof IContextFunction)) {
+ result.put(k, v);
}
- result.put(entry.getKey(), entry.getValue());
- }
+ });
return result;
}
// This method is for debug only, do not use externally
public Map<String, Object> localContextFunction() {
Map<String, Object> result = new HashMap<>(localValues.size());
- for (Map.Entry<String, Object> entry : localValues.entrySet()) {
- if (entry.getValue() instanceof IContextFunction) {
- result.put(entry.getKey(), entry.getValue());
+ localValues.forEach((k, v) -> {
+ if (v instanceof IContextFunction) {
+ result.put(k, v);
}
- }
+ });
return result;
}
diff --git a/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF
index 3aa9bf4..c9f8b95 100644
--- a/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF
+++ b/tests/org.eclipse.e4.core.tests/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
Bundle-ManifestVersion: 2
Bundle-Name: E4 Core Tests
Bundle-SymbolicName: org.eclipse.e4.core.tests
-Bundle-Version: 1.2.200.qualifier
+Bundle-Version: 1.2.300.qualifier
Bundle-Vendor: Eclipse.org
Bundle-Activator: org.eclipse.e4.core.internal.tests.CoreTestsActivator
Require-Bundle: org.eclipse.osgi;bundle-version="3.6.0",
diff --git a/tests/org.eclipse.e4.core.tests/pom.xml b/tests/org.eclipse.e4.core.tests/pom.xml
index 7ec41c2..cc0712d 100644
--- a/tests/org.eclipse.e4.core.tests/pom.xml
+++ b/tests/org.eclipse.e4.core.tests/pom.xml
@@ -19,7 +19,7 @@
</parent>
<groupId>org.eclipse.e4</groupId>
<artifactId>org.eclipse.e4.core.tests</artifactId>
- <version>1.2.200-SNAPSHOT</version>
+ <version>1.2.300-SNAPSHOT</version>
<packaging>eclipse-test-plugin</packaging>
<properties>
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/NeutralValueTest.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/NeutralValueTest.java
new file mode 100644
index 0000000..dccb2c3
--- /dev/null
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/internal/tests/contexts/NeutralValueTest.java
@@ -0,0 +1,144 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Joerg Kubitz.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Joerg Kubitz - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.e4.core.internal.tests.contexts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.e4.core.internal.contexts.ConcurrentNeutralValueMap;
+import org.eclipse.e4.core.internal.contexts.ConcurrentNeutralValueMap.Value;
+import org.junit.Test;
+
+public class NeutralValueTest {
+
+ @Test
+ public void testNeutralObject() {
+ assertFalse(new Object().equals(ConcurrentNeutralValueMap.neutralObject()));
+ assertFalse(ConcurrentNeutralValueMap.neutralObject().equals(new Object()));
+ assertNotEquals(ConcurrentNeutralValueMap.neutralObject(), ConcurrentNeutralValueMap.neutralObject());
+ assertEquals("" + null, ConcurrentNeutralValueMap.neutralObject().toString());
+ }
+
+ @Test
+ public void testConcurrentNeutralValueMap() {
+ ConcurrentNeutralValueMap<String, Double> map = new ConcurrentNeutralValueMap<>(Double.NaN);
+ map.put("nix", null); // modify
+ map.put("2", 2.0); // modify
+ map.put("3", 3.0); // modify
+ map.put("4", 4.0); // modify
+ map.put("garnix", null); // modify
+ assertTrue(map.containsKey("garnix"));
+ map.remove("garnix");
+
+ assertTrue(map.containsKey("nix"));
+ assertTrue(map.containsKey("2"));
+ assertFalse(map.containsKey("1"));
+ assertFalse(map.containsKey("garnix"));
+
+ assertEquals(4, map.size());
+
+ assertFalse(map.isEmpty());
+
+ assertEquals(null, map.get("nix"));
+ assertEquals(null, map.get("1"));
+ assertEquals(Double.valueOf(2.0), map.get("2"));
+ assertEquals(Double.valueOf(3.0), map.get("3"));
+ assertEquals(Double.valueOf(4.0), map.get("4"));
+
+ Set<String> keys = new HashSet<>();
+ Set<Double> values = new HashSet<>();
+ map.forEach((k, v) -> keys.add(k));
+ map.forEach((k, v) -> values.add(v));
+ assertEquals(Set.of("nix", "2", "3", "4"), keys);
+ assertTrue(values.contains(null));
+ assertTrue(values.contains(2.0));
+
+ assertTrue(map.getValue("nix").isPresent());
+ assertFalse(map.getValue("1").isPresent());
+ assertTrue(map.getValue("2").isPresent());
+ assertTrue(map.getValue("3").isPresent());
+ assertTrue(map.getValue("4").isPresent());
+
+ {
+ Value<Double> v = map.getValue("nix");
+ assertTrue(v.isPresent());
+ assertEquals(null, v.unwraped());
+ }
+ {
+ Value<Double> v = map.getValue("1");
+ assertFalse(v.isPresent());
+ assertEquals(null, v.unwraped());
+ }
+ {
+ Value<Double> v = map.getValue("2");
+ assertTrue(v.isPresent());
+ assertEquals(Double.valueOf(2.0), v.unwraped());
+ }
+
+ {
+ Value<Double> v = map.putAndGetOld("5", 5555.0); // modify
+ assertFalse(v.isPresent());
+ assertEquals(null, v.unwraped());
+ assertEquals(Double.valueOf(5555.0), map.get("5"));
+ }
+ {
+ Value<Double> v = map.putAndGetOld("5", 5.0); // modify
+ assertTrue(v.isPresent());
+ assertEquals(Double.valueOf(5555.0), v.unwraped());
+ assertEquals(Double.valueOf(5.0), map.get("5"));
+ }
+ map.putIfAbsent("5", 5555.0); // modify
+ assertEquals(Double.valueOf(5.0), map.get("5"));
+ map.remove("5"); // modify
+ assertFalse(map.containsKey("5"));
+
+ {
+ Value<Double> v = map.putAndGetOld("five", null); // modify
+ assertFalse(v.isPresent());
+ assertEquals(null, v.unwraped());
+ assertEquals(null, map.get("five"));
+ }
+ {
+ Value<Double> v = map.putAndGetOld("five", 5.0); // modify
+ assertTrue(v.isPresent());
+ assertEquals(null, v.unwraped());
+ assertEquals(Double.valueOf(5.0), map.get("five"));
+ }
+ {
+ map.putIfAbsent("five", null); // modify
+ Value<Double> v = map.getValue("five");
+ assertTrue(v.isPresent());
+ assertEquals(Double.valueOf(5.0), v.unwraped());
+ }
+ map.remove("five"); // modify
+ {
+ map.putIfAbsent("five", null); // modify
+ Value<Double> v = map.getValue("five");
+ assertTrue(v.isPresent());
+ assertEquals(null, v.unwraped());
+ }
+ map.remove("five"); // modify
+ assertFalse(map.containsKey("five"));
+
+ map.clear(); // modify
+ assertEquals(0, map.size());
+ assertTrue(map.isEmpty());
+ }
+
+}
diff --git a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java
index 1fe12e8..69a8f39 100644
--- a/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java
+++ b/tests/org.eclipse.e4.core.tests/src/org/eclipse/e4/core/tests/CoreTestSuite.java
@@ -22,6 +22,7 @@
import org.eclipse.e4.core.internal.tests.contexts.ContextDynamicTest;
import org.eclipse.e4.core.internal.tests.contexts.DependenciesLeakTest;
import org.eclipse.e4.core.internal.tests.contexts.EclipseContextTest;
+import org.eclipse.e4.core.internal.tests.contexts.NeutralValueTest;
import org.eclipse.e4.core.internal.tests.contexts.ReparentingTest;
import org.eclipse.e4.core.internal.tests.contexts.RunAndTrackTest;
import org.eclipse.e4.core.internal.tests.contexts.inject.ActivationInjectionTest;
@@ -102,6 +103,7 @@
DisposeClassLinkTest.class,
InjectStaticContextTest.class,
ActivationTest.class,
+ NeutralValueTest.class,
// Contexts injection
AnnotationsInjectionTest.class,