| /* |
| * 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.dev.jjs.ast.Context; |
| import com.google.gwt.dev.jjs.ast.JClassLiteral; |
| import com.google.gwt.dev.jjs.ast.JDeclaredType; |
| import com.google.gwt.dev.jjs.ast.JExpression; |
| import com.google.gwt.dev.jjs.ast.JField; |
| import com.google.gwt.dev.jjs.ast.JMethod; |
| import com.google.gwt.dev.jjs.ast.JNode; |
| import com.google.gwt.dev.jjs.ast.JProgram; |
| import com.google.gwt.dev.jjs.ast.JRunAsync; |
| import com.google.gwt.dev.jjs.ast.JType; |
| import com.google.gwt.dev.jjs.ast.JVisitor; |
| import com.google.gwt.dev.jjs.ast.js.JsniMethodRef; |
| import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer; |
| import com.google.gwt.dev.js.ast.JsStatement; |
| import com.google.gwt.thirdparty.guava.common.base.Predicates; |
| import com.google.gwt.thirdparty.guava.common.collect.Maps; |
| import com.google.gwt.thirdparty.guava.common.collect.Sets; |
| |
| import java.util.ArrayDeque; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| |
| /** |
| * A map from program atoms to fragments; each fragment may contain more than one runAsync. |
| * Maps atom to the fragments, if any, that they are exclusive to. Atoms not |
| * exclusive to any fragment are either mapped to NOT_EXCLUSIVE, or left out of the map entirely. |
| * Note that the map is incomplete; any entry not included has not been proven to be exclusive. |
| * Also, note that the initial load sequence is assumed to already be loaded. |
| */ |
| class ExclusivityMap { |
| /** |
| * A liveness predicate that is based on an exclusivity map. |
| */ |
| private class ExclusivityMapLivenessPredicate implements LivenessPredicate { |
| private final Fragment fragment; |
| |
| public ExclusivityMapLivenessPredicate(Fragment fragment) { |
| this.fragment = fragment; |
| } |
| |
| @Override |
| public boolean isLive(JDeclaredType type) { |
| return isLiveInFragment(fragment, type); |
| } |
| |
| @Override |
| public boolean isLive(JField field) { |
| return isLiveInFragment(fragment, field); |
| } |
| |
| @Override |
| public boolean isLive(JMethod method) { |
| return isLiveInFragment(fragment, method); |
| } |
| |
| @Override |
| public boolean miscellaneousStatementsAreLive() { |
| return true; |
| } |
| } |
| |
| /** |
| * A dummy fragment that represents atoms that are not in the map. |
| */ |
| public static final Fragment NOT_EXCLUSIVE = new Fragment(Fragment.Type.NOT_EXCLUSIVE) { |
| @Override |
| public int getFragmentId() { |
| throw makeUnsupportedException("getFragmentId"); |
| } |
| |
| @Override |
| public List<JsStatement> getStatements() { |
| throw makeUnsupportedException("getStatements"); |
| } |
| |
| @Override |
| public void setStatements(List<JsStatement> statements) { |
| throw makeUnsupportedException("setStatements"); |
| } |
| |
| @Override |
| public void addStatements(List<JsStatement> statements) { |
| throw makeUnsupportedException("addStatements"); |
| } |
| |
| @Override |
| public Set<JRunAsync> getRunAsyncs() { |
| throw makeUnsupportedException("getRunAsyncs"); |
| } |
| |
| @Override |
| public void addRunAsync(JRunAsync runAsync) { |
| throw makeUnsupportedException("addSplitPoint"); |
| } |
| |
| @Override |
| public void setFragmentId(int fragmentId) { |
| throw makeUnsupportedException("setFragmentId"); |
| } |
| |
| private UnsupportedOperationException makeUnsupportedException(String methodName) { |
| return new UnsupportedOperationException(methodName + " is not supported in the " |
| + "dummy NOT_EXCLUSIVE fragment"); |
| } |
| }; |
| |
| /** |
| * Gets the liveness predicate for fragment. |
| */ |
| LivenessPredicate getLivenessPredicate(Fragment fragment) { |
| return new ExclusivityMapLivenessPredicate(fragment); |
| } |
| |
| /** |
| * Determine whether a field is live in a fragment. |
| */ |
| public boolean isLiveInFragment(Fragment fragment, JField field) { |
| return isLiveInFragment(fragmentForField, field, fragment); |
| } |
| |
| /** |
| * Determine whether a method is live in a fragment. |
| */ |
| public boolean isLiveInFragment(Fragment fragment, JMethod method) { |
| return isLiveInFragment(fragmentForMethod, method, fragment); |
| } |
| |
| /** |
| * Determine whether a type is live in a fragment. |
| */ |
| public boolean isLiveInFragment(Fragment fragment, JDeclaredType type) { |
| return isLiveInFragment(fragmentForType, type, fragment); |
| } |
| |
| private Map<JField, Fragment> fragmentForField = Maps.newHashMap(); |
| private Map<JMethod, Fragment> fragmentForMethod = Maps.newHashMap(); |
| private Map<JDeclaredType, Fragment> fragmentForType = Maps.newHashMap(); |
| |
| /** |
| * Traverse {@code exp} and find all referenced class literals. |
| */ |
| private static Set<JClassLiteral> classLiteralsIn(JExpression expression) { |
| final Set<JClassLiteral> literals = Sets.newHashSet(); |
| new JVisitor() { |
| @Override |
| public void endVisit(JClassLiteral classLiteral, Context ctx) { |
| literals.add(classLiteral); |
| } |
| }.accept(expression); |
| return literals; |
| } |
| |
| /** |
| * Traverse {@code exp} and find all referenced JMethods. |
| */ |
| private static Set<JMethod> methodsReferencesIn(JExpression expression) { |
| final Set<JMethod> methods = Sets.newHashSet(); |
| new JVisitor() { |
| @Override |
| public void endVisit(JsniMethodRef jsniMethodRef, Context ctx) { |
| methods.add(jsniMethodRef.getTarget()); |
| } |
| }.accept(expression); |
| return methods; |
| } |
| |
| /** |
| * Map atoms to exclusive fragments. Do this by trying to find code atoms that |
| * are only needed by a single split point. Such code can be moved to the |
| * exclusively live fragment associated with that split point. |
| */ |
| public static ExclusivityMap computeExclusivityMap(Collection<Fragment> exclusiveFragments, |
| ControlFlowAnalyzer completeCfa, |
| Map<Fragment, ControlFlowAnalyzer> notExclusiveCfaByFragment) { |
| ExclusivityMap exclusivityMap = new ExclusivityMap(); |
| exclusivityMap.compute(exclusiveFragments, completeCfa, notExclusiveCfaByFragment); |
| return exclusivityMap; |
| } |
| |
| /** |
| * <p> |
| * Patch up the fragment map to satisfy load-order dependencies, as described |
| * in the comment of {@link LivenessPredicate}. |
| * Load-order dependencies can be |
| * violated when an atom is mapped to 0 as a leftover, but it has some |
| * load-order dependency on an atom that was put in an exclusive fragment. |
| * </p> |
| * |
| * <p> |
| * In general, it might be possible to split things better by considering load |
| * order dependencies when building the fragment map. However, fixing them |
| * after the fact makes CodeSplitter simpler. In practice, for programs tried |
| * so far, there are very few load order dependency fixups that actually |
| * happen, so it seems better to keep the compiler simpler. |
| * </p> |
| * |
| * <p> |
| * It would be safer and more robust to include the load order dependencies |
| * in the general scheme and uniformly use control flow analysis to determine |
| * dependencies instead of hand picking atoms to check and fix. Also note that |
| * some of the control flow and load dependencies are introduced as the Java |
| * AST is translated into JavaScript and hence not visible by ControlFlowAnalyzer. |
| * </p> |
| * |
| * <p> |
| * Furthermore, in some cases actual dependencies <i>differ</i> between Java AST and the |
| * final JavaScript output. For example whether a field initialization is done at declaration |
| * or during instance creation decided by |
| * {@link GenerateJavaScriptAST.GenerateJavaScriptVisitor#initializeAtTopScope}. Mismatches |
| * like these are handled explicitly by these fixup passes. |
| * </p> |
| */ |
| public void fixUpLoadOrderDependencies(TreeLogger logger, JProgram jprogram, |
| Set<JMethod> methodsStillInJavaScript) { |
| fixUpLoadOrderDependenciesForMethods(logger, jprogram, methodsStillInJavaScript); |
| fixUpLoadOrderDependenciesForTypes(logger, jprogram); |
| fixUpLoadOrderDependenciesForClassLiterals(logger, jprogram, methodsStillInJavaScript); |
| } |
| |
| /** |
| * Map atoms to exclusive fragments. Do this by trying to find code atoms that |
| * are only needed by a single split point. Such code can be moved to the |
| * exclusively live fragment associated with that split point. |
| */ |
| private void compute(Collection<Fragment> exclusiveFragments, ControlFlowAnalyzer completeCfa, |
| Map<Fragment, ControlFlowAnalyzer> notExclusiveCfaByFragment) { |
| |
| Set<JField> allLiveFields = filter(Sets.union(completeCfa.getLiveFieldsAndMethods(), |
| completeCfa.getFieldsWritten()), JField.class); |
| Set<JMethod> allLiveMethods = filter(completeCfa.getLiveFieldsAndMethods(), JMethod.class); |
| Set<JDeclaredType> allLiveTypes = |
| filter(completeCfa.getInstantiatedTypes(), JDeclaredType.class); |
| |
| for (Fragment fragment : exclusiveFragments) { |
| assert fragment.isExclusive(); |
| ControlFlowAnalyzer complementCfa = notExclusiveCfaByFragment.get(fragment); |
| Set<JNode> nodesNotExclusiveToFragment = Sets.union(complementCfa.getLiveFieldsAndMethods(), |
| complementCfa.getFieldsWritten()); |
| |
| putIfAbsent(fragmentForField, fragment, |
| Sets.difference(allLiveFields, nodesNotExclusiveToFragment)); |
| putIfAbsent(fragmentForMethod, fragment, |
| Sets.difference(allLiveMethods, complementCfa.getLiveFieldsAndMethods())); |
| putIfAbsent(fragmentForType, fragment, |
| Sets.difference(allLiveTypes, |
| filter(complementCfa.getInstantiatedTypes(), JDeclaredType.class))); |
| } |
| |
| // Assign all living atoms to left overs. |
| putIfAbsent(fragmentForField, NOT_EXCLUSIVE, allLiveFields); |
| putIfAbsent(fragmentForMethod, NOT_EXCLUSIVE, allLiveMethods); |
| putIfAbsent(fragmentForType, NOT_EXCLUSIVE, allLiveTypes); |
| } |
| |
| /** |
| * A class literal cannot be loaded until all the parameters to its createFor... class are. |
| * Make sure that the strings are available for all class literals at the time they are |
| * loaded and make sure that superclass class literals are loaded before. |
| */ |
| private void fixUpLoadOrderDependenciesForClassLiterals( |
| TreeLogger logger, JProgram jprogram, Set<JMethod> methodsStillInJavaScript) { |
| int numFixups = 0; |
| /** |
| * Consider all static fields of ClassLiteralHolder; the majority if not all its static |
| * fields are class literal fields. It is safe to fix up extra fields. |
| */ |
| Queue<JField> potentialClassLiteralFields = new ArrayDeque<JField>( |
| jprogram.getTypeClassLiteralHolder().getFields()); |
| int numClassLiterals = potentialClassLiteralFields.size(); |
| |
| while (!potentialClassLiteralFields.isEmpty()) { |
| JField field = potentialClassLiteralFields.remove(); |
| if (!field.isStatic()) { |
| continue; |
| } |
| |
| Fragment classLiteralFragment = fragmentForField.get(field); |
| |
| // In -XenableClosureFormat creation of class literals needs to happen before or with class |
| // definition. This fixup takes care when it is not the case. |
| JType type = jprogram.getTypeByClassLiteralField(field); |
| Fragment classLiteralTypeFragment = fragmentForType.get(type); |
| if (!canReferenceAtomsFrom(classLiteralTypeFragment, classLiteralFragment)) { |
| numFixups++; |
| fragmentForField.put(field, NOT_EXCLUSIVE); |
| classLiteralFragment = NOT_EXCLUSIVE; |
| } |
| |
| JExpression initializer = field.getInitializer(); |
| |
| // Fixup the superclass class literals. |
| for (JClassLiteral superclassClassLiteral : classLiteralsIn(initializer)) { |
| JField superclassClassLiteralField = superclassClassLiteral.getField(); |
| // Fix the super class literal and add it to the reexamined. |
| Fragment superclassClassLiteralFragment = fragmentForField.get(superclassClassLiteralField); |
| if (!canReferenceAtomsFrom(classLiteralFragment, superclassClassLiteralFragment)) { |
| numFixups++; |
| fragmentForField.put(superclassClassLiteralField, NOT_EXCLUSIVE); |
| // Add the field back so that its superclass class literal gets fixed if necessary. |
| potentialClassLiteralFields.add(superclassClassLiteralField); |
| } |
| } |
| |
| // If there are references to methods move those as well. In particular the enum class |
| // literals reference the static methods values() and valueOf() for the particular enum type |
| // those methods need to be defined before the class literal. |
| for (JMethod referencedMethod : methodsReferencesIn(initializer)) { |
| // Move the referenced methods if necessary. |
| Fragment referencedMethodFragment = fragmentForMethod.get(referencedMethod); |
| if (methodsStillInJavaScript.contains(referencedMethod) |
| && !canReferenceAtomsFrom(classLiteralFragment, referencedMethodFragment)) { |
| assert referencedMethod.isStatic(); |
| numFixups++; |
| fragmentForMethod.put(referencedMethod, NOT_EXCLUSIVE); |
| } |
| } |
| } |
| |
| logger.log(TreeLogger.DEBUG, "Fixed up load-order dependencies by moving " + |
| numFixups + " fields in class literal constructors to fragment 0, out of " + |
| numClassLiterals); |
| } |
| |
| /** |
| * Fixes up the load-order dependencies from instance methods to their enclosing types, in some |
| * cases there is some freedom to place instance methods in one of two or more exclusive |
| * fragment. That scenario arises when an instance method is only accessible after two or |
| * more exclusive fragments have been loaded. In such scenario this fixup will move the method |
| * to the fragment where the type is instantiated. |
| */ |
| private void fixUpLoadOrderDependenciesForMethods(TreeLogger logger, JProgram jprogram, |
| Set<JMethod> methodsStillInJavaScript) { |
| int numFixups = 0; |
| |
| for (JDeclaredType type : jprogram.getDeclaredTypes()) { |
| Fragment typeFrag = fragmentForType.get(type); |
| if (typeFrag == null || !typeFrag.isExclusive()) { |
| continue; |
| } |
| /* |
| * If the type is in an exclusive fragment, all its instance methods must be in the same one; |
| * if this is not the case move the type to the NOT_EXCLUSIVE fragment. |
| */ |
| for (JMethod method : type.getMethods()) { |
| if (method.needsDynamicDispatch() && methodsStillInJavaScript.contains(method) |
| && typeFrag != fragmentForMethod.get(method)) { |
| fragmentForType.put(type, NOT_EXCLUSIVE); |
| numFixups++; |
| break; |
| } |
| } |
| } |
| |
| logger.log(TreeLogger.DEBUG, |
| "Fixed up load-order dependencies for instance methods by moving " + numFixups |
| + " types to fragment 0, out of " + jprogram.getDeclaredTypes().size()); |
| } |
| |
| /** |
| * Fixes up load order dependencies from types to their supertypes. |
| */ |
| private void fixUpLoadOrderDependenciesForTypes(TreeLogger logger, JProgram jprogram) { |
| int numFixups = 0; |
| Queue<JDeclaredType> typesToCheck = |
| new ArrayDeque<JDeclaredType>(jprogram.getDeclaredTypes().size()); |
| typesToCheck.addAll(jprogram.getDeclaredTypes()); |
| |
| while (!typesToCheck.isEmpty()) { |
| JDeclaredType type = typesToCheck.remove(); |
| if (type.getSuperClass() != null) { |
| Fragment typeFrag = fragmentForType.get(type); |
| Fragment supertypeFrag = fragmentForType.get(type.getSuperClass()); |
| if (!canReferenceAtomsFrom(typeFrag, supertypeFrag)) { |
| numFixups++; |
| fragmentForType.put(type.getSuperClass(), NOT_EXCLUSIVE); |
| typesToCheck.add(type.getSuperClass()); |
| } |
| } |
| } |
| |
| logger.log(TreeLogger.DEBUG, "Fixed up load-order dependencies on supertypes by moving " |
| + numFixups + " types to fragment 0, out of " + jprogram.getDeclaredTypes().size()); |
| } |
| |
| private static <T> Set<T> filter(Set<?> types, Class<T> clazz) { |
| return (Set) Sets.filter(types, Predicates.instanceOf(clazz)); |
| } |
| |
| /** |
| * Returns true if atoms in thatFragment are visible from thisFragment. |
| */ |
| private static boolean canReferenceAtomsFrom(Fragment thisFragment, Fragment thatFragment) { |
| return thisFragment == null || thisFragment == thatFragment || !thatFragment.isExclusive(); |
| } |
| |
| /** |
| * An atom is live in a fragment if either it is exclusive to that fragment or not exclusive |
| * to any fragment. |
| */ |
| private static <T> boolean isLiveInFragment(Map<T, Fragment> map, T atom, |
| Fragment expectedFragment) { |
| Fragment actualFragment = map.get(atom); |
| return actualFragment != null && |
| (expectedFragment == actualFragment || !actualFragment.isExclusive()); |
| } |
| |
| private <T> void putIfAbsent(Map<T, Fragment> map, Fragment fragment, Iterable<T> atoms) { |
| for (T atom : atoms) { |
| if (!map.containsKey(atom)) { |
| // Some atoms might atoms might be dead until both split points i and j are reached, and |
| // thus they could be assigned to either. |
| // We choose here to assign to the first fragment, so that we could use this method |
| // to assign leftovers. |
| map.put(atom, fragment); |
| } |
| } |
| } |
| } |