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(); + } + } }