Add caching to ImageResourceGenerator to re-use work across permutations.
Patch by: bobv
Review by: rjrjr
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6506 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
index 162771c..d52a8bc 100644
--- a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
+++ b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
@@ -19,7 +19,6 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
-import com.google.gwt.resources.ext.ResourceContext;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
@@ -453,8 +452,8 @@
* Only PNG is supported right now. In the future, we may be able to infer the
* best output type, and get rid of this constant.
*/
- private static final String BUNDLE_FILE_TYPE = "png";
- private static final String BUNDLE_MIME_TYPE = "image/png";
+ static final String BUNDLE_FILE_TYPE = "png";
+ static final String BUNDLE_MIME_TYPE = "image/png";
private static final int IMAGE_MAX_SIZE = Integer.getInteger(
"gwt.imageResource.maxBundleSize", 256);
@@ -647,18 +646,10 @@
}
/**
- * Write all images into the output.
- *
- * @param logger a hierarchical logger which logs to the hosted console
- * @param context the local ResourceContext that is being used to accumulate
- * output files
- * @param arranger a provider of image layout logic
- * @return a Java expression which will evaluate to the location of the
- * composite image at runtime
+ * Render the composited image into an array of bytes.
*/
- public String writeBundledImage(TreeLogger logger, ResourceContext context,
- Arranger arranger) throws UnableToCompleteException {
-
+ public byte[] render(TreeLogger logger, Arranger arranger)
+ throws UnableToCompleteException {
if (imageNameToImageRectMap.isEmpty()) {
return null;
}
@@ -668,11 +659,7 @@
byte[] imageBytes = createImageBytes(logger, bundledImage);
- String bundleFileName = context.deploy(
- context.getClientBundleType().getQualifiedSourceName() + ".cache."
- + BUNDLE_FILE_TYPE, BUNDLE_MIME_TYPE, imageBytes, false);
-
- return bundleFileName;
+ return imageBytes;
}
private ImageRect addImage(TreeLogger logger, String imageName, URL imageUrl)
diff --git a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
index fe538a9..516cf4f 100644
--- a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
@@ -17,9 +17,9 @@
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
-import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.dev.util.Util;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.resources.client.impl.ImageResourcePrototype;
@@ -34,6 +34,8 @@
import com.google.gwt.user.rebind.StringSourceWriter;
import java.awt.geom.AffineTransform;
+import java.io.File;
+import java.io.IOException;
import java.net.URL;
import java.util.EnumMap;
import java.util.HashMap;
@@ -46,12 +48,73 @@
* Builds an image strip for all ImageResources defined within an ClientBundle.
*/
public final class ImageResourceGenerator extends AbstractResourceGenerator {
- private Map<String, ImageRect> imageRectsByName;
- private Map<ImageRect, ImageBundleBuilder> buildersByImageRect;
- private Map<RepeatStyle, ImageBundleBuilder> buildersByRepeatStyle;
- private Map<ImageBundleBuilder, String[]> urlsByBuilder;
- private Map<ImageRect, String[]> urlsByExternalImageRect;
- private Map<ImageRect, ImageBundleBuilder> rtlImages;
+ /**
+ * This is shared that can be shared across permutations for a given
+ * ClientBundle type.
+ */
+ static class CachedState {
+ /**
+ * Associates an ImageRect with the ImageBundleBuilder that will emit its
+ * bytes.
+ */
+ public final Map<ImageRect, ImageBundleBuilder> buildersByImageRect = new IdentityHashMap<ImageRect, ImageBundleBuilder>();
+
+ /**
+ * Associates a layout constraint with an ImageBundleBuilder that can
+ * satisfy that constraint.
+ */
+ public final Map<RepeatStyle, ImageBundleBuilder> buildersByRepeatStyle = new EnumMap<RepeatStyle, ImageBundleBuilder>(
+ RepeatStyle.class);
+
+ /**
+ * Associates a method name with the ImageRect that contains the data for
+ * that method.
+ */
+ public final Map<String, ImageRect> imageRectsByName = new HashMap<String, ImageRect>();
+
+ /**
+ * Records that ImageRects that also need to provide an RTL-flipped version.
+ */
+ public final Set<ImageRect> rtlImages = new HashSet<ImageRect>();
+
+ public final Map<ImageBundleBuilder, URL[]> urlsByBuilder = new IdentityHashMap<ImageBundleBuilder, URL[]>();
+
+ /**
+ * Maps an ImageRect to two URLs that contain the normal and flipped
+ * contents.
+ */
+ public final Map<ImageRect, URL[]> urlsByExternalImageRect = new IdentityHashMap<ImageRect, URL[]>();
+ }
+
+ /**
+ * This data is specific to a particular permutation.
+ */
+ static class LocalState {
+ /**
+ * Maps resource URLs to field names within the generated ClientBundle type.
+ * These fields will be statically initialized to an expression that can be
+ * used to access the contents of the resource URL.
+ *
+ * @see ImageResourceGenerator#maybeDeploy
+ */
+ public final Map<URL, String> fieldNamesByUrl = new HashMap<URL, String>();
+
+ /**
+ * Maps an ImageRect to a pair of Java expressions. The first can be used to
+ * access the normal version of the resource, while the second, optional,
+ * field is used to access an RTL-flipped version.
+ */
+ public final Map<ImageRect, String[]> urlExpressionsByImageRect = new HashMap<ImageRect, String[]>();
+ }
+
+ /**
+ * This is set to <code>true</code> by {@link #init} if {@link #shared} was
+ * initialized from cached data.
+ */
+ private boolean prepared;
+ private CachedState shared;
+ private LocalState local;
+ private JType stringType;
@Override
public String createAssignment(TreeLogger logger, ResourceContext context,
@@ -63,18 +126,10 @@
sw.indent();
sw.println('"' + name + "\",");
- ImageRect rect = imageRectsByName.get(name);
+ ImageRect rect = shared.imageRectsByName.get(name);
assert rect != null : "No ImageRect ever computed for " + name;
- String[] urlExpressions;
- {
- ImageBundleBuilder builder = buildersByImageRect.get(rect);
- if (builder == null) {
- urlExpressions = urlsByExternalImageRect.get(rect);
- } else {
- urlExpressions = urlsByBuilder.get(builder);
- }
- }
+ String[] urlExpressions = local.urlExpressionsByImageRect.get(rect);
assert urlExpressions != null : "No URL expression for " + name;
assert urlExpressions.length == 2;
@@ -93,110 +148,75 @@
return sw.toString();
}
+ /**
+ * We use this as a signal that we have received all image methods and can now
+ * create the bundled images.
+ */
@Override
public void createFields(TreeLogger logger, ResourceContext context,
ClientBundleFields fields) throws UnableToCompleteException {
-
- TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
- JClassType stringType = typeOracle.findType(String.class.getName());
- assert stringType != null;
-
- Map<ImageBundleBuilder, String> prettyNames = new IdentityHashMap<ImageBundleBuilder, String>();
-
- for (Map.Entry<RepeatStyle, ImageBundleBuilder> entry : buildersByRepeatStyle.entrySet()) {
- RepeatStyle repeatStyle = entry.getKey();
- ImageBundleBuilder builder = entry.getValue();
- Arranger arranger;
-
- switch (repeatStyle) {
- case None:
- arranger = new ImageBundleBuilder.BestFitArranger();
- break;
- case Horizontal:
- arranger = new ImageBundleBuilder.VerticalArranger();
- break;
- case Vertical:
- arranger = new ImageBundleBuilder.HorizontalArranger();
- break;
- case Both:
- // This is taken care of when writing the external images;
- continue;
- default:
- logger.log(TreeLogger.ERROR, "Unknown RepeatStyle" + repeatStyle);
- throw new UnableToCompleteException();
- }
-
- String bundleUrlExpression = builder.writeBundledImage(logger.branch(
- TreeLogger.DEBUG, "Writing image strip", null), context, arranger);
-
- if (bundleUrlExpression == null) {
- continue;
- }
-
- String prettyName = "imageUrl" + repeatStyle;
- prettyNames.put(builder, prettyName);
- String fieldName = fields.define(stringType, prettyName,
- bundleUrlExpression, true, true);
- String[] strings = {fieldName, null};
- urlsByBuilder.put(builder, strings);
+ if (!prepared) {
+ finalizeArrangements(logger, context);
}
- if (rtlImages.size() > 0) {
- Set<ImageBundleBuilder> rtlBuilders = new HashSet<ImageBundleBuilder>();
-
- for (Map.Entry<ImageRect, ImageBundleBuilder> entry : rtlImages.entrySet()) {
- ImageRect rtlImage = entry.getKey();
-
- AffineTransform tx = new AffineTransform();
- tx.setTransform(-1, 0, 0, 1, rtlImage.getWidth(), 0);
-
- rtlImage.setTransform(tx);
-
- if (buildersByImageRect.containsKey(rtlImage)) {
- rtlBuilders.add(buildersByImageRect.get(rtlImage));
+ for (ImageRect rect : shared.imageRectsByName.values()) {
+ String[] urlExpressions;
+ {
+ URL[] contents;
+ ImageBundleBuilder builder = shared.buildersByImageRect.get(rect);
+ if (builder == null) {
+ contents = shared.urlsByExternalImageRect.get(rect);
} else {
- String[] strings = urlsByExternalImageRect.get(rtlImage);
- assert strings != null;
- byte[] imageBytes = ImageBundleBuilder.toPng(logger, rtlImage);
- strings[1] = context.deploy(rtlImage.getName() + "_rtl.png",
- "image/png", imageBytes, false);
+ contents = shared.urlsByBuilder.get(builder);
}
+ assert contents != null && contents.length == 2;
+
+ urlExpressions = new String[2];
+ urlExpressions[0] = maybeDeploy(context, fields, contents[0]);
+ urlExpressions[1] = maybeDeploy(context, fields, contents[1]);
}
-
- for (ImageBundleBuilder builder : rtlBuilders) {
- String bundleUrlExpression = builder.writeBundledImage(logger.branch(
- TreeLogger.DEBUG, "Writing image strip", null), context,
- new ImageBundleBuilder.IdentityArranger());
-
- if (bundleUrlExpression == null) {
- continue;
- }
-
- String prettyName = prettyNames.get(builder);
- String[] strings = urlsByBuilder.get(builder);
- assert strings != null;
-
- strings[1] = fields.define(stringType, prettyName + "_rtl",
- bundleUrlExpression, true, true);
- }
+ local.urlExpressionsByImageRect.put(rect, urlExpressions);
}
}
@Override
+ public void finish(TreeLogger logger, ResourceContext context)
+ throws UnableToCompleteException {
+ local = null;
+ }
+
+ @Override
public void init(TreeLogger logger, ResourceContext context) {
- imageRectsByName = new HashMap<String, ImageRect>();
- buildersByImageRect = new IdentityHashMap<ImageRect, ImageBundleBuilder>();
- buildersByRepeatStyle = new EnumMap<RepeatStyle, ImageBundleBuilder>(
- RepeatStyle.class);
- rtlImages = new IdentityHashMap<ImageRect, ImageBundleBuilder>();
- urlsByBuilder = new IdentityHashMap<ImageBundleBuilder, String[]>();
- urlsByExternalImageRect = new IdentityHashMap<ImageRect, String[]>();
+ // The images are bundled differently when data resources are supported
+ String key = context.getClientBundleType().getQualifiedSourceName() + ":"
+ + context.supportsDataUrls();
+ shared = context.getCachedData(key, CachedState.class);
+ prepared = shared != null;
+ if (prepared) {
+ logger.log(TreeLogger.DEBUG, "Using cached data");
+ } else {
+ shared = new CachedState();
+ context.putCachedData(key, shared);
+ }
+ local = new LocalState();
+
+ stringType = context.getGeneratorContext().getTypeOracle().findType(
+ String.class.getCanonicalName());
+ assert stringType != null : "No String type";
}
+ /**
+ * Process each image method. This will either assign the image to an
+ * ImageBundleBuilder or reencode an external image.
+ */
@Override
public void prepare(TreeLogger logger, ResourceContext context,
ClientBundleRequirements requirements, JMethod method)
throws UnableToCompleteException {
+ if (prepared) {
+ return;
+ }
+
URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
method);
@@ -219,37 +239,96 @@
rect.setPosition(0, 0);
throw new UnsuitableForStripException(rect);
}
- buildersByImageRect.put(rect, builder);
+ shared.buildersByImageRect.put(rect, builder);
} catch (UnsuitableForStripException e) {
// Add the image to the output as a separate resource
+ URL normalContents;
rect = e.getImageRect();
- String urlExpression;
if (rect.isAnimated()) {
// Can't re-encode animated images, so we emit it as-is
- urlExpression = context.deploy(resource, false);
+ normalContents = resource;
} else {
- // Re-encode the image as a PNG to strip random header data
- byte[] imageBytes = ImageBundleBuilder.toPng(logger, rect);
- urlExpression = context.deploy(rect.getName() + ".png", "image/png",
- imageBytes, false);
+ normalContents = reencodeToTempFile(logger, rect);
}
- urlsByExternalImageRect.put(rect, new String[] {urlExpression, null});
+ shared.urlsByExternalImageRect.put(rect, new URL[] {normalContents, null});
}
- imageRectsByName.put(name, rect);
+ shared.imageRectsByName.put(name, rect);
if (getFlipRtl(method)) {
- rtlImages.put(rect, null);
+ shared.rtlImages.add(rect);
+ }
+ }
+
+ private void finalizeArrangements(TreeLogger logger, ResourceContext context)
+ throws UnableToCompleteException {
+ for (Map.Entry<RepeatStyle, ImageBundleBuilder> entry : shared.buildersByRepeatStyle.entrySet()) {
+ RepeatStyle repeatStyle = entry.getKey();
+ ImageBundleBuilder builder = entry.getValue();
+ Arranger arranger;
+
+ switch (repeatStyle) {
+ case None:
+ arranger = new ImageBundleBuilder.BestFitArranger();
+ break;
+ case Horizontal:
+ arranger = new ImageBundleBuilder.VerticalArranger();
+ break;
+ case Vertical:
+ arranger = new ImageBundleBuilder.HorizontalArranger();
+ break;
+ case Both:
+ // This is taken care of when writing the external images;
+ continue;
+ default:
+ logger.log(TreeLogger.ERROR, "Unknown RepeatStyle" + repeatStyle);
+ throw new UnableToCompleteException();
+ }
+ URL normalContents = renderToTempFile(logger, builder, arranger);
+
+ shared.urlsByBuilder.put(builder, new URL[] {normalContents, null});
+ }
+
+ if (shared.rtlImages.size() > 0) {
+ Set<ImageBundleBuilder> rtlBuilders = new HashSet<ImageBundleBuilder>();
+
+ for (ImageRect rtlImage : shared.rtlImages) {
+ // Create a transformation to mirror about the Y-axis and translate
+ AffineTransform tx = new AffineTransform();
+ tx.setTransform(-1, 0, 0, 1, rtlImage.getWidth(), 0);
+ rtlImage.setTransform(tx);
+
+ if (shared.buildersByImageRect.containsKey(rtlImage)) {
+ /*
+ * This image is assigned to a builder, so we'll just remember to
+ * regenerate that builder.
+ */
+ rtlBuilders.add(shared.buildersByImageRect.get(rtlImage));
+ } else {
+ // Otherwise, emit the external version
+ URL[] contents = shared.urlsByExternalImageRect.get(rtlImage);
+ assert contents != null;
+ contents[1] = reencodeToTempFile(logger, rtlImage);
+ }
+ }
+
+ for (ImageBundleBuilder builder : rtlBuilders) {
+ URL[] contents = shared.urlsByBuilder.get(builder);
+ assert contents != null && contents.length == 2;
+
+ contents[1] = renderToTempFile(logger, builder,
+ new ImageBundleBuilder.IdentityArranger());
+ }
}
}
private ImageBundleBuilder getBuilder(JMethod method) {
RepeatStyle repeatStyle = getRepeatStyle(method);
- ImageBundleBuilder builder = buildersByRepeatStyle.get(repeatStyle);
+ ImageBundleBuilder builder = shared.buildersByRepeatStyle.get(repeatStyle);
if (builder == null) {
builder = new ImageBundleBuilder();
- buildersByRepeatStyle.put(repeatStyle, builder);
+ shared.buildersByRepeatStyle.put(repeatStyle, builder);
}
return builder;
}
@@ -271,4 +350,71 @@
return options.repeatStyle();
}
}
+
+ /**
+ * Create a field in the ClientBundle type that is used to intern the URL
+ * expressions that can be used to access the contents of the given resource.
+ *
+ * @return the name of the field that was created
+ */
+ private String maybeDeploy(ResourceContext context,
+ ClientBundleFields fields, URL resource) throws UnableToCompleteException {
+ if (resource == null) {
+ return null;
+ }
+
+ String toReturn = local.fieldNamesByUrl.get(resource);
+ if (toReturn == null) {
+ String urlExpression = context.deploy(resource, false);
+ toReturn = fields.define(stringType, "internedUrl"
+ + local.fieldNamesByUrl.size(), urlExpression, true, true);
+ local.fieldNamesByUrl.put(resource, toReturn);
+ }
+ return toReturn;
+ }
+
+ /**
+ * Re-encode an image as a PNG to strip random header data.
+ */
+ private URL reencodeToTempFile(TreeLogger logger, ImageRect rect)
+ throws UnableToCompleteException {
+ try {
+ byte[] imageBytes = ImageBundleBuilder.toPng(logger, rect);
+
+ if (imageBytes == null) {
+ return null;
+ }
+
+ File file = File.createTempFile(
+ ImageResourceGenerator.class.getSimpleName(), ".png");
+ file.deleteOnExit();
+ Util.writeBytesToFile(logger, file, imageBytes);
+ return file.toURI().toURL();
+ } catch (IOException ex) {
+ logger.log(TreeLogger.ERROR, "Unable to write re-encoded PNG", ex);
+ throw new UnableToCompleteException();
+ }
+ }
+
+ /**
+ * Re-encode an image as a PNG to strip random header data.
+ */
+ private URL renderToTempFile(TreeLogger logger, ImageBundleBuilder builder,
+ Arranger arranger) throws UnableToCompleteException {
+ try {
+ byte[] imageBytes = builder.render(logger, arranger);
+ if (imageBytes == null) {
+ return null;
+ }
+
+ File file = File.createTempFile(
+ ImageResourceGenerator.class.getSimpleName(), ".png");
+ file.deleteOnExit();
+ Util.writeBytesToFile(logger, file, imageBytes);
+ return file.toURI().toURL();
+ } catch (IOException ex) {
+ logger.log(TreeLogger.ERROR, "Unable to write re-encoded PNG", ex);
+ throw new UnableToCompleteException();
+ }
+ }
}