/* * Copyright 2008 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.dev.jjs.ast.JClassType; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JRunAsync; import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer; import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap; import com.google.gwt.dev.js.ast.JsBlock; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsModVisitor; import com.google.gwt.dev.js.ast.JsNumericEntry; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.js.ast.JsStatement; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.thirdparty.guava.common.base.Predicate; import com.google.gwt.thirdparty.guava.common.collect.Collections2; import com.google.gwt.thirdparty.guava.common.collect.Iterables; import com.google.gwt.thirdparty.guava.common.collect.Lists; import com.google.gwt.thirdparty.guava.common.collect.Maps; 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; import java.util.Map; import java.util.Set; /** * <p> * Divides the code in a {@link JsProgram} into multiple fragments. The initial * fragment is sufficient to run all of the program's functionality except for * anything called in a callback supplied to * {@link com.google.gwt.core.client.GWT#runAsync(com.google.gwt.core.client.RunAsyncCallback) * GWT.runAsync()}. The remaining code should be downloadable via * {@link com.google.gwt.core.client.impl.AsyncFragmentLoader#inject(int)}. * </p> * * <p>Code splitting is implemented in a two stage process: the first stage decides how runAsyncs * are grouped together and the second decides the code in each fragment by computing the * appropriate control flow analyses. * </p> * * <p> The first stage is implemented by a {@link FragmentPartitionStrategy}, that can use fast but * unsound analyses to determine how to group the runAsyncs together, its only requirement is that * the result is a partition of a subset of the runAsyncs. Currently two strategies are implemented: * (1) {@link OneToOneFragmentPartitionStrategy} that assigns each runAsync to one fragment (as in * the original CodeSplitter), and (2) {@link MergeBySimilarityFragmentPartitionStrategy}</p> where * runAsyncs are pairwise merged together into a predetermined maximum number of fragments is a * way that is estimated to minimize the leftover fragment size (which is the strategy previously * attempted in the now obsolete CodeSplitter2. Additionally if the option * {@link CodeSplitters.MIN_FRAGMENT_SIZE} is set, this strategy also merge fragments that are * smaller than the minimum fragments size together and if the resulting combined fragment is still * smaller than the minimum fragment size it is left out of the fragmentation so that it is merged * into the leftovers by the second stage. * <p> * * <p> * The second stage assigns program atoms that to fragments that exclusively use them, setting up * and extra fragment for the non exclusive atoms and the runAsyncs that were not assigned to any * fragment. Code that is activated when the application starts and runAsyncs that are part of the * {@link CodeSplitters.PROP_INITIAL_SEQUENCE} are excluded from the first stage and are treated as * special initial fragments that will be downloaded when the application starts. * Whenever this second stage is changed <code>AsyncFragmentLoader</code> must be updated * in tandem. * </p> * * <p> * The fragment for a runAsync point contains different things depending on whether * it is in the initial load sequence or not. If it's in the initial load * sequence, then the fragment includes the code newly live once that split * point is crossed, that wasn't already live for the set of split points * earlier in the sequence. For a split point not in the initial load sequence, * the fragment contains only code exclusive to that split point, that is, code * that cannot be reached except via that split point. All other code goes into * the leftovers fragment. * </p> */ public class CodeSplitter { public static ControlFlowAnalyzer computeInitiallyLive(JProgram jprogram) { return computeInitiallyLive(jprogram, MultipleDependencyGraphRecorder.NULL_RECORDER); } public static void exec(TreeLogger logger, JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap map, int expectedFragmentCount, int minFragmentSize, MultipleDependencyGraphRecorder dependencyRecorder) { if (jprogram.getRunAsyncs().isEmpty()) { // Don't do anything if there is no call to runAsync return; } Event codeSplitterEvent = SpeedTracerLogger.start(CompilerEventType.CODE_SPLITTER); dependencyRecorder.open(); new CodeSplitter(logger, jprogram, jsprogram, map, expectedFragmentCount, minFragmentSize, dependencyRecorder).execImpl(); dependencyRecorder.close(); codeSplitterEvent.end(); } /** * <p> * Computes the "maximum total script size" for one permutation. The total * script size for one sequence of split points reached is the sum of the * scripts that are downloaded for that sequence. The maximum total script * size is the maximum such size for all possible sequences of split points. * </p> * * @param jsLengths The lengths of the fragments for the compilation of one * permutation */ public static int computeTotalSize(int[] jsLengths) { /* * The total script size is currently simple: it's the sum of all the * individual script files. * * TODO(rluble): This function seems unnecessary and out of place here. */ int totalSize = 0; for (int size : jsLengths) { totalSize += size; } return totalSize; } /** * Compute the set of initially live code for this program. Such code must be * included in the initial download of the program. */ private static ControlFlowAnalyzer computeInitiallyLive(JProgram jprogram, MultipleDependencyGraphRecorder dependencyRecorder) { dependencyRecorder.startDependencyGraph("initial", null); ControlFlowAnalyzer cfa = new ControlFlowAnalyzer(jprogram); cfa.setDependencyRecorder(dependencyRecorder); cfa.traverseEntryMethods(); computeLivenessFromCodeGenTypes(jprogram, cfa); dependencyRecorder.endDependencyGraph(); return cfa; } /** * Any immortal codegen types must be part of the initial download. */ private static void computeLivenessFromCodeGenTypes(JProgram jprogram, ControlFlowAnalyzer cfa) { for (JClassType type : jprogram.immortalCodeGenTypes) { cfa.traverseFromInstantiationOf(type); for (JMethod method : type.getMethods()) { if (!method.needsDynamicDispatch()) { cfa.traverseFrom(method); } } } } /** * Group run asyncs that have the same class literal as the first parameter in the two parameter * GWT.runAsync call. */ private static Collection<Collection<JRunAsync>> groupAsyncsByClassLiteral( Collection<JRunAsync> runAsyncs) { Collection<Collection<JRunAsync>> result = Lists.newArrayList(); Multimap<String, JRunAsync> asyncsGroupedByName = CodeSplitters.computeRunAsyncsByName(runAsyncs, true); // Add runAsyncs that have class literals in groups. result.addAll(asyncsGroupedByName.asMap().values()); // Add all the rest. result.addAll(CodeSplitters.getListOfLists(Collections2.filter(runAsyncs, new Predicate<JRunAsync>() { @Override public boolean apply(JRunAsync runAsync) { return !runAsync.hasExplicitClassLiteral(); } }))); return result; } private final MultipleDependencyGraphRecorder dependencyRecorder; private final FragmentExtractor fragmentExtractor; private final LinkedHashSet<JRunAsync> initialLoadSequence; /** * Code that is initially live when the program first downloads. */ private final ControlFlowAnalyzer initiallyLiveCfa; private final JProgram jprogram; private final JsProgram jsprogram; /** * Computed during {@link #execImpl()}, so that intermediate steps of it can * be used as they are created. */ private ControlFlowAnalyzer initialSequenceCfa; private final TreeLogger logger; private final boolean logFragmentMap; private final JavaToJavaScriptMap map; private final Set<JMethod> methodsStillInJavaScript; private final List<Fragment> fragments = Lists.newArrayList(); private final FragmentPartitionStrategy partitionStrategy; private CodeSplitter(TreeLogger logger, JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap map, int expectedFragmentCount, int minFragmentSize, MultipleDependencyGraphRecorder dependencyRecorder) { this.logger = logger.branch(TreeLogger.TRACE, "Splitting JavaScript for incremental download"); this.jprogram = jprogram; this.jsprogram = jsprogram; this.map = map; this.dependencyRecorder = dependencyRecorder; this.initialLoadSequence = jprogram.getInitialAsyncSequence(); assert initialLoadSequence != null; logFragmentMap = Boolean.getBoolean(CodeSplitters.PROP_LOG_FRAGMENT_MAP); fragmentExtractor = new FragmentExtractor(jprogram, jsprogram, map); initiallyLiveCfa = computeInitiallyLive(jprogram, dependencyRecorder); methodsStillInJavaScript = fragmentExtractor.findAllMethodsStillInJavaScript(); // TODO(rluble): expected fragment count is not enforced. the actual number // of fragments may be more or less.... partitionStrategy = expectedFragmentCount > 0 ? new MergeBySimilarityFragmentPartitionStrategy( CodeSplitters.getNumberOfExclusiveFragmentFromExpectedFragmentCount( initialLoadSequence.size(), expectedFragmentCount), minFragmentSize) : new OneToOneFragmentPartitionStrategy(); } /** * Compute the statements that go into a fragment. * * @param fragmentId the fragment number * @param alreadyLoaded The code that should be assumed to have already been * loaded * @param liveNow The code that is assumed live once this fragment loads; * anything in here but not in <code>alreadyLoaded</code> will be * included in the created fragment */ private List<JsStatement> statementsForFragment(int fragmentId, LivenessPredicate alreadyLoaded, LivenessPredicate liveNow) { if (logFragmentMap) { System.out.println(); System.out.println("==== Fragment " + fragmentId + " ===="); fragmentExtractor.setStatementLogger(new EchoStatementLogger(map)); } return fragmentExtractor.extractStatements(liveNow, alreadyLoaded); } /** * For each exclusive fragment (those that are not part of the initial load sequence) compute * a CFA that traces every split point not in the fragment; i.e. computes the atoms that are * live in (WholeProgram - Fragment). */ private Map<Fragment, ControlFlowAnalyzer> computeNotExclusiveCfaForFragments( Collection<Fragment> exclusiveFragments) { String dependencyGraphNameAfterInitialSequence = dependencyGraphNameAfterInitialSequence(); Map<Fragment, ControlFlowAnalyzer> notExclusiveCfaByFragment = Maps.newHashMap(); for (Fragment fragment : exclusiveFragments) { assert fragment.isExclusive(); dependencyRecorder.startDependencyGraph("sp" + fragment.getFragmentId(), dependencyGraphNameAfterInitialSequence); ControlFlowAnalyzer cfa = new ControlFlowAnalyzer(initialSequenceCfa); cfa.setDependencyRecorder(dependencyRecorder); for (Fragment otherFragment : exclusiveFragments) { // don't trace the initial fragments as they have already been traced and their atoms are // already in {@code initialSequenceCfa}. if (otherFragment.isInitial()) { continue; } if (otherFragment == fragment) { continue; } for (JRunAsync otherRunAsync : otherFragment.getRunAsyncs()) { cfa.traverseFromRunAsync(otherRunAsync); } } dependencyRecorder.endDependencyGraph(); notExclusiveCfaByFragment.put(fragment, cfa); } return notExclusiveCfaByFragment; } /** * Compute a CFA that covers the entire live code of the program. */ private ControlFlowAnalyzer computeCompleteCfa() { dependencyRecorder.startDependencyGraph("total", null); ControlFlowAnalyzer completeCfa = new ControlFlowAnalyzer(jprogram); completeCfa.setDependencyRecorder(dependencyRecorder); completeCfa.traverseEverything(); dependencyRecorder.endDependencyGraph(); return completeCfa; } /** * The name of the dependency graph that corresponds to * {@link #initialSequenceCfa}. */ private String dependencyGraphNameAfterInitialSequence() { if (initialLoadSequence.isEmpty()) { return "initial"; } else { return "sp" + Iterables.getLast(initialLoadSequence).getRunAsyncId(); } } /** * Map each program atom as exclusive to some split point, whenever possible. * Also fixes up load order problems that could result from splitting code * based on this assumption. */ private ExclusivityMap computeExclusivityMapWithFixups(Collection<Fragment> exclusiveFragments) { ControlFlowAnalyzer completeCfa = computeCompleteCfa(); Map<Fragment, ControlFlowAnalyzer> notExclusiveCfaByFragment = computeNotExclusiveCfaForFragments(exclusiveFragments); ExclusivityMap exclusivityMap = ExclusivityMap.computeExclusivityMap(exclusiveFragments, completeCfa, notExclusiveCfaByFragment); exclusivityMap.fixUpLoadOrderDependencies(logger, jprogram, methodsStillInJavaScript); return exclusivityMap; } /** * The current implementation of code splitting divides the program into fragments. There are * four different types of fragment. * - initial download: the part of the program that will execute from the entry point and is * not part of any runAsync. This fragment is implicit and there is not representation of * it in the code splitter. * - initial fragments: some runAsyncs are forced to be in the initial download by listing them * in the {@link CodeSplitters.PROP_INITIAL_SEQUENCE} property. A separate fragment (Type.INITIAL) * is created for each of there splitpoints and each contains only one splitpoit. * - exclusive fragments: the remaining runAsyncs are assigned to some exclusive fragment. Many * splitpoints may be in the same fragment but each of these splitpoints is in one and only * one fragment. The fragmentation strategy assigns splitpoints to fragments. * - leftover fragments: this is an artificial fragment that will contain all the atoms that * are not in the initial and are not exclusive to a fragment. * *<p>Code splitting is a three stage process: * - first the initial fragment are determined. * - then a fragmentation strategy is run to partition runAsyncs into exclusive fragments. * - lastly atoms that are not exclusive are assigned to the LEFT_OVERS fragment. */ private void execImpl() { Fragment lastInitialFragment = null; // Fragments are numbered from 0. int nextFragmentIdToAssign = 0; // Step #1: Decide how to map splitpoints to fragments. { /* * Compute the base fragment. It includes everything that is live when the * program starts. */ LivenessPredicate alreadyLoaded = new NothingAlivePredicate(); LivenessPredicate liveNow = new CfaLivenessPredicate(initiallyLiveCfa); Fragment fragment = new Fragment(Fragment.Type.INITIAL); fragment.setFragmentId(nextFragmentIdToAssign++); List<JsStatement> statementsForFragment = statementsForFragment(fragment.getFragmentId(), alreadyLoaded, liveNow); fragment.setStatements(statementsForFragment); lastInitialFragment = fragment; fragments.add(fragment); } /* * Compute the base fragments, for split points in the initial load * sequence. */ initialSequenceCfa = new ControlFlowAnalyzer(initiallyLiveCfa); String extendsCfa = "initial"; List<Integer> initialFragmentNumberSequence = new ArrayList<Integer>(); for (JRunAsync runAsync : initialLoadSequence) { LivenessPredicate alreadyLoaded = new CfaLivenessPredicate(initialSequenceCfa); String depGraphName = "sp" + runAsync.getRunAsyncId(); dependencyRecorder.startDependencyGraph(depGraphName, extendsCfa); extendsCfa = depGraphName; ControlFlowAnalyzer liveAfterSp = new ControlFlowAnalyzer(initialSequenceCfa); liveAfterSp.traverseFromRunAsync(runAsync); dependencyRecorder.endDependencyGraph(); LivenessPredicate liveNow = new CfaLivenessPredicate(liveAfterSp); Fragment fragment = new Fragment(Fragment.Type.INITIAL, lastInitialFragment); fragment.setFragmentId(nextFragmentIdToAssign++); fragment.addRunAsync(runAsync); List<JsStatement> statements = statementsForFragment(fragment.getFragmentId(), alreadyLoaded, liveNow); statements.addAll(fragmentExtractor.createOnLoadedCall(fragment.getFragmentId())); fragment.setStatements(statements); fragments.add(fragment); lastInitialFragment = fragment; initialFragmentNumberSequence.add(fragment.getFragmentId()); initialSequenceCfa = liveAfterSp; } // Set the initial fragment sequence. jprogram.setInitialFragmentIdSequence(initialFragmentNumberSequence); Collection<Collection<JRunAsync>> groupedNonInitialRunAsyncs = groupAsyncsByClassLiteral(Collections2.filter(jprogram.getRunAsyncs(), new Predicate<JRunAsync>() { @Override public boolean apply(JRunAsync jRunAsync) { return !isInitial(jRunAsync); } } )); // Decide exclusive fragments according to the preselected partitionStrategy. Collection<Fragment> exclusiveFragments = partitionStrategy.partitionIntoFragments(logger, initialSequenceCfa, groupedNonInitialRunAsyncs); Fragment leftOverFragment = new Fragment(Fragment.Type.NOT_EXCLUSIVE, lastInitialFragment); int firstExclusiveFragmentNumber = nextFragmentIdToAssign; // Assign fragment numbers to exclusive fragments. for (Fragment fragment : exclusiveFragments) { fragment.setFragmentId(nextFragmentIdToAssign++); fragment.addImmediateAncestors(leftOverFragment); } // From here numbers are unchanged, // Determine which atoms actually land in each exclusive fragment. ExclusivityMap exclusivityMap = computeExclusivityMapWithFixups(exclusiveFragments); /* * Populate the exclusively live fragments. Each includes everything * exclusively live after entry point i. */ for (Fragment fragment : exclusiveFragments) { assert fragment.isExclusive(); LivenessPredicate alreadyLoaded = exclusivityMap.getLivenessPredicate( ExclusivityMap.NOT_EXCLUSIVE); LivenessPredicate liveNow = exclusivityMap.getLivenessPredicate(fragment); List<JsStatement> statements = statementsForFragment(fragment.getFragmentId(), alreadyLoaded, liveNow); fragment.setStatements(statements); fragment.addStatements( fragmentExtractor.createOnLoadedCall(fragment.getFragmentId())); } fragments.addAll(exclusiveFragments); /* * Populate the leftovers fragment. */ { LivenessPredicate alreadyLoaded = new CfaLivenessPredicate(initialSequenceCfa); LivenessPredicate liveNow = exclusivityMap.getLivenessPredicate(ExclusivityMap.NOT_EXCLUSIVE); leftOverFragment.setFragmentId(nextFragmentIdToAssign++); List<JsStatement> statements = statementsForFragment(leftOverFragment.getFragmentId(), alreadyLoaded, liveNow); statements.addAll(fragmentExtractor.createOnLoadedCall(leftOverFragment.getFragmentId())); leftOverFragment.setStatements(statements); fragments.add(leftOverFragment); } // now install the new statements in the program fragments jsprogram.setFragmentCount(fragments.size()); for (int i = 0; i < fragments.size(); i++) { JsBlock fragmentBlock = jsprogram.getFragmentBlock(i); fragmentBlock.getStatements().clear(); fragmentBlock.getStatements().addAll(fragments.get(i).getStatements()); } // Pass the fragment partitioning information to JProgram. jprogram.setFragmentPartitioningResult( new FragmentPartitioningResult(fragments, jprogram.getRunAsyncs().size())); // Lastly patch up the JavaScript AST replaceFragmentId(); } private boolean isInitial(JRunAsync runAsync) { return initialLoadSequence.contains(runAsync); } /** * Patch up the fragment loading code in the JavaScript AST. * * <p>Initially GWT.runAsyncs are replaced in the {@link ReplaceRunAsyncs} pass and some code * is added to the AST that references the fragment for a runAsync. At that stage (before any * code splitting has occurred) each unique runAsync id and the number of runAsyncs are embedded * in the AST as "tagged" JsNumbericEntry. After code splitting those entries need to be replaced * by the frament ids associatied with each runAsync and the total number of fragments. * </p> */ private void replaceFragmentId() { // TODO(rluble): this approach where the ast is patched is not very clean. Maybe the fragment // information should be data instead of code in the ast. final FragmentPartitioningResult result = jprogram.getFragmentPartitioningResult(); (new JsModVisitor() { @Override public void endVisit(JsNumericEntry x, JsContext ctx) { if (x.getKey().equals("RunAsyncFragmentIndex")) { int fragmentId = result.getFragmentForRunAsync(x.getValue()); x.setValue(fragmentId); } // this is actually the fragmentId for the leftovers fragment. if (x.getKey().equals("RunAsyncFragmentCount")) { x.setValue(jsprogram.getFragmentCount() - 1); } } }).accept(jsprogram); } }