| /* |
| * 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; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.dev.jjs.InternalCompilerException; |
| import com.google.gwt.dev.jjs.SourceInfo; |
| import com.google.gwt.dev.jjs.ast.Context; |
| import com.google.gwt.dev.jjs.ast.JClassLiteral; |
| import com.google.gwt.dev.jjs.ast.JClassType; |
| 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.JMethodCall; |
| import com.google.gwt.dev.jjs.ast.JModVisitor; |
| import com.google.gwt.dev.jjs.ast.JPrimitiveType; |
| import com.google.gwt.dev.jjs.ast.JProgram; |
| import com.google.gwt.dev.jjs.ast.JType; |
| 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 java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Replaces calls to |
| * {@link com.google.gwt.core.client.GWT#runAsync(com.google.gwt.core.client.RunAsyncCallback)} |
| * and |
| * {@link com.google.gwt.core.client.GWT#runAsync(Class, com.google.gwt.core.client.RunAsyncCallback)} |
| * by calls to a fragment loader. Additionally, replaces access to |
| * {@link com.google.gwt.core.client.prefetch.RunAsyncCode#runAsyncCode(Class)} |
| * by an equivalent call using an integer rather than a class literal. |
| */ |
| public class ReplaceRunAsyncs { |
| /** |
| * Information about the replacement of one runAsync call by a call to a |
| * generated code-loading method. |
| */ |
| public static class RunAsyncReplacement implements Serializable { |
| private final JMethod enclosingMethod; |
| private final JMethod loadMethod; |
| private final String name; |
| private final int number; |
| |
| RunAsyncReplacement(int number, JMethod enclosingMethod, |
| JMethod loadMethod, String name) { |
| this.number = number; |
| this.enclosingMethod = enclosingMethod; |
| this.loadMethod = loadMethod; |
| this.name = name; |
| } |
| |
| /** |
| * Can be null if the enclosing method cannot be designated with a JSNI |
| * reference. |
| */ |
| public JMethod getEnclosingMethod() { |
| return enclosingMethod; |
| } |
| |
| /** |
| * The load method to request loading the code for this method. |
| */ |
| public JMethod getLoadMethod() { |
| return loadMethod; |
| } |
| |
| /** |
| * Return the name of this runAsync, which is specified by a class literal |
| * in the two-argument version of runAsync(). Returns <code>null</code> if |
| * there is no name for the call. |
| */ |
| public String getName() { |
| return name; |
| } |
| |
| /** |
| * The index of this runAsync, numbered from 1 to n. |
| */ |
| public int getNumber() { |
| return number; |
| } |
| |
| @Override |
| public String toString() { |
| return "#" + number + ": " + enclosingMethod.toString(); |
| } |
| } |
| |
| private class AsyncCreateVisitor extends JModVisitor { |
| private JMethod currentMethod; |
| private int entryCount = 1; |
| |
| @Override |
| public void endVisit(JMethodCall x, Context ctx) { |
| JMethod method = x.getTarget(); |
| if (isRunAsyncMethod(method)) { |
| JExpression asyncCallback; |
| String name; |
| switch (x.getArgs().size()) { |
| case 1: |
| name = null; |
| asyncCallback = x.getArgs().get(0); |
| break; |
| case 2: |
| name = nameFromClassLiteral((JClassLiteral) x.getArgs().get(0)); |
| asyncCallback = x.getArgs().get(1); |
| break; |
| default: |
| throw new InternalCompilerException( |
| "runAsync call found with neither 1 nor 2 arguments: " + x); |
| } |
| |
| int entryNumber = entryCount++; |
| JClassType loader = getFragmentLoader(entryNumber); |
| JMethod loadMethod = getRunAsyncMethod(loader); |
| assert loadMethod != null; |
| runAsyncReplacements.put(entryNumber, new RunAsyncReplacement( |
| entryNumber, currentMethod, loadMethod, name)); |
| |
| JMethodCall methodCall = new JMethodCall(x.getSourceInfo(), null, |
| loadMethod); |
| methodCall.addArg(asyncCallback); |
| |
| tightenCallbackType(entryNumber, asyncCallback.getType()); |
| |
| program.addEntryMethod(getOnLoadMethod(loader), entryNumber); |
| |
| ctx.replaceMe(methodCall); |
| } |
| } |
| |
| @Override |
| public boolean visit(JMethod x, Context ctx) { |
| currentMethod = x; |
| return true; |
| } |
| |
| private boolean isRunAsyncMethod(JMethod method) { |
| /* |
| * The method is overloaded, so check the enclosing type plus the name. |
| */ |
| return method.getEnclosingType() == program.getIndexedType("GWT") |
| && method.getName().equals("runAsync"); |
| } |
| |
| /** |
| * Tighten some types and method calls immediately, in case the optimizer is |
| * not run. Without a little bit of tightening, code splitting will be |
| * completely ineffective. |
| * |
| * Note that {@link FragmentLoaderCreator} can't simply generate the tighter |
| * types to begin with, because when it runs, it doesn't know which runAsync |
| * call it is generating a loader for. |
| * |
| * This method can be deleted if {@link FragmentLoaderCreator} is |
| * eliminated. |
| */ |
| private void tightenCallbackType(int entryNumber, JType callbackType) { |
| JClassType loaderClass = getFragmentLoader(entryNumber); |
| |
| /* |
| * Before: class AsyncLoader3 { static void runAsync(RunAsyncCallback cb) |
| * { ... } } |
| * |
| * After: class AsyncLoader3 { static void runAsync(RunAsyncCallback$3 cb) |
| * { ... } } |
| */ |
| JMethod loadMethod = getRunAsyncMethod(loaderClass); |
| loadMethod.getParams().get(0).setType(callbackType); |
| |
| /* |
| * Before: class AsyncLoader3__Callback { RunAsyncCallback callback; } |
| * |
| * After: class AsyncLoader3__Callback { RunAsyncCallback$3 callback; } |
| */ |
| JClassType callbackListType = getFragmentLoaderCallbackList(entryNumber); |
| JField callbackField = getField(callbackListType, "callback"); |
| |
| /* |
| * The method AsyncLoaderNNN.runCallbacks has a lot of calls to onSuccess |
| * methods where the target is onSuccess in the RunAsyncCallback |
| * interface. Use MethodCallTightener to tighten those calls down to |
| * target the onSuccess method of a specific callback class. |
| */ |
| callbackField.setType(callbackType); |
| JMethod runCallbacksMethod = getMethod(loaderClass, |
| FragmentLoaderCreator.RUN_CALLBACKS); |
| MethodCallTightener.exec(program, runCallbacksMethod); |
| } |
| } |
| private class ReplaceRunAsyncResources extends JModVisitor { |
| private Map<String, List<RunAsyncReplacement>> replacementsByName; |
| |
| public ReplaceRunAsyncResources() { |
| replacementsByName = new HashMap<String, List<RunAsyncReplacement>>(); |
| for (RunAsyncReplacement replacement : runAsyncReplacements.values()) { |
| String name = replacement.getName(); |
| if (name != null) { |
| List<RunAsyncReplacement> list = replacementsByName.get(name); |
| if (list == null) { |
| list = new ArrayList<RunAsyncReplacement>(); |
| replacementsByName.put(name, list); |
| } |
| list.add(replacement); |
| } |
| } |
| } |
| |
| @Override |
| public void endVisit(JMethodCall x, Context ctx) { |
| if (x.getTarget() == program.getIndexedMethod("RunAsyncCode.runAsyncCode")) { |
| JExpression arg0 = x.getArgs().get(0); |
| if (!(arg0 instanceof JClassLiteral)) { |
| error(arg0.getSourceInfo(), |
| "Only a class literal may be passed to runAsyncCode"); |
| return; |
| } |
| JClassLiteral lit = (JClassLiteral) arg0; |
| String name = nameFromClassLiteral(lit); |
| List<RunAsyncReplacement> matches = replacementsByName.get(name); |
| if (matches == null || matches.size() == 0) { |
| error(x.getSourceInfo(), "No runAsync call is named " + name); |
| return; |
| } |
| if (matches.size() > 1) { |
| TreeLogger branch = error(x.getSourceInfo(), |
| "Multiple runAsync calls are named " + name); |
| for (RunAsyncReplacement match : matches) { |
| branch.log(TreeLogger.ERROR, "One call is in " |
| + methodDescription(match.getEnclosingMethod())); |
| } |
| return; |
| } |
| Integer splitPoint = matches.get(0).getNumber(); |
| |
| JMethodCall newCall = new JMethodCall(x.getSourceInfo(), null, |
| program.getIndexedMethod("RunAsyncCode.forSplitPointNumber")); |
| newCall.addArg(program.getLiteralInt(splitPoint)); |
| ctx.replaceMe(newCall); |
| } |
| } |
| |
| private String methodDescription(JMethod method) { |
| StringBuilder desc = new StringBuilder(); |
| desc.append(method.getEnclosingType().getName()); |
| desc.append("."); |
| desc.append(method.getName()); |
| desc.append(" ("); |
| desc.append(method.getSourceInfo().getFileName()); |
| desc.append(':'); |
| desc.append(method.getSourceInfo().getStartLine()); |
| desc.append(")"); |
| |
| return desc.toString(); |
| } |
| } |
| |
| public static void exec(TreeLogger logger, JProgram program) |
| throws UnableToCompleteException { |
| Event codeSplitterEvent = SpeedTracerLogger.start( |
| CompilerEventType.CODE_SPLITTER, "phase", "ReplaceRunAsyncs"); |
| TreeLogger branch = logger.branch(TreeLogger.TRACE, |
| "Replacing GWT.runAsync with island loader calls"); |
| new ReplaceRunAsyncs(branch, program).execImpl(); |
| codeSplitterEvent.end(); |
| } |
| |
| /** |
| * Extract the initializer of AsyncFragmentLoader.BROWSER_LOADER. A couple of |
| * parts of the compiler modify this initializer call. |
| */ |
| static JMethodCall getBrowserLoaderConstructor(JProgram program) { |
| JField field = program.getIndexedField("AsyncFragmentLoader.BROWSER_LOADER"); |
| JMethodCall initializerCall = (JMethodCall) field.getDeclarationStatement().getInitializer(); |
| assert initializerCall.getArgs().size() == 2; |
| return initializerCall; |
| } |
| |
| private static JMethod getMethod(JClassType type, String name) { |
| for (JMethod method : type.getMethods()) { |
| if (method.getName().equals(name)) { |
| return method; |
| } |
| } |
| throw new InternalCompilerException("Method not found: " + type.getName() |
| + "." + name); |
| } |
| |
| private static JMethod getOnLoadMethod(JClassType loaderType) { |
| assert loaderType != null; |
| assert loaderType.getMethods() != null; |
| JMethod method = getMethod(loaderType, "onLoad"); |
| assert method.isStatic(); |
| assert method.getParams().size() == 0; |
| return method; |
| } |
| |
| private static JMethod getRunAsyncMethod(JClassType loaderType) { |
| assert loaderType != null; |
| assert loaderType.getMethods() != null; |
| JMethod method = getMethod(loaderType, "runAsync"); |
| assert (method.isStatic()); |
| assert (method.getParams().size() == 1); |
| assert (method.getParams().get(0).getType().getName().equals(FragmentLoaderCreator.RUN_ASYNC_CALLBACK)); |
| return method; |
| } |
| |
| /** |
| * Convert a class literal to a runAsync name. |
| */ |
| private static String nameFromClassLiteral(JClassLiteral classLiteral) { |
| return classLiteral.getRefType().getName(); |
| } |
| |
| private boolean errorsFound = false; |
| private final TreeLogger logger; |
| private JProgram program; |
| |
| private Map<Integer, RunAsyncReplacement> runAsyncReplacements = new HashMap<Integer, RunAsyncReplacement>(); |
| |
| private ReplaceRunAsyncs(TreeLogger logger, JProgram program) { |
| this.logger = logger; |
| this.program = program; |
| } |
| |
| private TreeLogger error(SourceInfo info, String message) { |
| errorsFound = true; |
| TreeLogger fileLogger = logger.branch(TreeLogger.ERROR, "Error in '" |
| + info.getFileName() + "'"); |
| String linePrefix = ""; |
| if (info.getStartLine() > 0) { |
| linePrefix = "Line " + info.getStartLine() + ": "; |
| } |
| fileLogger.log(TreeLogger.ERROR, linePrefix + message); |
| return fileLogger; |
| } |
| |
| private void execImpl() throws UnableToCompleteException { |
| AsyncCreateVisitor visitor = new AsyncCreateVisitor(); |
| visitor.accept(program); |
| setNumEntriesInAsyncFragmentLoader(visitor.entryCount); |
| program.setRunAsyncReplacements(runAsyncReplacements); |
| new ReplaceRunAsyncResources().accept(program); |
| if (errorsFound) { |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| private JField getField(JClassType type, String name) { |
| for (JField field : type.getFields()) { |
| if (field.getName().equals(name)) { |
| return field; |
| } |
| } |
| throw new InternalCompilerException("Field not found: " + type.getName() |
| + "." + name); |
| } |
| |
| private JClassType getFragmentLoader(int fragmentNumber) { |
| String fragmentLoaderClassName = FragmentLoaderCreator.ASYNC_LOADER_PACKAGE |
| + "." + FragmentLoaderCreator.ASYNC_LOADER_CLASS_PREFIX |
| + fragmentNumber; |
| JType result = program.getFromTypeMap(fragmentLoaderClassName); |
| assert (result != null); |
| assert (result instanceof JClassType); |
| return (JClassType) result; |
| } |
| |
| private JClassType getFragmentLoaderCallbackList(int fragmentNumber) { |
| String className = FragmentLoaderCreator.ASYNC_LOADER_PACKAGE + "." |
| + FragmentLoaderCreator.ASYNC_LOADER_CLASS_PREFIX + fragmentNumber |
| + FragmentLoaderCreator.CALLBACK_LIST_SUFFIX; |
| JType result = program.getFromTypeMap(className); |
| assert (result != null); |
| return (JClassType) result; |
| } |
| |
| private void setNumEntriesInAsyncFragmentLoader(int entryCount) { |
| JMethodCall constructorCall = getBrowserLoaderConstructor(program); |
| assert constructorCall.getArgs().get(0).getType() == JPrimitiveType.INT; |
| constructorCall.setArg(0, program.getLiteralInt(entryCount)); |
| } |
| } |