Skip to content

Commit

Permalink
Minimize features array size (#4584)
Browse files Browse the repository at this point in the history
Order index mapping of feature types according to how often they are
actually used (based on actual use frequencies in Beverage Buddy and
Bakery). This allows allocating a smaller features array for each state
node to only hold the features that are actually instantiated.

The case with only one used feature is further optimized to directly
store the feature instance instead of using a one-item array.

Reduces BasicElementView memory use from 149909 to 136045 bytes and the
memory use in BeverageBuddy with an edit dialog open from 151733 to
146061 bytes.
  • Loading branch information
Legioth authored and ZheSun88 committed Sep 10, 2018
1 parent 49169f0 commit f0d537d
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 46 deletions.
82 changes: 72 additions & 10 deletions flow-server/src/main/java/com/vaadin/flow/internal/StateNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -118,8 +119,10 @@ private static class FeatureSet implements Serializable {
public FeatureSet(FeatureSetKey featureSetKey) {
reportedFeatures = featureSetKey.reportedFeatures;

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

Expand All @@ -131,9 +134,9 @@ public FeatureSet(FeatureSetKey featureSetKey) {
private final FeatureSet featureSet;

/**
* Node feature instances for this node.
* Node feature instances for this node, or a single item.
*/
private final NodeFeature[] features;
private Serializable features;

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

Expand Down Expand Up @@ -200,7 +203,7 @@ public StateNode(List<Class<? extends NodeFeature>> reportableFeatureTypes,
.computeIfAbsent(new FeatureSetKey(reportableFeatureTypes,
additionalFeatureTypes), FeatureSet::new);

features = new NodeFeature[featureSet.mappings.size()];
features = null;
// Eagerly initialize features that should always be sent
reportableFeatureTypes.forEach(this::getFeature);
}
Expand Down Expand Up @@ -320,7 +323,13 @@ private void forEachFeature(Consumer<NodeFeature> action) {
}

private Stream<NodeFeature> getInitializedFeatures() {
return Stream.of(features).filter(Objects::nonNull);
if (features == null) {
return Stream.empty();
} else if (features instanceof NodeFeature) {
return Stream.of((NodeFeature) features);
} else {
return Stream.of((NodeFeature[]) features).filter(Objects::nonNull);
}
}

/**
Expand Down Expand Up @@ -349,10 +358,46 @@ protected void setTree(StateTree tree) {
public <T extends NodeFeature> T getFeature(Class<T> featureType) {
int featureIndex = getFeatureIndex(featureType);

NodeFeature feature = features[featureIndex];
if (feature == null) {
/*
* To limit memory use, the features array is kept as short as possible
* and the size is increased when needed.
*
* Furthermore, instead of a one-item array, the single item is stored
* as the field value. This further optimizes the case of text nodes and
* template model nodes.
*/
NodeFeature feature;
if (featureIndex == 0 && features instanceof NodeFeature) {
feature = (NodeFeature) features;
} else if (featureIndex == 0 && features == null) {
feature = NodeFeatureRegistry.create(featureType, this);
features[featureIndex] = feature;
features = feature;
} else {
NodeFeature[] featuresArray;
if (features instanceof NodeFeature[]) {
featuresArray = (NodeFeature[]) features;
} else {
assert features == null || features instanceof NodeFeature;

featuresArray = new NodeFeature[featureIndex + 1];
if (features instanceof NodeFeature) {
featuresArray[0] = (NodeFeature) features;
}
features = featuresArray;
}

// Increase size if necessary
if (featureIndex >= featuresArray.length) {
featuresArray = Arrays.copyOf(featuresArray, featureIndex + 1);
features = featuresArray;
}

feature = featuresArray[featureIndex];

if (feature == null) {
feature = NodeFeatureRegistry.create(featureType, this);
featuresArray[featureIndex] = feature;
}
}

return featureType.cast(feature);
Expand Down Expand Up @@ -385,9 +430,26 @@ private <T extends NodeFeature> int getFeatureIndex(Class<T> featureType) {
*/
public <T extends NodeFeature> Optional<T> getFeatureIfInitialized(
Class<T> featureType) {
if (features == null) {
return Optional.empty();
}
int featureIndex = getFeatureIndex(featureType);

return Optional.ofNullable(features[featureIndex])
if (features instanceof NodeFeature) {
if (featureIndex == 0) {
return Optional.of(featureType.cast(features));
} else {
return Optional.empty();
}
}

NodeFeature[] featuresArray = (NodeFeature[]) features;

if (featureIndex >= featuresArray.length) {
return Optional.empty();
}

return Optional.ofNullable(featuresArray[featureIndex])
.map(featureType::cast);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -32,68 +33,114 @@
* @since 1.0
*/
public class NodeFeatureRegistry {
private static int nextNodeFeatureId = 0;
private static int nextNodePriority = 0;

// Non-private for testing purposes
static final Map<Class<? extends NodeFeature>, NodeFeatureData> nodeFeatures = new HashMap<>();
private static final Map<Integer, Class<? extends NodeFeature>> idToFeature = new HashMap<>();

/**
* Comparator for finding the priority order between node feature types.
*/
public static final Comparator<Class<? extends NodeFeature>> PRIORITY_COMPARATOR = Comparator
.comparingInt(feature -> getData(feature).priority);

private static class NodeFeatureData implements Serializable {
private final SerializableFunction<StateNode, ? extends NodeFeature> factory;
private final int id;
private final int priority;

private <T extends NodeFeature> NodeFeatureData(
SerializableFunction<StateNode, T> factory) {
SerializableFunction<StateNode, T> factory, int id) {
this.factory = factory;
id = nextNodeFeatureId++;
this.id = id;
priority = nextNodePriority++;
}
}

static {
registerFeature(ElementData.class, ElementData::new);
registerFeature(ElementPropertyMap.class, ElementPropertyMap::new);
registerFeature(ElementChildrenList.class, ElementChildrenList::new);
registerFeature(ElementAttributeMap.class, ElementAttributeMap::new);
registerFeature(ElementListenerMap.class, ElementListenerMap::new);
registerFeature(PushConfigurationMap.class, PushConfigurationMap::new);
registerFeature(PushConfigurationParametersMap.class,
PushConfigurationParametersMap::new);
registerFeature(TextNodeMap.class, TextNodeMap::new);
registerFeature(PollConfigurationMap.class, PollConfigurationMap::new);
registerFeature(ReconnectDialogConfigurationMap.class,
ReconnectDialogConfigurationMap::new);
registerFeature(LoadingIndicatorConfigurationMap.class,
LoadingIndicatorConfigurationMap::new);
registerFeature(ElementClassList.class, ElementClassList::new);
registerFeature(ElementStylePropertyMap.class,
ElementStylePropertyMap::new);
/* Primary features */
registerFeature(ElementData.class, ElementData::new,
NodeFeatures.ELEMENT_DATA);
registerFeature(TextNodeMap.class, TextNodeMap::new,
NodeFeatures.TEXT_NODE);
registerFeature(ModelList.class, ModelList::new,
NodeFeatures.TEMPLATE_MODELLIST);
registerFeature(BasicTypeValue.class, BasicTypeValue::new,
NodeFeatures.BASIC_TYPE_VALUE);

/* Common element features */
registerFeature(ElementChildrenList.class, ElementChildrenList::new,
NodeFeatures.ELEMENT_CHILDREN);
registerFeature(ElementPropertyMap.class, ElementPropertyMap::new,
NodeFeatures.ELEMENT_PROPERTIES);

/* Component mapped features */
registerFeature(ComponentMapping.class, ComponentMapping::new,
NodeFeatures.COMPONENT_MAPPING);
registerFeature(ClientCallableHandlers.class,
ClientCallableHandlers::new,
NodeFeatures.CLIENT_DELEGATE_HANDLERS);

/* Supplementary element stuff */
registerFeature(ElementClassList.class, ElementClassList::new,
NodeFeatures.CLASS_LIST);
registerFeature(ElementAttributeMap.class, ElementAttributeMap::new,
NodeFeatures.ELEMENT_ATTRIBUTES);
registerFeature(ElementListenerMap.class, ElementListenerMap::new,
NodeFeatures.ELEMENT_LISTENERS);
registerFeature(SynchronizedPropertiesList.class,
SynchronizedPropertiesList::new);
SynchronizedPropertiesList::new,
NodeFeatures.SYNCHRONIZED_PROPERTIES);
registerFeature(SynchronizedPropertyEventsList.class,
SynchronizedPropertyEventsList::new);
registerFeature(ComponentMapping.class, ComponentMapping::new);
registerFeature(ModelList.class, ModelList::new);
registerFeature(PolymerServerEventHandlers.class,
PolymerServerEventHandlers::new);
SynchronizedPropertyEventsList::new,
NodeFeatures.SYNCHRONIZED_PROPERTY_EVENTS);
registerFeature(VirtualChildrenList.class, VirtualChildrenList::new,
NodeFeatures.VIRTUAL_CHILDREN);

/* PolymerTemplate stuff */
registerFeature(PolymerEventListenerMap.class,
PolymerEventListenerMap::new);
registerFeature(ClientCallableHandlers.class,
ClientCallableHandlers::new);
registerFeature(ShadowRootData.class, ShadowRootData::new);
registerFeature(ShadowRootHost.class, ShadowRootHost::new);
PolymerEventListenerMap::new,
NodeFeatures.POLYMER_EVENT_LISTENERS);
registerFeature(PolymerServerEventHandlers.class,
PolymerServerEventHandlers::new,
NodeFeatures.POLYMER_SERVER_EVENT_HANDLERS);

/* Rarely used element stuff */
registerFeature(ElementStylePropertyMap.class,
ElementStylePropertyMap::new,
NodeFeatures.ELEMENT_STYLE_PROPERTIES);
registerFeature(ShadowRootData.class, ShadowRootData::new,
NodeFeatures.SHADOW_ROOT_DATA);
registerFeature(ShadowRootHost.class, ShadowRootHost::new,
NodeFeatures.SHADOW_ROOT_HOST);
registerFeature(AttachExistingElementFeature.class,
AttachExistingElementFeature::new);
registerFeature(BasicTypeValue.class, BasicTypeValue::new);
registerFeature(VirtualChildrenList.class, VirtualChildrenList::new);
AttachExistingElementFeature::new,
NodeFeatures.ATTACH_EXISTING_ELEMENT);

/* Only used for the root node */
registerFeature(PushConfigurationMap.class, PushConfigurationMap::new,
NodeFeatures.UI_PUSHCONFIGURATION);
registerFeature(PushConfigurationParametersMap.class,
PushConfigurationParametersMap::new,
NodeFeatures.UI_PUSHCONFIGURATION_PARAMETERS);
registerFeature(LoadingIndicatorConfigurationMap.class,
LoadingIndicatorConfigurationMap::new,
NodeFeatures.LOADING_INDICATOR_CONFIGURATION);
registerFeature(PollConfigurationMap.class, PollConfigurationMap::new,
NodeFeatures.POLL_CONFIGURATION);
registerFeature(ReconnectDialogConfigurationMap.class,
ReconnectDialogConfigurationMap::new,
NodeFeatures.RECONNECT_DIALOG_CONFIGURATION);
}

private NodeFeatureRegistry() {
// Static only
}

private static <T extends NodeFeature> void registerFeature(Class<T> type,
SerializableFunction<StateNode, T> factory) {
NodeFeatureData featureData = new NodeFeatureData(factory);
SerializableFunction<StateNode, T> factory, int id) {
NodeFeatureData featureData = new NodeFeatureData(factory, id);
nodeFeatures.put(type, featureData);
idToFeature.put(featureData.id, type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package com.vaadin.flow.internal.nodefeature;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.junit.Assert;
import org.junit.Test;
Expand Down Expand Up @@ -122,4 +125,52 @@ public void testGetById() {
});
}

@Test
public void priorityOrder() {
List<Class<? extends NodeFeature>> priorityOrder = buildExpectedIdMap()
.keySet().stream()
.sorted(NodeFeatureRegistry.PRIORITY_COMPARATOR)
.collect(Collectors.toList());

List<Class<? extends NodeFeature>> expectedOrder = Arrays.asList(
/* Primary features */
ElementData.class, TextNodeMap.class, ModelList.class,
BasicTypeValue.class,

/* Common element features */
ElementChildrenList.class, ElementPropertyMap.class,

/* Component mapped features */
ComponentMapping.class, ClientCallableHandlers.class,

/* Supplementary element stuff */
ElementClassList.class, ElementAttributeMap.class,
ElementListenerMap.class, SynchronizedPropertiesList.class,
SynchronizedPropertyEventsList.class, VirtualChildrenList.class,

/* PolymerTemplate stuff */
PolymerEventListenerMap.class, PolymerServerEventHandlers.class,

/* Rarely used element stuff */
ElementStylePropertyMap.class, ShadowRootData.class,
ShadowRootHost.class, AttachExistingElementFeature.class,

/* Only used for the root node */
PushConfigurationMap.class,
PushConfigurationParametersMap.class,
LoadingIndicatorConfigurationMap.class,
PollConfigurationMap.class,
ReconnectDialogConfigurationMap.class);

Assert.assertEquals(expectedOrder.size(), priorityOrder.size());

for (int i = 0; i < priorityOrder.size(); i++) {
if (priorityOrder.get(i) != expectedOrder.get(i)) {
Assert.fail("Invalid priority ordering at index " + i
+ ". Expected " + expectedOrder.get(i).getSimpleName()
+ " but got " + priorityOrder.get(i).getSimpleName());
}
}
}

}

0 comments on commit f0d537d

Please sign in to comment.