All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.dev.jjs.impl.codesplitter.FragmentExtractor Maven / Gradle / Ivy

/*
 * 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.JField;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.JsHoister.Cloner;
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.JsFunction;
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.
 *
 * 

* Liveness as defined here is not an intuitive concept. 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). *

*/ public class FragmentExtractor { /** * A logger for statements that the fragment extractor encounters. Install one using * {@link FragmentExtractor#setStatementLogger(StatementLogger)} . */ public static 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) { JMethod method = map.nameToMethod(x.getName()); if (!(method instanceof JConstructor)) { return; } // Only examines references to constructor methods. JConstructor constructor = (JConstructor) method; boolean fragmentExpandsConstructorLiveness = !alreadyLoadedPredicate.isLive(constructor) && livenessPredicate.isLive(constructor); if (fragmentExpandsConstructorLiveness) { // Counts kept references to live constructors. liveConstructorCount++; } else { // Removes references to dead constructors. ctx.removeMe(); } } /** * Enables varargs mutation. */ @Override protected void doAcceptList(List 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 class NullStatementLogger implements StatementLogger { @Override public void log(JsStatement statement, boolean include) { } } /** * Return the Java method corresponding to stat, or * null if there isn't one. It recognizes JavaScript of the form * function foo(...) { ...}, where foo is the name * of the JavaScript translation of a Java method. */ public static JMethod methodFor(JsStatement stat, JavaToJavaScriptMap map) { if (stat instanceof JsExprStmt) { JsExpression exp = ((JsExprStmt) stat).getExpression(); if (exp instanceof JsFunction) { JsFunction func = (JsFunction) exp; if (func.getName() != null) { JMethod method = map.nameToMethod(func.getName()); if (method != null) { return method; } } } } return map.vtableInitToMethod(stat); } private static JsExprStmt createDefineClassClone(JsExprStmt defineClassStatement) { Cloner cloner = new Cloner(); cloner.accept(defineClassStatement.getExpression()); JsExprStmt minimalDefineClassStatement = cloner.getExpression().makeStmt(); return minimalDefineClassStatement; } private final JProgram jprogram; private final JsProgram jsprogram; private final JavaToJavaScriptMap map; private StatementLogger statementLogger = new NullStatementLogger(); public FragmentExtractor(JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap map) { this.jprogram = jprogram; this.jsprogram = jsprogram; this.map = map; } /** * Create a call to {@link AsyncFragmentLoader#onLoad}. */ public List createOnLoadedCall(int fragmentId) { JMethod loadMethod = jprogram.getIndexedMethod("AsyncFragmentLoader.onLoad"); JsName loadMethodName = map.nameForMethod(loadMethod); SourceInfo sourceInfo = jsprogram.getSourceInfo(); JsInvocation call = new JsInvocation(sourceInfo); call.setQualifier(wrapWithEntry(loadMethodName.makeRef(sourceInfo))); call.getArguments().add(new JsNumberLiteral(sourceInfo, fragmentId)); List newStats = Collections. singletonList(call.makeStmt()); return newStats; } /** * Assume that all code described by alreadyLoadedPredicate has * been downloaded. Extract enough JavaScript statements that the code * described by livenessPredicate can also run. The caller should * ensure that livenessPredicate includes strictly more live code * than alreadyLoadedPredicate. */ public List extractStatements( LivenessPredicate livenessPredicate, LivenessPredicate alreadyLoadedPredicate) { List extractedStats = new ArrayList(); /** * The type whose vtables can currently be installed. */ JClassType currentVtableType = null; JClassType pendingVtableType = null; JsExprStmt pendingDefineClass = null; List statements = jsprogram.getGlobalBlock().getStatements(); for (JsStatement statement : statements) { boolean keep; JClassType 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; } JClassType vtableType = vtableTypeNeeded(statement); if (vtableType != null && vtableType != currentVtableType) { assert pendingVtableType == vtableType; 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 findAllMethodsInJavaScript() { Set methodsInJs = new HashSet(); for (int frag = 0; frag < jsprogram.getFragmentCount(); frag++) { List stats = jsprogram.getFragmentBlock(frag).getStatements(); for (JsStatement stat : stats) { JMethod method = methodFor(stat); 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 stat) { if (stat instanceof JsVars) { for (JsVar var : (JsVars) stat) { 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. * *

* 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. *

*/ 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 stat, LivenessPredicate livenessPredicate) { JClassType type = map.typeForStatement(stat); if (type != null) { // This is part of the code only needed once a type is instantiable return livenessPredicate.isLive(type); } JMethod meth = methodFor(stat); if (meth != null) { /* * This statement either defines a method or installs it in a vtable. */ if (!livenessPredicate.isLive(meth)) { // 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 !meth.needsVtable() || livenessPredicate.isLive(meth.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 true. * * 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(); } /** * Return the Java method corresponding to stat, or * null if there isn't one. It recognizes JavaScript of the form * function foo(...) { ...}, where foo is the name * of the JavaScript translation of a Java method. */ private JMethod methodFor(JsStatement stat) { return methodFor(stat, map); } /** * If stat is a {@link JsVars} that initializes a bunch of intern vars, return * a modified statement that skips any vars are needed by * currentLivenessPredicate but not by * alreadyLoadedPredicate. */ 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 state is of the form _ = String.prototype, * then return String. If the form is * defineClass(id, superId, cTM, ctor1, ctor2, ...) return the type * corresponding to that id. Otherwise return null. */ private JClassType vtableTypeAssigned(JsStatement stat) { if (!(stat instanceof JsExprStmt)) { return null; } JsExprStmt expr = (JsExprStmt) stat; 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(); JsFunction defineClassJsFunc = jsprogram.getIndexedFunction("JavaClassHierarchySetupUtil.defineClass"); JsFunction defineClassJsProtoFunc = jsprogram.getIndexedFunction( "JavaClassHierarchySetupUtil.defineClassWithPrototype"); if (func.getName() != defineClassJsFunc.getName() && func.getName() != defineClassJsProtoFunc.getName()) { return null; } return map.typeForStatement(stat); } // 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(stat); } private JClassType vtableTypeNeeded(JsStatement stat) { JMethod meth = map.vtableInitToMethod(stat); if (meth != null) { if (meth.needsVtable()) { return (JClassType) meth.getEnclosingType(); } } return null; } /** * Wrap an expression with a call to $entry. */ private JsInvocation wrapWithEntry(JsExpression exp) { SourceInfo sourceInfo = exp.getSourceInfo(); JsInvocation call = new JsInvocation(sourceInfo, jsprogram.getScope().findExistingName("$entry").makeRef(sourceInfo), exp); return call; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy