com.google.gwt.dev.jjs.impl.codesplitter.ExclusivityMap Maven / Gradle / Ivy
/*
* 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.JReferenceType;
import com.google.gwt.dev.jjs.ast.JRunAsync;
import com.google.gwt.dev.jjs.ast.JStringLiteral;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.util.collect.HashMap;
import com.google.gwt.dev.util.collect.HashSet;
import com.google.gwt.thirdparty.guava.common.base.Predicates;
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;
}
public boolean isLive(JDeclaredType type) {
return isLiveInFragment(fragment, type);
}
public boolean isLive(JField field) {
return isLiveInFragment(fragment, field);
}
public boolean isLive(JMethod method) {
return isLiveInFragment(fragment, method);
}
public boolean isLive(String literal) {
return isLiveInFragment(fragment, literal);
}
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 getStatements() {
throw makeUnsupportedException("getStatements");
}
@Override
public void setStatements(List statements) {
throw makeUnsupportedException("setStatements");
}
@Override
public void addStatements(List statements) {
throw makeUnsupportedException("addStatements");
}
@Override
public Set 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 string is live in a fragment.
*/
public boolean isLiveInFragment(Fragment fragment, String string) {
return isLiveInFragment(fragmentForString, string, fragment);
}
/**
* Determine whether a type is live in a fragment.
*/
public boolean isLiveInFragment(Fragment fragment, JDeclaredType type) {
return isLiveInFragment(fragmentForType, type, fragment);
}
/**
* Update fragment map so that fields that are not in notExclusiveAtoms are assigned to
* the specified fragment.
*/
public void updateFields(Fragment fragment, Set> notExclusiveAtoms, Iterable fields) {
updateMap(fragment, fragmentForField, notExclusiveAtoms, fields);
}
/**
* Update fragment map so that methods that are not in notExclusiveAtoms are assigned to
* the specified fragment.
*/
public void updateMethods(Fragment fragment, Set> notExclusiveAtoms,
Iterable methods) {
updateMap(fragment, fragmentForMethod, notExclusiveAtoms, methods);
}
/**
* Update fragment map so that strings that are not in notExclusiveAtoms are assigned to
* the specified fragment.
*/
public void updateStrings(Fragment fragment, Set> notExclusiveAtoms, Iterable strings) {
updateMap(fragment, fragmentForString, notExclusiveAtoms, strings);
}
/**
* Update fragment map so that types that are not in notExclusiveAtoms are assigned to
* the specified fragment.
*/
public void updateTypes(Fragment fragment, Set> notExclusiveAtoms,
Iterable types) {
updateMap(fragment, fragmentForType, notExclusiveAtoms, types);
}
private Map fragmentForField = new HashMap();
private Map fragmentForMethod = new HashMap();
private Map fragmentForString = new HashMap();
private Map fragmentForType = new HashMap();
private Set allFields = Sets.newHashSet();
private Set allMethods = Sets.newHashSet();
/**
* Traverse {@code exp} and find all referenced JFields.
*/
private static Set classLiteralsIn(JExpression exp) {
final Set literals = new HashSet();
class ClassLiteralFinder extends JVisitor {
@Override
public void endVisit(JClassLiteral classLiteral, Context ctx) {
literals.add(classLiteral);
}
}
(new ClassLiteralFinder()).accept(exp);
return literals;
}
/**
* 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 exclusiveFragments,
ControlFlowAnalyzer completeCfa, Map notLiveCfaByFragment) {
ExclusivityMap exclusivityMap = new ExclusivityMap();
exclusivityMap.compute(exclusiveFragments, completeCfa, notLiveCfaByFragment);
return exclusivityMap;
}
/**
*
* 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.
*
*
*
* 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.
*
*
*
* 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.
*
*
*
* Furthermore, in some cases actual dependencies differ 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.
*
*/
public void fixUpLoadOrderDependencies(TreeLogger logger, JProgram jprogram,
Set methodsInJavaScript) {
fixUpLoadOrderDependenciesForMethods(logger, jprogram, methodsInJavaScript);
fixUpLoadOrderDependenciesForTypes(logger, jprogram);
fixUpLoadOrderDependenciesForClassLiterals(logger, jprogram);
fixUpLoadOrderDependenciesForFieldsInitializedToStrings(logger, jprogram);
}
/**
* 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 exclusiveFragments,
ControlFlowAnalyzer completeCfa, Map notLiveCfaByFragment) {
for (JNode node : completeCfa.getLiveFieldsAndMethods()) {
if (node instanceof JField) {
allFields.add((JField) node);
}
if (node instanceof JMethod) {
allMethods.add((JMethod) node);
}
}
allFields.addAll(completeCfa.getFieldsWritten());
for (Fragment fragment : exclusiveFragments) {
assert fragment.isExclusive();
ControlFlowAnalyzer notLiveInFragment = notLiveCfaByFragment.get(fragment);
Set allLiveNodes =
Sets.union(notLiveInFragment.getLiveFieldsAndMethods(),
notLiveInFragment.getFieldsWritten());
updateFields(fragment, allLiveNodes, allFields);
updateMethods(fragment, notLiveInFragment.getLiveFieldsAndMethods(),
allMethods);
updateStrings(fragment, notLiveInFragment.getLiveStrings(), completeCfa
.getLiveStrings());
updateTypes(fragment,
declaredTypesIn(notLiveInFragment.getInstantiatedTypes()),
declaredTypesIn(completeCfa.getInstantiatedTypes()));
}
}
/**
* 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) {
int numClassLitStrings = 0;
int numFixups = 0;
int numClassLiteralFixups = 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 potentialClassLiteralFields = new ArrayDeque(
jprogram.getTypeClassLiteralHolder().getFields());
int numClassLiterals = potentialClassLiteralFields.size();
while (!potentialClassLiteralFields.isEmpty()) {
JField field = potentialClassLiteralFields.remove();
if (!field.isStatic()) {
continue;
}
Fragment classLiteralFragment = getFragment(fragmentForField, field);
JExpression initializer = field.getInitializer();
// Fixup the string literals.
for (String string : stringsIn(initializer)) {
numClassLitStrings++;
Fragment stringFrag = getFragment(fragmentForString, string);
if (!fragmentsAreConsistent(classLiteralFragment, stringFrag)) {
numFixups++;
fragmentForString.put(string, NOT_EXCLUSIVE);
}
}
// Fixup the class literals.
for (JClassLiteral superclassClassLiteral : classLiteralsIn(initializer)) {
JField superclassClassLiteralField = superclassClassLiteral.getField();
// Fix the super class literal and add it to the reexamined.
Fragment superclassClassLiteralFragment = getFragment(fragmentForField,
superclassClassLiteralField);
if (!fragmentsAreConsistent(classLiteralFragment, superclassClassLiteralFragment)) {
numClassLiteralFixups++;
fragmentForField.put(superclassClassLiteralField, NOT_EXCLUSIVE);
// Add the field back so that its superclass classliteral gets fixed if necessary.
potentialClassLiteralFields.add(superclassClassLiteralField);
}
}
}
logger.log(TreeLogger.DEBUG, "Fixed up load-order dependencies by moving " + numFixups
+ " strings in class literal constructors to fragment 0, out of " + numClassLitStrings);
logger.log(TreeLogger.DEBUG, "Fixed up load-order dependencies by moving " +
numClassLiteralFixups + " fields in class literal constructors to fragment 0, out of " +
numClassLiterals);
}
/**
* Fixup string literals that appear in field initializers.
*
* GenerateJavaScriptAST decides whether a field will be initialized at the declaration or
* by the instance/class initialer when lowering to JavasScript.
*
*
Only literals are affeced and only string literals are relevant for code splitting.
*/
private void fixUpLoadOrderDependenciesForFieldsInitializedToStrings(TreeLogger logger,
JProgram jprogram) {
final int[] numFixups = new int[1];
final int[] numFieldStrings = new int[1];
(new JVisitor() {
@Override
public void endVisit(JField field, Context ctx) {
if (field.getInitializer() instanceof JStringLiteral) {
numFieldStrings[0]++;
String string = ((JStringLiteral) field.getInitializer()).getValue();
Fragment fieldFrag = getFragment(fragmentForField, field);
Fragment stringFrag = getFragment(fragmentForString, string);
if (!fragmentsAreConsistent(fieldFrag, stringFrag)) {
numFixups[0]++;
fragmentForString.put(string, NOT_EXCLUSIVE);
}
}
}
}).accept(jprogram);
logger.log(TreeLogger.DEBUG, "Fixed up load-order dependencies by moving " + numFixups[0]
+ " strings used to initialize fields to fragment 0, out of " + numFieldStrings[0]);
}
/**
* Fixes up the load-order dependencies from instance methods to their enclosing types.
*/
private void fixUpLoadOrderDependenciesForMethods(TreeLogger logger, JProgram jprogram,
Set methodsInJavaScript) {
int numFixups = 0;
for (JDeclaredType type : jprogram.getDeclaredTypes()) {
Fragment typeFrag = getFragment(fragmentForType, type);
if (!typeFrag.isExclusive()) {
continue;
}
/*
* If the type is in an exclusive fragment, all its instance methods
* must be in the same one.
*/
for (JMethod method : type.getMethods()) {
if (method.needsVtable() && methodsInJavaScript.contains(method)
&& typeFrag != getFragment(fragmentForMethod, 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 typesToCheck =
new ArrayDeque(jprogram.getDeclaredTypes().size());
typesToCheck.addAll(jprogram.getDeclaredTypes());
while (!typesToCheck.isEmpty()) {
JDeclaredType type = typesToCheck.remove();
if (type.getSuperClass() != null) {
Fragment typeFrag = getFragment(fragmentForType, type);
Fragment supertypeFrag = getFragment(fragmentForType, type.getSuperClass());
if (!fragmentsAreConsistent(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());
}
/**
* Extract the types from a set that happen to be declared types.
*/
private static Set declaredTypesIn(Set types) {
return (Set) Sets.filter(types, Predicates.instanceOf(JDeclaredType.class));
}
/**
* Returns true if atoms in thatStatement are visible from thisFragment.
*/
private static boolean fragmentsAreConsistent(Fragment thisFragment, Fragment thatFragment) {
return thisFragment == thatFragment || !thatFragment.isExclusive();
}
private static Fragment getFragment(Map fragmentForAtom, T atom) {
Fragment fragment = fragmentForAtom.get(atom);
return (fragment == null) ? NOT_EXCLUSIVE : fragment;
}
/**
* An atom is live in a fragment if either it is exclusive to that fragment or not exclusive
* to any fragment.
*/
private static boolean isLiveInFragment(Map map, T atom,
Fragment expectedFragment) {
Fragment actualFragment = getFragment(map, atom);
return (expectedFragment == actualFragment || !actualFragment.isExclusive());
}
/**
* Traverse {@code exp} and find all string literals within it.
*/
private static Set stringsIn(JExpression exp) {
final Set strings = new HashSet();
class StringFinder extends JVisitor {
@Override
public void endVisit(JStringLiteral stringLiteral, Context ctx) {
strings.add(stringLiteral.getValue());
}
}
(new StringFinder()).accept(exp);
return strings;
}
private void updateMap(Fragment fragment, Map map, Set> notExclusiveAtoms,
Iterable atoms) {
for (T atom : atoms) {
if (!notExclusiveAtoms.contains(atom)) {
/*
* Note that it is fine to overwrite a preexisting entry in the map. If
* an atom is dead until split point i has been reached, and is also
* dead until entry j has been reached, then it is dead until both have
* been reached. Thus, it can be downloaded along with either i's or j's
* code.
*/
map.put(atom, fragment);
}
}
}
}