/* * Copyright 2013 Google Inc. * * 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.gwt.dev.jjs.impl.codesplitter; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.dev.cfg.ConfigurationProperties; import com.google.gwt.dev.jjs.ast.JArrayType; import com.google.gwt.dev.jjs.ast.JExpression; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JMethodCall; import com.google.gwt.dev.jjs.ast.JNewArray; import com.google.gwt.dev.jjs.ast.JNode; import com.google.gwt.dev.jjs.ast.JNumericEntry; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JRunAsync; import com.google.gwt.dev.jjs.impl.JsniRefLookup; import com.google.gwt.dev.util.JsniRef; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.thirdparty.guava.common.base.Function; import com.google.gwt.thirdparty.guava.common.base.Joiner; import com.google.gwt.thirdparty.guava.common.collect.Collections2; import com.google.gwt.thirdparty.guava.common.collect.LinkedListMultimap; import com.google.gwt.thirdparty.guava.common.collect.Lists; import com.google.gwt.thirdparty.guava.common.collect.Multimap; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; /** * Utility function related to code splitting. */ public class CodeSplitters { /** * Choose an initial load sequence of split points for the specified program. * Do so by identifying split points whose code always load first, before any * other split points. As a side effect, modifies * {@link com.google.gwt.core.client.impl.AsyncFragmentLoader#initialLoadSequence} * in the program being compiled. * * @throws UnableToCompleteException If the module specifies a bad load order */ public static void pickInitialLoadSequence(TreeLogger logger, JProgram program, ConfigurationProperties config) throws UnableToCompleteException { SpeedTracerLogger.Event codeSplitterEvent = SpeedTracerLogger .start(CompilerEventType.CODE_SPLITTER, "phase", "pickInitialLoadSequence"); TreeLogger branch = logger.branch(TreeLogger.TRACE, "Looking up initial load sequence for split points"); LinkedHashSet<JRunAsync> asyncsInInitialLoadSequence = new LinkedHashSet<JRunAsync>(); List<String> initialSequence = config.getStrings(PROP_INITIAL_SEQUENCE); for (String runAsyncReference : initialSequence) { JRunAsync runAsync = findRunAsync(runAsyncReference, program, branch); if (asyncsInInitialLoadSequence.contains(runAsync)) { branch.log(TreeLogger.ERROR, "Split point specified more than once: " + runAsyncReference); } asyncsInInitialLoadSequence.add(runAsync); } logInitialLoadSequence(logger, asyncsInInitialLoadSequence); installInitialLoadSequenceField(program, asyncsInInitialLoadSequence); program.setInitialAsyncSequence(asyncsInInitialLoadSequence); codeSplitterEvent.end(); } /** * Find a split point as designated in the {@link #PROP_INITIAL_SEQUENCE} * configuration property. */ public static JRunAsync findRunAsync(String refString, JProgram program, TreeLogger branch) throws UnableToCompleteException { SpeedTracerLogger.Event codeSplitterEvent = SpeedTracerLogger.start(CompilerEventType.CODE_SPLITTER, "phase", "findRunAsync"); Multimap<String, JRunAsync> splitPointsByRunAsyncName = computeRunAsyncsByName(program.getRunAsyncs(), false); if (refString.startsWith("@")) { JsniRef jsniRef = JsniRef.parse(refString); if (jsniRef == null) { branch.log(TreeLogger.ERROR, "Badly formatted JSNI reference in " + PROP_INITIAL_SEQUENCE + ": " + refString); throw new UnableToCompleteException(); } final String lookupErrorHolder[] = new String[1]; JNode referent = JsniRefLookup.findJsniRefTarget(jsniRef, program, new JsniRefLookup.ErrorReporter() { @Override public void reportError(String error) { lookupErrorHolder[0] = error; } }); if (referent == null) { TreeLogger resolveLogger = branch.branch(TreeLogger.ERROR, "Could not resolve JSNI reference: " + jsniRef); resolveLogger.log(TreeLogger.ERROR, lookupErrorHolder[0]); throw new UnableToCompleteException(); } if (!(referent instanceof JMethod)) { branch.log(TreeLogger.ERROR, "Not a method: " + referent); throw new UnableToCompleteException(); } JMethod method = (JMethod) referent; String canonicalName = ReplaceRunAsyncs.getImplicitName(method); Collection<JRunAsync> splitPoints = splitPointsByRunAsyncName.get(canonicalName); if (splitPoints == null) { branch.log(TreeLogger.ERROR, "Method does not enclose a runAsync call: " + jsniRef); throw new UnableToCompleteException(); } if (splitPoints.size() > 1) { branch.log(TreeLogger.ERROR, "Method includes multiple runAsync calls, " + "so it's ambiguous which one is meant: " + jsniRef); throw new UnableToCompleteException(); } assert splitPoints.size() == 1; return splitPoints.iterator().next(); } // Assume it's a raw class name Collection<JRunAsync> splitPoints = splitPointsByRunAsyncName.get(refString); if (splitPoints == null || splitPoints.size() == 0) { branch.log(TreeLogger.ERROR, "No runAsync call is labelled with class " + refString); throw new UnableToCompleteException(); } if (splitPoints.size() > 1) { branch.log(TreeLogger.ERROR, "More than one runAsync call is labelled with class " + refString); throw new UnableToCompleteException(); } assert splitPoints.size() == 1; JRunAsync result = splitPoints.iterator().next(); codeSplitterEvent.end(); return result; } /** * Returns the collection of asyncs as a collection of singleton collections containing one * async each. */ static Collection<Collection<JRunAsync>> getListOfLists(Collection<JRunAsync> runAsyncs) { return Collections2.transform(runAsyncs, new Function<JRunAsync, Collection<JRunAsync>>() { @Override public Collection<JRunAsync> apply(JRunAsync runAsync) { return Lists.newArrayList(runAsync); } }); } /** * Returns the number of exclusive fragments from the expected number of fragments. The * result is expectedFragmentCount - (initials + 1) - 1 (for leftovers). * */ public static int getNumberOfExclusiveFragmentFromExpectedFragmentCount( int numberOfInitialAsyncs, int expectedFragmentCount) { return Math.max(0, expectedFragmentCount - (numberOfInitialAsyncs + 1) - 1); } /** * A Java property that causes the fragment map to be logged. */ static String PROP_LOG_FRAGMENT_MAP = "gwt.jjs.logFragmentMap"; static final String PROP_INITIAL_SEQUENCE = "compiler.splitpoint.initial.sequence"; public static final String MIN_FRAGMENT_SIZE = "compiler.splitpoint.leftovermerge.size"; private static void logInitialLoadSequence(TreeLogger logger, LinkedHashSet<JRunAsync> initialLoadSequence) { if (!logger.isLoggable(TreeLogger.TRACE)) { return; } StringBuilder message = new StringBuilder(); message.append("Initial load sequence of split points: "); if (initialLoadSequence.isEmpty()) { message.append("(none)"); } else { Collection<Integer> runAsyncIds = Collections2.transform(initialLoadSequence, new Function<JRunAsync, Integer>() { @Override public Integer apply(JRunAsync runAsync) { return runAsync.getRunAsyncId(); } }); message.append(Joiner.on(", ").join(runAsyncIds)); } logger.log(TreeLogger.TRACE, message.toString()); } /** * Installs the initial load sequence into AsyncFragmentLoader.BROWSER_LOADER. * The initializer looks like this: * * <pre> * AsyncFragmentLoader BROWSER_LOADER = makeBrowserLoader(1, new int[]{}); * </pre> * * The second argument (<code>new int[]</code>) gets replaced by an array * corresponding to <code>initialLoadSequence</code>. */ private static void installInitialLoadSequenceField(JProgram program, LinkedHashSet<JRunAsync> initialLoadSequence) { // Arg 1 is initialized in the source as "new int[]{}". JMethodCall call = ReplaceRunAsyncs.getBrowserLoaderConstructor(program); JExpression arg1 = call.getArgs().get(1); assert arg1 instanceof JNewArray; JArrayType arrayType = program.getTypeArray(JPrimitiveType.INT); assert ((JNewArray) arg1).getArrayType() == arrayType; List<JExpression> initializers = new ArrayList<JExpression>(initialLoadSequence.size()); // RunAsyncFramentIndex will later be replaced by the fragment the async is in. // TODO(rluble): this approach is not very clean, ideally the load sequence should be // installed AFTER code splitting when the fragment ids are known; rather than inserting // a placeholder in the AST and patching the ast later. for (JRunAsync runAsync : initialLoadSequence) { initializers.add(new JNumericEntry(call.getSourceInfo(), "RunAsyncFragmentIndex", runAsync.getRunAsyncId())); } JNewArray newArray = JNewArray.createArrayWithInitializers(arg1.getSourceInfo(), arrayType, Lists.newArrayList(initializers)); call.setArg(1, newArray); } static Multimap<String, JRunAsync> computeRunAsyncsByName(Collection<JRunAsync> runAsyncs, boolean onlyExplicitNames) { Multimap<String, JRunAsync> runAsyncsByName = LinkedListMultimap.create(); for (JRunAsync runAsync : runAsyncs) { String name = runAsync.getName(); if (name == null || (onlyExplicitNames && !runAsync.hasExplicitClassLiteral())) { continue; } runAsyncsByName.put(name, runAsync); } return runAsyncsByName; } }