From f4c35a4c46e0fe54ee30813dbc274784efe35e22 Mon Sep 17 00:00:00 2001 From: Lucian Boaca Date: Wed, 24 Jul 2024 10:20:31 +0100 Subject: [PATCH] Refactor active calculation (#34) Refactor the active memory footprint calculation so that the core logic can be used in the ambient calculation as well, to optimise the way we evaluate nodes under multiple combinations of user styles. This change also removes some code that was unused after changes were made to the ambient calculation. --- .../ActiveMemoryFootprintCalculator.java | 40 +++++ .../AmbientMemoryFootprintCalculator.java | 9 +- .../dfx/memory/DrawableResourceDetails.java | 34 +++- ...ePerConfigurationFootprintCalculator.java} | 166 ++++++------------ .../memory/FootprintResourceReference.java | 143 --------------- .../dfx/memory/WatchFaceLayoutEvaluator.java | 21 +-- 6 files changed, 138 insertions(+), 275 deletions(-) create mode 100644 play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ActiveMemoryFootprintCalculator.java rename play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/{VariantMemoryFootprintCalculator.java => DynamicNodePerConfigurationFootprintCalculator.java} (54%) delete mode 100644 play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/FootprintResourceReference.java diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ActiveMemoryFootprintCalculator.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ActiveMemoryFootprintCalculator.java new file mode 100644 index 0000000..60baba3 --- /dev/null +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ActiveMemoryFootprintCalculator.java @@ -0,0 +1,40 @@ +package com.google.wear.watchface.dfx.memory; + +import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.findSceneNode; + +import org.w3c.dom.Document; + +import java.util.Map; + +/** Computes the memory footprint of a watch face in active. */ +public class ActiveMemoryFootprintCalculator { + private final VariantConfigValue activeConfigValue; + private final Document document; + private final Map resourceMemoryMap; + private final EvaluationSettings evaluationSettings; + + ActiveMemoryFootprintCalculator( + Document document, + Map resourceMemoryMap, + EvaluationSettings evaluationSettings) { + this.activeConfigValue = VariantConfigValue.active(evaluationSettings); + this.document = document; + this.resourceMemoryMap = resourceMemoryMap; + this.evaluationSettings = evaluationSettings; + } + + long computeAmbientMemoryFootprint() { + DrawableNodeConfigTable drawableNodeConfigTable = + DrawableNodeConfigTable.create(findSceneNode(document), activeConfigValue); + WatchFaceResourceCollector resourceCollector = + new WatchFaceResourceCollector(document, resourceMemoryMap, evaluationSettings); + return new DynamicNodePerConfigurationFootprintCalculator( + document, + resourceMemoryMap, + evaluationSettings, + activeConfigValue, + resourceCollector, + drawableNodeConfigTable) + .calculateMaxFootprintBytes(); + } +} diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AmbientMemoryFootprintCalculator.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AmbientMemoryFootprintCalculator.java index b874f56..1367c58 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AmbientMemoryFootprintCalculator.java +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AmbientMemoryFootprintCalculator.java @@ -174,13 +174,8 @@ long computeMaximumResourceUsage(Set topLevelResources) { for (Set set : sets) { long total = 0; for (String resource : set) { - DrawableResourceDetails details = resourceMemoryMap.get(resource); - if (details == null) { - throw new TestFailedException( - String.format( - "Asset %s was not found in the watch face package", - resource)); - } + DrawableResourceDetails details = + DrawableResourceDetails.findInMap(resourceMemoryMap, resource); long imageBytes = details.getBiggestFrameFootprintBytes(); // If this image can be downsampled then the size is halved. diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java index 6f546b0..ed0c7b2 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java @@ -28,6 +28,7 @@ import java.security.MessageDigest; import java.util.Formatter; import java.util.Iterator; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -262,8 +263,8 @@ String getSha1() { } /** - * Whether this image can be quantised into an RGB565 image with out loosing too much - * visual fidelity. + * Whether this image can be quantised into an RGB565 image with out loosing too much visual + * fidelity. */ boolean canUseRGB565() { return canUseRGB565; @@ -372,7 +373,13 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash( - name, numberOfImages, biggestFrameFootprintBytes, bounds, width, height, sha1, + name, + numberOfImages, + biggestFrameFootprintBytes, + bounds, + width, + height, + sha1, canUseRGB565); } @@ -433,7 +440,13 @@ public Builder setCanUseRGB565(boolean canUseRGB565) { public DrawableResourceDetails build() { return new DrawableResourceDetails( - name, numberOfImages, biggestFrameFootprintBytes, bounds, width, height, sha1, + name, + numberOfImages, + biggestFrameFootprintBytes, + bounds, + width, + height, + sha1, canUseRGB565); } } @@ -522,7 +535,7 @@ private static class QualtizationStats { long visiblePixelQuantizationErrorSum = 0; double getVisibleError() { - return (double)visiblePixelQuantizationErrorSum / (double)visiblePixels; + return (double) visiblePixelQuantizationErrorSum / (double) visiblePixels; } } @@ -577,4 +590,15 @@ private static String byteArray2Hex(final byte[] hash) { } return formatter.toString(); } + + static DrawableResourceDetails findInMap( + Map resourceMemoryMap, String resourceName) { + DrawableResourceDetails details = resourceMemoryMap.get(resourceName); + if (details == null) { + throw new TestFailedException( + String.format( + "Asset %s was not found in the watch face package", resourceName)); + } + return details; + } } diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/VariantMemoryFootprintCalculator.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DynamicNodePerConfigurationFootprintCalculator.java similarity index 54% rename from play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/VariantMemoryFootprintCalculator.java rename to play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DynamicNodePerConfigurationFootprintCalculator.java index 9bf3274..aa64e2b 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/VariantMemoryFootprintCalculator.java +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DynamicNodePerConfigurationFootprintCalculator.java @@ -16,16 +16,11 @@ package com.google.wear.watchface.dfx.memory; -import static com.google.wear.watchface.dfx.memory.FootprintResourceReference.sumDrawableResourceFootprintBytes; +import static com.google.wear.watchface.dfx.memory.DrawableResourceDetails.findInMap; import static com.google.wear.watchface.dfx.memory.UserConfigValue.SupportedConfigs.isValidUserConfigNode; import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.childrenStream; import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.findSceneNode; -import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.isBitmapFont; -import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.isClock; import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.isDrawableNode; -import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.isFont; -import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.isPartAnimatedImage; -import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.isPartImage; import static java.lang.Math.max; import static java.util.stream.Collectors.toList; @@ -34,42 +29,51 @@ import org.w3c.dom.Document; import org.w3c.dom.Node; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.LongStream; -/** Computes the memory footprint of a watch face for a single variant. */ -class VariantMemoryFootprintCalculator { +/** + * Calculates the maximum memory footprint needed to render the given dynamic nodes for any user + * style combination. The dynamic nodes, ie. the nodes that contain drawable resources and need to + * be re-drawn on each render, are provided via the {@code drawableNodeConfigTable} constructor + * argument, together with the user style configurations that cause them to be drawn. In active, + * this contains all the nodes that have drawable resource references. In ambient however, where the + * watch face interactivity is restricted, only a sub-set of the dynamic nodes are calculated (the + * clocks and complication slots). + */ +class DynamicNodePerConfigurationFootprintCalculator { private final Document document; private final Map resourceMemoryMap; private final EvaluationSettings evaluationSettings; private final Node sceneNode; private final WatchFaceResourceCollector resourceCollector; + private final VariantConfigValue variant; + private final DrawableNodeConfigTable drawableNodeConfigTable; + private final Set dynamicNodesToConsider; - VariantMemoryFootprintCalculator( + DynamicNodePerConfigurationFootprintCalculator( Document document, Map resourceMemoryMap, - EvaluationSettings evaluationSettings) { + EvaluationSettings evaluationSettings, + VariantConfigValue variant, + WatchFaceResourceCollector resourceCollector, + DrawableNodeConfigTable drawableNodeConfigTable) { this.document = document; this.sceneNode = findSceneNode(document); this.resourceMemoryMap = resourceMemoryMap; this.evaluationSettings = evaluationSettings; - this.resourceCollector = - new WatchFaceResourceCollector(document, resourceMemoryMap, evaluationSettings); + this.variant = variant; + this.resourceCollector = resourceCollector; + this.drawableNodeConfigTable = drawableNodeConfigTable; + this.dynamicNodesToConsider = + drawableNodeConfigTable.getAllEntries().stream() + .map(entry -> entry.node) + .collect(toSet()); } - /** - * Evaluates the watch face layout document under the given variant. - * - * @param variant the variant to consider when traversing and evaluating the watch face. - * @return the memory footprint, in bytes, that the watch face consumes under the given variant. - */ - long evaluateBytes(VariantConfigValue variant) { - DrawableNodeConfigTable drawableNodeConfigTable = - DrawableNodeConfigTable.create(sceneNode, variant); + long calculateMaxFootprintBytes() { // find the user config keys that are mutually exclusive, meaning that no two keys from // different sets are parents of the same resource. List> userConfigSplit = @@ -94,10 +98,10 @@ long evaluateBytes(VariantConfigValue variant) { if (evaluationSettings.isVerbose()) { System.out.println("Using greedy evaluation%n"); } - return greedyEvaluate(variant); + return greedyEvaluate(); } - return lazyEvaluate(variant, configIterators, drawableNodeConfigTable); + return lazyEvaluate(configIterators); } /** @@ -106,21 +110,23 @@ long evaluateBytes(VariantConfigValue variant) { * This approach is only used when the watch face has too many user configs and cannot be * accurately evaluated in a reasonable amount of time. */ - private long greedyEvaluate(VariantConfigValue variant) { - return greedyEvaluate(sceneNode, variant); + long greedyEvaluate() { + return greedyEvaluate(sceneNode); } - private long greedyEvaluate(Node currentNode, VariantConfigValue variant) { + private long greedyEvaluate(Node currentNode) { if (variant.isNodeSkipped(currentNode)) { return 0; } - if (isDrawableNode(currentNode)) { - return sumDrawableResourceFootprintBytes( - resourceMemoryMap, collectDrawableNodeResourceReferences(currentNode, variant)); + if (isDrawableNode(currentNode) && dynamicNodesToConsider.contains(currentNode)) { + return resourceCollector.collectResources(currentNode, variant).stream() + .mapToLong( + resourceName -> + findInMap(resourceMemoryMap, resourceName) + .getTotalFootprintBytes()) + .sum(); } - LongStream childrenFootprints = - childrenStream(currentNode) - .mapToLong(childNode -> greedyEvaluate(childNode, variant)); + LongStream childrenFootprints = childrenStream(currentNode).mapToLong(this::greedyEvaluate); if (isValidUserConfigNode(currentNode)) { return childrenFootprints.max().orElse(0); } @@ -139,14 +145,9 @@ private long greedyEvaluate(Node currentNode, VariantConfigValue variant) { *

We use a lazy iterator because we can still have too many configs to store in memory, * hence we are * producing and evaluating them lazily. * - * @param variant the variant under which the layout is evaluated * @param configIterators the list of iterators producing mutually-exclusive config sets. */ - private long lazyEvaluate( - VariantConfigValue variant, - List> configIterators, - DrawableNodeConfigTable drawableNodeConfigTable) { - + private long lazyEvaluate(List> configIterators) { long footprintOfResourcesWithConfigs = configIterators.stream() .mapToLong( @@ -169,15 +170,13 @@ private long lazyEvaluate( */ private long evaluateIndependentDrawableNodesBytes( VariantConfigValue variant, DrawableNodeConfigTable drawableNodeConfigTable) { - Set topLevelResourceReferences = - drawableNodeConfigTable.getIndependentDrawableNodes().stream() - .flatMap( - drawable -> - collectDrawableNodeResourceReferences( - drawable.node, variant) - .stream()) - .collect(toSet()); - return sumDrawableResourceFootprintBytes(resourceMemoryMap, topLevelResourceReferences); + return drawableNodeConfigTable.getIndependentDrawableNodes().stream() + .flatMap(entry -> resourceCollector.collectResources(entry.node, variant).stream()) + .distinct() + .mapToLong( + resourceName -> + findInMap(resourceMemoryMap, resourceName).getTotalFootprintBytes()) + .sum(); } /** @@ -197,70 +196,21 @@ private long evaluateConfigSetForMaxFootprintBytes( .filter(entry -> entry.matchesConfigSet(next)) .collect(toList()); - Set resourcesForConfigSet = + long footprintForConfig = nodesMatchingConfigSet.stream() .flatMap( - leafAndConfig -> - collectDrawableNodeResourceReferences( - leafAndConfig.node, variant) + entry -> + resourceCollector + .collectResources(entry.node, variant) .stream()) - .collect(toSet()); - long footprintForConfig = - sumDrawableResourceFootprintBytes(resourceMemoryMap, resourcesForConfigSet); + .distinct() + .mapToLong( + resourceName -> + findInMap(resourceMemoryMap, resourceName) + .getTotalFootprintBytes()) + .sum(); maxFootprint = max(footprintForConfig, maxFootprint); } return maxFootprint; } - - /** - * Collect the footprint resource references from a node that renders images. It is important to - * work with nodes (PartAnimatedImage, PartImage etc) because there are differences in the logic - * for computing memory footprint between ambient and active depending on the node. - */ - private Set collectDrawableNodeResourceReferences( - Node currentNode, VariantConfigValue variant) { - if (isPartAnimatedImage(currentNode)) { - return collectPartAnimatedImageResources(currentNode, variant); - } else if (isPartImage(currentNode) - || isBitmapFont(currentNode) - || isFont(currentNode) - || isClock(currentNode)) { - return collectTotalOfResources(currentNode); - } - // explicitly check for the expected nodes and throw if we forget one of them to make it - // easier to add more cases - throw new IllegalArgumentException( - String.format("Drawable node %s was not handled", currentNode.getNodeName())); - } - - private Set collectPartAnimatedImageResources( - Node currentNode, VariantConfigValue variant) { - Set resourceNamesUnderAnimatedImage = - resourceCollector.collectResources(currentNode); - // Animated images are counted differently in ambient and active. - if (variant.isAmbient()) { - // If we're evaluating the watch face in ambient, then we want the biggest frame - // of the resources referenced by the animation. - String resourceNameWithBiggestFrame = - Collections.max( - resourceNamesUnderAnimatedImage, - Comparator.comparingLong( - resourceName -> - resourceMemoryMap - .get(resourceName) - .getBiggestFrameFootprintBytes())); - return Collections.singleton( - FootprintResourceReference.biggestFrameOf(resourceNameWithBiggestFrame)); - } else { - return resourceNamesUnderAnimatedImage.stream() - .map(FootprintResourceReference::totalOf) - .collect(toSet()); - } - } - - private Set collectTotalOfResources(Node currentNode) { - return resourceCollector.collectResources(currentNode).stream() - .map(FootprintResourceReference::totalOf) - .collect(toSet()); - } } diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/FootprintResourceReference.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/FootprintResourceReference.java deleted file mode 100644 index 738571d..0000000 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/FootprintResourceReference.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * 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 com.google.wear.watchface.dfx.memory; - -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -/** - * A reference to a drawable resource that must be counted towards the memory footprint in either - * ambient or active. - */ -class FootprintResourceReference { - - /** The type of the reference. */ - enum Type { - /** - * A "Total" reference counts the total footprint of that drawable in the memory footprint. - */ - TOTAL, - /** - * A "BiggestFrame" reference counts only the biggest frame of the drawable in the memory - * footprint. It is relevant for GIFs and WEBPs, which can have more than one frame, but in - * ambient we want to count only the biggest frame of that resource in the memory footprint - * value because animations are paused in ambient. - */ - BIGGEST_FRAME - } - - /** - * Constructs a FootprintResourceReference object with the type of "BiggestFrame". - * - * @param resourceName the name of the referenced resource. - */ - static FootprintResourceReference biggestFrameOf(String resourceName) { - return new FootprintResourceReference(resourceName, Type.BIGGEST_FRAME); - } - - /** - * Constructs a FootprintResourceReference object with the type of "Total". - * - * @param resourceName the name of the referenced resource. - */ - static FootprintResourceReference totalOf(String resourceName) { - return new FootprintResourceReference(resourceName, Type.TOTAL); - } - - private final String name; - - private final Type type; - - private FootprintResourceReference(String name, Type type) { - this.name = name; - this.type = type; - } - - /** The name of the referenced resource. */ - public String getName() { - return name; - } - - /** - * The type of the reference. - * - * @see Type - */ - public Type getType() { - return type; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof FootprintResourceReference)) return false; - FootprintResourceReference that = (FootprintResourceReference) o; - return Objects.equals(name, that.name) && type == that.type; - } - - @Override - public int hashCode() { - return Objects.hash(name, type); - } - - /** - * Gets the memory footprint of the current resource reference from all the resource details - * from the watch face, based on the current reference type. - * - * @param memoryFootprints a map holding the resource details found in the watch face package. - * The keys match resource reference names. - * @return the memory footprint in bytes of this reference, based on its type. If it is a - * reference of type {@link Type#TOTAL}, then returns the total footprint of this resource. - * If it is of type {@link Type#BIGGEST_FRAME}, return just the footprint of this resource's - * biggest frame. - * @throws TestFailedException if any of the assets referenced by the watch face does not exist - * in the watch face package. - */ - long getFootprintBytes(Map memoryFootprints) { - DrawableResourceDetails drawableResourceDetails = memoryFootprints.get(getName()); - if (drawableResourceDetails == null) { - throw new TestFailedException( - String.format("Asset %s was not found in the watch face package", getName())); - } - switch (getType()) { - case TOTAL: - return drawableResourceDetails.getTotalFootprintBytes(); - case BIGGEST_FRAME: - return drawableResourceDetails.getBiggestFrameFootprintBytes(); - default: - throw new IllegalStateException( - String.format("Resource reference of type %s not supported", getType())); - } - } - - /** - * Calculates the memory footprint of the given resource references, in bytes. - * - * @param memoryFootprints the memory footprint table with all the resources. - * @param resources the resource references to be evaluated. - * @throws TestFailedException if any of the resources referenced by the watch face does not - * exist in the watch face package. - */ - static long sumDrawableResourceFootprintBytes( - Map memoryFootprints, - Set resources) { - return resources.stream() - .mapToLong(asset -> asset.getFootprintBytes(memoryFootprints)) - .sum(); - } -} diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceLayoutEvaluator.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceLayoutEvaluator.java index 72358d4..b001517 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceLayoutEvaluator.java +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceLayoutEvaluator.java @@ -16,11 +16,9 @@ package com.google.wear.watchface.dfx.memory; -import static com.google.wear.watchface.dfx.memory.FootprintResourceReference.sumDrawableResourceFootprintBytes; +import static com.google.wear.watchface.dfx.memory.DrawableResourceDetails.findInMap; import static com.google.wear.watchface.dfx.memory.WatchFaceDocuments.findSceneNode; -import static java.util.stream.Collectors.toSet; - import org.w3c.dom.Document; import java.util.Map; @@ -54,11 +52,9 @@ private static MemoryFootprint getMemoryFootprint( long totalFootprint = computeTotalMemory(document, resourceMemoryMap, settings); - VariantMemoryFootprintCalculator variantMemoryFootprintCalculator = - new VariantMemoryFootprintCalculator(document, resourceMemoryMap, settings); - long maxInActive = - variantMemoryFootprintCalculator.evaluateBytes(VariantConfigValue.active(settings)); + new ActiveMemoryFootprintCalculator(document, resourceMemoryMap, settings) + .computeAmbientMemoryFootprint(); long maxInAmbient = new AmbientMemoryFootprintCalculator(document, resourceMemoryMap, settings) .computeAmbientMemoryFootprint(450, 450); @@ -84,10 +80,11 @@ private static long computeTotalMemory( currentLayout, resourceMemoryMap, evaluationSettings); Set allResourceNames = resourceCollector.collectResources(findSceneNode(currentLayout)); - return sumDrawableResourceFootprintBytes( - resourceMemoryMap, - allResourceNames.stream() - .map(FootprintResourceReference::totalOf) - .collect(toSet())); + + return allResourceNames.stream() + .mapToLong( + resourceName -> + findInMap(resourceMemoryMap, resourceName).getTotalFootprintBytes()) + .sum(); } }