Skip to content

Commit

Permalink
Use array instead of HashMap for StateNode features
Browse files Browse the repository at this point in the history
There is only a limited set of different state node configurations. For
each configuration, we cache a mapping from feature type to the index in
an array containing the actual feature instances. This representation is
much more memory efficient than the previously used generic HashMap.

Reduces BasicElementView memory use from 429706 to 278950 bytes and the
memory use in BeverageBuddy with an edit dialog open from 262257 to
183211 bytes.
  • Loading branch information
Legioth committed Aug 31, 2018
1 parent 73704c2 commit 1917f40
Showing 1 changed file with 103 additions and 55 deletions.
158 changes: 103 additions & 55 deletions flow-server/src/main/java/com/vaadin/flow/internal/StateNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.vaadin.flow.component.UI;
Expand All @@ -55,14 +56,76 @@
* @since 1.0
*/
public class StateNode implements Serializable {
private static class FeatureSetKey implements Serializable {
private final Set<Class<? extends NodeFeature>> reportedFeatures;
private final Set<Class<? extends NodeFeature>> nonReportableFeatures;

public FeatureSetKey(
Collection<Class<? extends NodeFeature>> reportableFeatureTypes,
Class<? extends NodeFeature>[] nonReportableFeatureTypes) {
reportedFeatures = new HashSet<>(reportableFeatureTypes);
nonReportableFeatures = Stream.of(nonReportableFeatureTypes)
.collect(Collectors.toSet());

assert !nonReportableFeatures.removeAll(
reportedFeatures) : "No reportable feature should also be non-reportable";
assert !reportedFeatures.removeAll(
nonReportableFeatures) : "No non-reportable feature should also be reportable";
}

@Override
public int hashCode() {
return Objects.hash(reportedFeatures, nonReportableFeatures);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof FeatureSetKey) {
FeatureSetKey that = (FeatureSetKey) obj;
return that.nonReportableFeatures.equals(nonReportableFeatures)
&& that.reportedFeatures.equals(reportedFeatures);
} else {
return false;
}
}

public Stream<Class<? extends NodeFeature>> getAllFeatures() {
return Stream.concat(nonReportableFeatures.stream(),
reportedFeatures.stream());
}
}

private static class FeatureSet implements Serializable {
private final Set<Class<? extends NodeFeature>> reportedFeatures;

/**
* Maps from a node feature type to its index in the {@link #features}
* array. This instance is cached per unique set of used node feature
* types in {@link #featureSetCache}.
*/
private final Map<Class<? extends NodeFeature>, Integer> mappings = new HashMap<>();

public FeatureSet(FeatureSetKey featureSetKey) {
reportedFeatures = featureSetKey.reportedFeatures;

featureSetKey.getAllFeatures().forEach(
key -> mappings.put(key, Integer.valueOf(mappings.size())));
}
}

/**
* Cache of immutable node feature type set instances.
*/
private static final Map<Set<Class<? extends NodeFeature>>, Set<Class<? extends NodeFeature>>> nodeFeatureSetCache = new ConcurrentHashMap<>();
private static final Map<FeatureSetKey, FeatureSet> featureSetCache = new ConcurrentHashMap<>();

private final Map<Class<? extends NodeFeature>, NodeFeature> features = new HashMap<>();
private final FeatureSet featureSet;

private final Set<Class<? extends NodeFeature>> reportedFeatures;
/**
* Node feature instances for this node.
*/
private final NodeFeature[] features;

private Map<Class<? extends NodeFeature>, Serializable> changes;

Expand Down Expand Up @@ -106,7 +169,7 @@ public StateNode(Class<? extends NodeFeature>... featureTypes) {
*/
@SuppressWarnings("unchecked")
public StateNode(StateNode node) {
this(new ArrayList<>(node.reportedFeatures),
this(new ArrayList<>(node.featureSet.reportedFeatures),
getNonRepeatebleFeatures(node));
}

Expand All @@ -123,26 +186,14 @@ public StateNode(StateNode node) {
@SafeVarargs
public StateNode(List<Class<? extends NodeFeature>> reportableFeatureTypes,
Class<? extends NodeFeature>... nonReportableFeatureTypes) {
reportedFeatures = getCachedFeatureSet(reportableFeatureTypes);
Stream.concat(reportableFeatureTypes.stream(),
Stream.of(nonReportableFeatureTypes)).forEach(this::addFeature);
}

private static Set<Class<? extends NodeFeature>> getCachedFeatureSet(
Collection<Class<? extends NodeFeature>> reportableFeatureTypes) {
Set<Class<? extends NodeFeature>> keyAndValue = Collections
.unmodifiableSet(new HashSet<>(reportableFeatureTypes));
featureSet = featureSetCache
.computeIfAbsent(new FeatureSetKey(reportableFeatureTypes,
nonReportableFeatureTypes), FeatureSet::new);

Set<Class<? extends NodeFeature>> currentValue = nodeFeatureSetCache
.putIfAbsent(keyAndValue, keyAndValue);

if (currentValue == null) {
// If we put the value there
return keyAndValue;
} else {
// If there was already a value there
return currentValue;
}
features = new NodeFeature[featureSet.mappings.size()];
featureSet.mappings.forEach((featureType,
index) -> features[index.intValue()] = NodeFeatureRegistry
.create(featureType, this));
}

/**
Expand Down Expand Up @@ -252,7 +303,13 @@ private void onDetach() {
}

private void forEachChild(Consumer<StateNode> action) {
getFeatures().values().forEach(n -> n.forEachChild(action));
forEachFeature(n -> n.forEachChild(action));
}

private void forEachFeature(Consumer<NodeFeature> action) {
for (NodeFeature feature : features) {
action.accept(feature);
}
}

/**
Expand Down Expand Up @@ -281,12 +338,15 @@ protected void setTree(StateTree tree) {
public <T extends NodeFeature> T getFeature(Class<T> featureType) {
assert featureType != null;

NodeFeature feature = getFeatures().get(featureType);
if (feature == null) {
Integer featureIndex = featureSet.mappings.get(featureType);
if (featureIndex == null) {
throw new IllegalStateException(
"Node does not have the feature " + featureType);
}

NodeFeature feature = features[featureIndex.intValue()];
assert feature != null;

return featureType.cast(feature);
}

Expand All @@ -301,7 +361,7 @@ public <T extends NodeFeature> T getFeature(Class<T> featureType) {
public boolean hasFeature(Class<? extends NodeFeature> featureType) {
assert featureType != null;

return getFeatures().containsKey(featureType);
return featureSet.mappings.containsKey(featureType);
}

/**
Expand Down Expand Up @@ -366,8 +426,7 @@ public void collectChanges(Consumer<NodeChange> collector) {

// Make all changes show up as if the node was recently attached
clearChanges();
getFeatures().values()
.forEach(NodeFeature::generateChangesFromEmpty);
forEachFeature(NodeFeature::generateChangesFromEmpty);
} else {
collector.accept(new NodeDetachChange(this));
}
Expand All @@ -381,15 +440,15 @@ public void collectChanges(Consumer<NodeChange> collector) {
if (isInitialChanges) {
// send only required (reported) features updates
Stream<NodeFeature> initialFeatures = Stream
.concat(getFeatures().entrySet().stream().filter(
entry -> isReportedFeature(entry.getKey()))
.map(Entry::getValue), getDisalowFeatures());
.concat(featureSet.mappings.keySet().stream()
.filter(this::isReportedFeature)
.map(this::getFeature), getDisalowFeatures());
doCollectChanges(collector, initialFeatures);
} else {
doCollectChanges(collector, getDisalowFeatures());
}
} else {
doCollectChanges(collector, getFeatures().values().stream());
doCollectChanges(collector, Stream.of(features));
}
}

Expand Down Expand Up @@ -575,7 +634,7 @@ private void fireAttachListeners(boolean initialAttach) {
copy.forEach(Command::execute);
}

getFeatures().values().forEach(f -> f.onAttach(initialAttach));
forEachFeature(f -> f.onAttach(initialAttach));
}

private void fireDetachListeners() {
Expand All @@ -585,7 +644,7 @@ private void fireDetachListeners() {
copy.forEach(Command::execute);
}

getFeatures().values().forEach(NodeFeature::onDetach);
forEachFeature(NodeFeature::onDetach);
}

/**
Expand Down Expand Up @@ -644,7 +703,7 @@ public void execute() {
* @return whether the feature required by the client side
*/
public boolean isReportedFeature(Class<? extends NodeFeature> featureType) {
return reportedFeatures.contains(featureType);
return featureSet.reportedFeatures.contains(featureType);
}

/**
Expand Down Expand Up @@ -688,16 +747,15 @@ public boolean isInactive() {
}

private Stream<NodeFeature> getDisalowFeatures() {
return getFeatures().values().stream()
.filter(feature -> !feature.allowsChanges());
return Stream.of(features).filter(feature -> !feature.allowsChanges());
}

private void setInactive(boolean inactive) {
if (isInactiveSelf != inactive) {
isInactiveSelf = inactive;

visitNodeTree(child -> {
if (!this.equals(child) && !child.isInactiveSelf) {
if (!equals(child) && !child.isInactiveSelf) {
/*
* We are here if: the child node itself is not inactive but
* it has some ascendant which is inactive.
Expand Down Expand Up @@ -734,25 +792,15 @@ private UI getUI() {
return ((StateTree) getOwner()).getUI();
}

private void addFeature(Class<? extends NodeFeature> featureType) {
if (!features.containsKey(featureType)) {
NodeFeature feature = NodeFeatureRegistry.create(featureType, this);
features.put(featureType, feature);
}
}

private Map<Class<? extends NodeFeature>, NodeFeature> getFeatures() {
return features;
}

@SuppressWarnings("rawtypes")
private static Class[] getNonRepeatebleFeatures(StateNode node) {
if (node.reportedFeatures.isEmpty()) {
Set<Class<? extends NodeFeature>> set = node.features.keySet();
if (node.featureSet.reportedFeatures.isEmpty()) {
Set<Class<? extends NodeFeature>> set = node.featureSet.mappings
.keySet();
return set.toArray(new Class[set.size()]);
}
return node.features.keySet().stream()
.filter(clazz -> !node.reportedFeatures.contains(clazz))
return node.featureSet.mappings.keySet().stream().filter(
clazz -> !node.featureSet.reportedFeatures.contains(clazz))
.toArray(Class[]::new);
}

Expand Down

0 comments on commit 1917f40

Please sign in to comment.