| /* |
| * 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.dev.jjs.SourceInfo; |
| import com.google.gwt.dev.jjs.ast.JClassType; |
| import com.google.gwt.dev.jjs.ast.JConstructor; |
| import com.google.gwt.dev.jjs.ast.JDeclaredType; |
| import com.google.gwt.dev.jjs.ast.JField; |
| import com.google.gwt.dev.jjs.ast.JMethod; |
| import com.google.gwt.dev.jjs.ast.JProgram; |
| import com.google.gwt.dev.jjs.ast.RuntimeConstants; |
| import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap; |
| import com.google.gwt.dev.js.JsSafeCloner.Cloner; |
| import com.google.gwt.dev.js.JsUtils; |
| import com.google.gwt.dev.js.ast.JsBinaryOperation; |
| import com.google.gwt.dev.js.ast.JsBinaryOperator; |
| import com.google.gwt.dev.js.ast.JsContext; |
| import com.google.gwt.dev.js.ast.JsEmpty; |
| import com.google.gwt.dev.js.ast.JsExprStmt; |
| import com.google.gwt.dev.js.ast.JsExpression; |
| import com.google.gwt.dev.js.ast.JsInvocation; |
| import com.google.gwt.dev.js.ast.JsModVisitor; |
| import com.google.gwt.dev.js.ast.JsName; |
| import com.google.gwt.dev.js.ast.JsNameRef; |
| import com.google.gwt.dev.js.ast.JsNumberLiteral; |
| import com.google.gwt.dev.js.ast.JsProgram; |
| import com.google.gwt.dev.js.ast.JsStatement; |
| import com.google.gwt.dev.js.ast.JsVars; |
| import com.google.gwt.dev.js.ast.JsVars.JsVar; |
| import com.google.gwt.dev.js.ast.JsVisitable; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Extracts multiple JS statements (called a fragment) out of the complete JS program based on |
| * supplied type/method/field/string liveness conditions. |
| * |
| * <p> |
| * <b>Liveness as defined here is not an intuitive concept.</b> A type or method (note that |
| * constructors are methods) is considered live for the current fragment when that type can only be |
| * instantiated or method executed when the current fragment has already been loaded. That does not |
| * always mean that it was caused by direct execution of the current fragment. It may instead mean |
| * that direction execution of some other fragment has been affected by the loading of the current |
| * fragment in a way that results in the instantiation of the type or execution of the method. It is |
| * this second case that can lead to seemingly contradictory but valid situations like having a type |
| * which is not currently live but which has a currently live constructor. For example it might be |
| * possible to instantiate type Foo even with fragment Bar being loaded (i.e. Foo is not live for |
| * Bar) but the loading of fragment Bar might be required to reach a particular one of Bar's |
| * multiple constructor (i.e. that constructor is live for Bar). |
| * </p> |
| */ |
| public class FragmentExtractor { |
| |
| /** |
| * A logger for statements that the fragment extractor encounters. Install one using |
| * {@link FragmentExtractor#setStatementLogger(StatementLogger)} . |
| */ |
| public interface StatementLogger { |
| void log(JsStatement statement, boolean include); |
| } |
| |
| /** |
| * Mutates the provided defineClass statement to remove references to constructors which have not |
| * been made live by the current fragment. It also counts the constructor references that |
| * were not removed. |
| */ |
| private class DefineClassMinimizerVisitor extends JsModVisitor { |
| |
| private final LivenessPredicate alreadyLoadedPredicate; |
| private final LivenessPredicate livenessPredicate; |
| private int liveConstructorCount; |
| |
| private DefineClassMinimizerVisitor( |
| LivenessPredicate alreadyLoadedPredicate, LivenessPredicate livenessPredicate) { |
| this.alreadyLoadedPredicate = alreadyLoadedPredicate; |
| this.livenessPredicate = livenessPredicate; |
| } |
| |
| @Override |
| public void endVisit(JsNameRef x, JsContext ctx) { |
| // Removes constructor references from defineClass parameters. |
| // These references can be either be originated by JConstructors or |
| // (in closure formatted code) by JClassTypes. |
| JClassType classType = map.nameToType(x.getName()); |
| JMethod method = map.nameToMethod(x.getName()); |
| method = method instanceof JConstructor ? method : null; |
| if (classType == null && method == null) { |
| // Regular argument to defineClass, ignore; |
| return; |
| } |
| assert classType != null || method != null; |
| |
| boolean isConstructorLive = method != null ? |
| !alreadyLoadedPredicate.isLive(method) && livenessPredicate.isLive(method) : |
| !alreadyLoadedPredicate.isLive(classType) && livenessPredicate.isLive(classType); |
| if (isConstructorLive) { |
| // Constructor is live in current fragment. |
| // Counts kept references to live constructors. |
| liveConstructorCount++; |
| } else { |
| // Removes references to dead constructors. |
| ctx.removeMe(); |
| } |
| } |
| |
| /** |
| * Enables varargs mutation. |
| */ |
| @Override |
| protected <T extends JsVisitable> void doAcceptList(List<T> collection) { |
| doAcceptWithInsertRemove(collection); |
| } |
| } |
| |
| private static class MinimalDefineClassResult { |
| |
| private int liveConstructorCount; |
| private JsExprStmt statement; |
| |
| public MinimalDefineClassResult(JsExprStmt statement, int liveConstructorCount) { |
| this.statement = statement; |
| this.liveConstructorCount = liveConstructorCount; |
| } |
| } |
| |
| private static JsExprStmt createDefineClassClone(JsExprStmt defineClassStatement) { |
| Cloner cloner = new Cloner(); |
| cloner.accept(defineClassStatement.getExpression()); |
| JsExprStmt minimalDefineClassStatement = cloner.getExpression().makeStmt(); |
| return minimalDefineClassStatement; |
| } |
| |
| private final JsProgram jsprogram; |
| |
| private final JavaToJavaScriptMap map; |
| |
| private final JsName asyncFragmentLoaderOnLoadFnName; |
| private final JsName defineClassFnName; |
| |
| private StatementLogger statementLogger = new StatementLogger() { |
| @Override |
| public void log(JsStatement statement, boolean include) { |
| } |
| }; |
| |
| public FragmentExtractor(JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap map) { |
| this(jsprogram, map, |
| JsUtils.getJsNameForMethod(map, jprogram, RuntimeConstants.ASYNC_FRAGMENT_LOADER_ON_LOAD), |
| JsUtils.getJsNameForMethod(map, jprogram, (RuntimeConstants.RUNTIME_DEFINE_CLASS))); |
| } |
| |
| public FragmentExtractor(JsProgram jsprogram, JavaToJavaScriptMap map, |
| JsName asyncFragmentLoaderOnLoadFnName, JsName defineClassFnName) { |
| this.jsprogram = jsprogram; |
| this.map = map; |
| this.asyncFragmentLoaderOnLoadFnName = asyncFragmentLoaderOnLoadFnName; |
| this.defineClassFnName = defineClassFnName; |
| } |
| |
| /** |
| * Create a call to {@link AsyncFragmentLoader#onLoad}. |
| */ |
| public List<JsStatement> createOnLoadedCall(int fragmentId) { |
| SourceInfo sourceInfo = jsprogram.getSourceInfo(); |
| JsInvocation call = new JsInvocation(sourceInfo); |
| call.setQualifier(wrapWithEntry(asyncFragmentLoaderOnLoadFnName.makeRef(sourceInfo))); |
| call.getArguments().add(new JsNumberLiteral(sourceInfo, fragmentId)); |
| List<JsStatement> newStats = Collections.<JsStatement> singletonList(call.makeStmt()); |
| return newStats; |
| } |
| |
| /** |
| * Assume that all code described by <code>alreadyLoadedPredicate</code> has |
| * been downloaded. Extract enough JavaScript statements that the code |
| * described by <code>livenessPredicate</code> can also run. The caller should |
| * ensure that <code>livenessPredicate</code> includes strictly more live code |
| * than <code>alreadyLoadedPredicate</code>. |
| */ |
| public List<JsStatement> extractStatements( |
| LivenessPredicate livenessPredicate, LivenessPredicate alreadyLoadedPredicate) { |
| List<JsStatement> extractedStats = new ArrayList<JsStatement>(); |
| |
| /** |
| * The type whose vtables can currently be installed. |
| */ |
| JDeclaredType currentVtableType = null; |
| JDeclaredType pendingVtableType = null; |
| JsExprStmt pendingDefineClass = null; |
| |
| List<JsStatement> statements = jsprogram.getGlobalBlock().getStatements(); |
| for (JsStatement statement : statements) { |
| |
| boolean keep; |
| JDeclaredType vtableTypeAssigned = vtableTypeAssigned(statement); |
| if (vtableTypeAssigned != null) { |
| // Keeps defineClass statements of live types or types with a live constructor. |
| MinimalDefineClassResult minimalDefineClassResult = createMinimalDefineClass( |
| livenessPredicate, alreadyLoadedPredicate, (JsExprStmt) statement); |
| boolean liveType = !alreadyLoadedPredicate.isLive(vtableTypeAssigned) |
| && livenessPredicate.isLive(vtableTypeAssigned); |
| boolean liveConstructors = minimalDefineClassResult.liveConstructorCount > 0; |
| |
| if (liveConstructors || liveType) { |
| statement = minimalDefineClassResult.statement; |
| keep = true; |
| } else { |
| pendingDefineClass = minimalDefineClassResult.statement; |
| pendingVtableType = vtableTypeAssigned; |
| keep = false; |
| } |
| } else if (containsRemovableVars(statement)) { |
| statement = removeSomeVars((JsVars) statement, livenessPredicate, alreadyLoadedPredicate); |
| keep = !(statement instanceof JsEmpty); |
| } else { |
| keep = isLive(statement, livenessPredicate) && !isLive(statement, alreadyLoadedPredicate); |
| } |
| |
| statementLogger.log(statement, keep); |
| |
| if (keep) { |
| if (vtableTypeAssigned != null) { |
| currentVtableType = vtableTypeAssigned; |
| } |
| JDeclaredType vtableType = vtableTypeNeeded(statement); |
| if (vtableType != null && vtableType != currentVtableType) { |
| // there is no defineClass() call in -XclosureFormattedOutput |
| assert pendingVtableType == vtableType || pendingDefineClass == null; |
| if (pendingDefineClass != null) { |
| extractedStats.add(pendingDefineClass); |
| } |
| currentVtableType = pendingVtableType; |
| pendingDefineClass = null; |
| pendingVtableType = null; |
| } |
| extractedStats.add(statement); |
| } |
| } |
| |
| return extractedStats; |
| } |
| |
| /** |
| * Find all Java methods that still exist in the resulting JavaScript, even |
| * after JavaScript inlining and pruning. |
| */ |
| public Set<JMethod> findAllMethodsStillInJavaScript() { |
| Set<JMethod> methodsInJs = new HashSet<JMethod>(); |
| for (int fragment = 0; fragment < jsprogram.getFragmentCount(); fragment++) { |
| for (JsStatement statement : jsprogram.getFragmentBlock(fragment).getStatements()) { |
| JMethod method = map.methodForStatement(statement); |
| if (method != null) { |
| methodsInJs.add(method); |
| } |
| } |
| } |
| return methodsInJs; |
| } |
| |
| public void setStatementLogger(StatementLogger logger) { |
| statementLogger = logger; |
| } |
| |
| /** |
| * Check whether this statement is a {@link JsVars} that contains individual vars that could be |
| * removed. If it does, then {@link #removeSomeVars(JsVars, LivenessPredicate, LivenessPredicate)} |
| * is sensible for this statement and should be used instead of |
| * {@link #isLive(JsStatement, LivenessPredicate)} . |
| */ |
| private boolean containsRemovableVars(JsStatement statement) { |
| if (statement instanceof JsVars) { |
| for (JsVar var : (JsVars) statement) { |
| |
| JField field = map.nameToField(var.getName()); |
| if (field != null) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * DefineClass calls mark the existence of a class and associate a castMaps with the class's |
| * various constructors. These multiple constructors are provided as JsNameRef varargs to the |
| * defineClass call but only the constructors that are live in the current fragment should be |
| * included. |
| * |
| * <p> |
| * This function strips out the dead constructors and returns the modified defineClass call. The |
| * stripped constructors will be kept by other defineClass calls in other fragments at other times. |
| * </p> |
| */ |
| private MinimalDefineClassResult createMinimalDefineClass(LivenessPredicate livenessPredicate, |
| LivenessPredicate alreadyLoadedPredicate, JsExprStmt defineClassStatement) { |
| DefineClassMinimizerVisitor defineClassMinimizerVisitor = |
| new DefineClassMinimizerVisitor(alreadyLoadedPredicate, livenessPredicate); |
| JsExprStmt minimalDefineClassStatement = createDefineClassClone(defineClassStatement); |
| defineClassMinimizerVisitor.accept(minimalDefineClassStatement); |
| return new MinimalDefineClassResult( |
| minimalDefineClassStatement, defineClassMinimizerVisitor.liveConstructorCount); |
| } |
| |
| private boolean isLive(JsStatement statement, LivenessPredicate livenessPredicate) { |
| JDeclaredType type = map.typeForStatement(statement); |
| if (type != null) { |
| // This is part of the code only needed once a type is instantiable |
| return livenessPredicate.isLive(type); |
| } |
| |
| JMethod method = map.methodForStatement(statement); |
| |
| if (method != null) { |
| /* |
| * This statement either defines a method or installs it in a vtable. |
| */ |
| if (!livenessPredicate.isLive(method)) { |
| // The method is not live. Skip it. |
| return false; |
| } |
| // The method is live. Check that its enclosing type is instantiable. |
| // TODO(spoon): this check should not be needed once the CFA is updated |
| return !method.needsDynamicDispatch() || livenessPredicate.isLive(method.getEnclosingType()); |
| } |
| |
| return livenessPredicate.miscellaneousStatementsAreLive(); |
| } |
| |
| /** |
| * Check whether a variable is needed. If the variable is an intern variable, |
| * then return whether the interned value is live. If the variable corresponds |
| * to a Java field, then return whether the Java field is live. Otherwise, |
| * assume the variable is needed and return <code>true</code>. |
| * |
| * Whenever this method is updated, also look at |
| * {@link #containsRemovableVars(JsStatement)}. |
| */ |
| private boolean isLive(JsVar var, LivenessPredicate livenessPredicate) { |
| JField field = map.nameToField(var.getName()); |
| if (field != null) { |
| // It's a field |
| return livenessPredicate.isLive(field); |
| } |
| |
| // It's not an intern variable at all |
| return livenessPredicate.miscellaneousStatementsAreLive(); |
| } |
| |
| /** |
| * If stat is a {@link JsVars} that initializes a bunch of intern vars, return |
| * a modified statement that skips any vars are needed by |
| * <code>currentLivenessPredicate</code> but not by |
| * <code>alreadyLoadedPredicate</code>. |
| */ |
| private JsStatement removeSomeVars(JsVars stat, LivenessPredicate currentLivenessPredicate, |
| LivenessPredicate alreadyLoadedPredicate) { |
| JsVars newVars = new JsVars(stat.getSourceInfo()); |
| |
| for (JsVar var : stat) { |
| if (isLive(var, currentLivenessPredicate) && !isLive(var, alreadyLoadedPredicate)) { |
| newVars.add(var); |
| } |
| } |
| |
| if (newVars.getNumVars() == stat.getNumVars()) { |
| // no change |
| return stat; |
| } |
| |
| if (newVars.iterator().hasNext()) { |
| /* |
| * The new variables are non-empty; return them. |
| */ |
| return newVars; |
| } else { |
| /* |
| * An empty JsVars seems possibly surprising; return a true empty |
| * statement instead. |
| */ |
| return new JsEmpty(stat.getSourceInfo()); |
| } |
| } |
| |
| /** |
| * If <code>state</code> is of the form <code>_ = String.prototype</code>, |
| * then return <code>String</code>. If the form is |
| * <code>defineClass(id, superId, cTM, ctor1, ctor2, ...)</code> return the type |
| * corresponding to that id. Otherwise return <code>null</code>. |
| */ |
| private JDeclaredType vtableTypeAssigned(JsStatement statement) { |
| if (!(statement instanceof JsExprStmt)) { |
| return null; |
| } |
| JsExprStmt expr = (JsExprStmt) statement; |
| if (expr.getExpression() instanceof JsInvocation) { |
| // Handle a defineClass call. |
| JsInvocation call = (JsInvocation) expr.getExpression(); |
| if (!(call.getQualifier() instanceof JsNameRef)) { |
| return null; |
| } |
| JsNameRef func = (JsNameRef) call.getQualifier(); |
| if (func.getName() != defineClassFnName) { |
| return null; |
| } |
| return map.typeForStatement(statement); |
| } |
| |
| // Handle String. |
| if (!(expr.getExpression() instanceof JsBinaryOperation)) { |
| return null; |
| } |
| JsBinaryOperation binExpr = (JsBinaryOperation) expr.getExpression(); |
| if (binExpr.getOperator() != JsBinaryOperator.ASG) { |
| return null; |
| } |
| if (!(binExpr.getArg1() instanceof JsNameRef)) { |
| return null; |
| } |
| JsNameRef lhs = (JsNameRef) binExpr.getArg1(); |
| JsName underBar = jsprogram.getScope().findExistingName("_"); |
| assert underBar != null; |
| if (lhs.getName() != underBar) { |
| return null; |
| } |
| if (!(binExpr.getArg2() instanceof JsNameRef)) { |
| return null; |
| } |
| |
| JsNameRef rhsRef = (JsNameRef) binExpr.getArg2(); |
| if (!(rhsRef.getQualifier() instanceof JsNameRef)) { |
| return null; |
| } |
| if (!((JsNameRef) rhsRef.getQualifier()).getShortIdent().equals("String")) { |
| return null; |
| } |
| |
| if (!rhsRef.getName().getShortIdent().equals("prototype")) { |
| return null; |
| } |
| return map.typeForStatement(statement); |
| } |
| |
| private JDeclaredType vtableTypeNeeded(JsStatement statement) { |
| JMethod method = map.methodForStatement(statement); |
| if (method != null) { |
| if (method.needsDynamicDispatch()) { |
| return method.getEnclosingType(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Wrap an expression with a call to $entry. |
| */ |
| private JsInvocation wrapWithEntry(JsExpression expression) { |
| SourceInfo sourceInfo = expression.getSourceInfo(); |
| JsInvocation call = new JsInvocation(sourceInfo, |
| jsprogram.getScope().findExistingName("$entry").makeRef(sourceInfo), expression); |
| return call; |
| } |
| } |