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

com.google.javascript.jscomp.PhaseOptimizer Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2009 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp;

import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/** An object that optimizes the order of compiler passes. */
class PhaseOptimizer implements CompilerPass {

  private static final Logger logger = Logger.getLogger(PhaseOptimizer.class.getName());
  private final AbstractCompiler compiler;
  private final PerformanceTracker tracker;
  private final List passes;
  private boolean inLoop;
  private PassFactory validityCheck;
  private boolean printAstHashcodes = false;

  // These fields are used during optimization loops.
  // They are declared here for two reasons:
  // 1) Loop and ScopedChangeHandler can communicate via shared state
  // 2) Compiler talks to PhaseOptimizer, not Loop or ScopedChangeHandler
  private NamedPass currentPass;
  // For each pass, remember the time at the end of the pass's last run.
  private Map lastRuns;
  // The time of the last change made to the program by any pass.
  private int lastChange;
  private static final int START_TIME = 0;
  private final Node jsRoot;

  private final boolean useSizeHeuristicToStopOptimizationLoop;

  // Checks that passes have reported code changes correctly.
  private ChangeVerifier changeVerifier;

  /**
   * When processing loopable passes in order, the PhaseOptimizer can be in one of these two states.
   *
   * 

This enum is used by Loop/process only, but enum types can't be local. */ enum State { RUN_PASSES_NOT_RUN_IN_PREV_ITER, RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER } static final ImmutableList CODE_REMOVING_PASSES = ImmutableList.of(PassNames.PEEPHOLE_OPTIMIZATIONS, PassNames.REMOVE_UNREACHABLE_CODE); static final int MAX_LOOPS = 100; static final String OPTIMIZE_LOOP_ERROR = "Fixed point loop exceeded the maximum number of iterations."; /** * @see CompilerOptions#optimizationLoopMaxIterations */ private final int optimizationLoopMaxIterations; /** * @param comp the compiler that owns/creates this. * @param tracker an optional performance tracker */ PhaseOptimizer(AbstractCompiler comp, PerformanceTracker tracker) { this.compiler = comp; this.jsRoot = comp.getJsRoot(); this.tracker = tracker; this.passes = new ArrayList<>(); this.inLoop = false; this.lastChange = START_TIME; this.useSizeHeuristicToStopOptimizationLoop = comp.getOptions().useSizeHeuristicToStopOptimizationLoop; int maxIterations = comp.getOptions().optimizationLoopMaxIterations; if (maxIterations > 0 && maxIterations <= MAX_LOOPS) { this.optimizationLoopMaxIterations = maxIterations; } else { this.optimizationLoopMaxIterations = MAX_LOOPS; } } /** * Add the passes generated by the given factories to the compile sequence. * *

Automatically pulls multi-run passes into fixed point loops. If there are 1 or more * multi-run passes in a row, they will run together in the same fixed point loop. The passes will * run until they are finished making changes. * *

The PhaseOptimizer is free to tweak the order and frequency of multi-run passes in a * fixed-point loop. */ void consume(List factories) { Loop currentLoop = new Loop(); for (PassFactory factory : factories) { if (factory.isRunInFixedPointLoop()) { currentLoop.addLoopedPass(factory); } else { if (currentLoop.isPopulated()) { passes.add(currentLoop); currentLoop = new Loop(); } addOneTimePass(factory); } } if (currentLoop.isPopulated()) { passes.add(currentLoop); } } /** * Add the pass generated by the given factory to the compile sequence. This pass will be run * once. */ @VisibleForTesting void addOneTimePass(PassFactory factory) { passes.add(new NamedPass(factory)); } /** * Add a loop to the compile sequence. This loop will continue running until the AST stops * changing. * * @return The loop structure. Pass suppliers should be added to the loop. */ Loop addFixedPointLoop() { Loop loop = new Loop(); passes.add(loop); return loop; } /** Adds a checker to be run after every pass. Intended for development. */ void setValidityCheck(PassFactory validityCheck) { this.validityCheck = validityCheck; this.changeVerifier = new ChangeVerifier(compiler).snapshot(jsRoot); } /** Sets the hashcode of the AST to be logged every pass. Intended for development. */ void setPrintAstHashcodes(boolean printAstHashcodes) { this.printAstHashcodes = printAstHashcodes; } /** Run all the passes in the optimizer. */ @Override public void process(Node externs, Node root) { // When looking at this code, one can mistakenly think that the instance of // PhaseOptimizer keeps all compiler passes live. This would be undesirable. A pass can // create large data structures that are only useful to the pass, and we shouldn't // retain them until the end of the compilation. But if you look at // NamedPass#process, the actual pass is created and immediately executed, and no // reference to it is retained in PhaseOptimizer: // factory.create(compiler).process(externs, root); for (CompilerPass pass : passes) { if (Platform.isThreadInterrupted()) { throw new RuntimeException(new InterruptedException()); } pass.process(externs, root); if (hasHaltingErrors()) { return; } } } private void maybePrintAstHashcodes(String passName, Node root) { if (printAstHashcodes) { String hashCodeMsg = "AST hashCode after " + passName + ": " + compiler.toSource(root).hashCode(); logger.info(hashCodeMsg); } } /** Runs the validity check if it is available. */ private void maybeRunValidityCheck(String passName, Node externs, Node root) { if (validityCheck == null) { return; } try { validityCheck.create(compiler).process(externs, root); changeVerifier.checkRecordedChanges(passName, jsRoot); } catch (RuntimeException e) { throw new IllegalStateException("Validity checks failed for pass: " + passName, e); } } private boolean hasHaltingErrors() { return compiler.hasHaltingErrors(); } /** A single compiler pass. */ class NamedPass implements CompilerPass { final String name; private final PassFactory factory; private Tracer tracer; NamedPass(PassFactory factory) { this.name = factory.getName(); this.factory = factory; } @Override public void process(Node externs, Node root) { logger.fine("Running pass " + name); if (validityCheck != null) { // Before running the pass, clone the AST so you can check the // changed AST against the clone after the pass finishes. changeVerifier = new ChangeVerifier(compiler).snapshot(jsRoot); } if (tracker != null) { tracker.recordPassStart(name, !factory.isRunInFixedPointLoop()); } tracer = new Tracer("Compiler", name); compiler.beforePass(name); // Delay the creation of the actual pass until *after* all previous passes // have been processed. // Some precondition checks rely on this, eg, in CoalesceVariableNames. factory.create(compiler).process(externs, root); compiler.afterPass(name); try { // Don't move this line in the IF. We create a Tracer even when the tracker // is null; so we must also stop the tracer when the tracker is null. // Otherwise, Tracer.ThreadTrace#events can become too big. long traceRuntime = tracer.stop(); if (tracker != null) { tracker.recordPassStop(name, traceRuntime); } maybePrintAstHashcodes(name, root); maybeRunValidityCheck(name, externs, root); } catch (IllegalStateException e) { // TODO(johnlenz): Remove this once the normalization checks report // errors instead of exceptions. throw new RuntimeException("Validity check failed for " + name, e); } } @Override public String toString() { return "pass: " + name; } } boolean hasScopeChanged(Node n) { // Outside loops we don't track changed scopes, so we visit them all. if (!inLoop) { return true; } int timeOfLastRun = lastRuns.get(currentPass); // A pass looks at all functions when it first runs return timeOfLastRun == START_TIME || n.getChangeTime() > timeOfLastRun; } /** A change handler that marks scopes as changed when reportChange is called. */ private class ScopedChangeHandler implements CodeChangeHandler { private int lastCodeChangeQuery; ScopedChangeHandler() { this.lastCodeChangeQuery = compiler.getChangeStamp(); } @Override public void reportChange() { lastChange = compiler.getChangeStamp(); } private boolean hasCodeChangedSinceLastCall() { boolean result = lastChange > lastCodeChangeQuery; lastCodeChangeQuery = compiler.getChangeStamp(); // The next call to the method will happen at a different time compiler.incrementChangeStamp(); return result; } } /** * A compound pass that contains atomic passes and runs them until they reach a fixed point. * *

Notice that this is a non-static class, because it includes the closure of PhaseOptimizer. */ @VisibleForTesting class Loop implements CompilerPass { private final List myPasses = new ArrayList<>(); private final Set myNames = new HashSet<>(); private ScopedChangeHandler scopeHandler; private boolean isCodeRemovalLoop = false; private int howmanyIterationsUnderThreshold = 0; void addLoopedPass(PassFactory factory) { String name = factory.getName(); Preconditions.checkArgument( !myNames.contains(name), "Already a pass with name '%s' in this loop", name); myNames.add(name); myPasses.add(new NamedPass(factory)); } @Override public void process(Node externs, Node root) { checkState(!inLoop, "Nested loops are forbidden"); inLoop = true; this.isCodeRemovalLoop = isCodeRemovalLoop(); // Set up function-change tracking scopeHandler = new ScopedChangeHandler(); compiler.addChangeHandler(scopeHandler); // lastRuns is initialized before each loop. This way, when a pass is run // in the 2nd loop for the 1st time, it looks at all scopes. lastRuns = new HashMap<>(); for (NamedPass pass : myPasses) { lastRuns.put(pass, START_TIME); } // Contains a pass iff it made changes the last time it was run. Set madeChanges = new HashSet<>(); // Contains a pass iff it was run during the last inner loop. Set runInPrevIter = new HashSet<>(); // Contains a pass iff it did not make changes. This set is cleared each time any pass makes // changes. The purpose of this is to ensure we do not rerun any passes that did not make // changes unless a later run pass did make changes. Set didNotMakeChanges = new HashSet<>(); State state = State.RUN_PASSES_NOT_RUN_IN_PREV_ITER; boolean lastIterMadeChanges; int count = 1; int astSize = NodeUtil.countAstSize(root); int previousAstSize = astSize; // The loop starts at state RUN_PASSES_NOT_RUN_IN_PREV_ITER and runs all passes. // After that, it goes to state RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER, and // runs several iterations in that state, until there are no longer any changes // to the AST, and then it goes back to RUN_PASSES_NOT_RUN_IN_PREV_ITER. // We call one sequence of RUN_PASSES_NOT_RUN_IN_PREV_ITER followed by // RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER a batch. // At the end of every loop batch, if the batch made so few changes that the // changed percentage of the AST is below some threshold, we stop the loop // without waiting to reach a fixpoint. try { while (true) { if (count > optimizationLoopMaxIterations && this.isCodeRemovalLoop) { return; } if (count > MAX_LOOPS) { compiler.throwInternalError(OPTIMIZE_LOOP_ERROR, null); } count++; lastIterMadeChanges = false; for (NamedPass pass : myPasses) { if ((state == State.RUN_PASSES_NOT_RUN_IN_PREV_ITER && !runInPrevIter.contains(pass) && !didNotMakeChanges.contains(pass)) || (state == State.RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER && madeChanges.contains(pass))) { compiler.incrementChangeStamp(); currentPass = pass; pass.process(externs, root); runInPrevIter.add(pass); lastRuns.put(pass, compiler.getChangeStamp()); if (hasHaltingErrors()) { return; } else if (scopeHandler.hasCodeChangedSinceLastCall()) { madeChanges.add(pass); didNotMakeChanges.clear(); lastIterMadeChanges = true; } else { madeChanges.remove(pass); didNotMakeChanges.add(pass); } } else { runInPrevIter.remove(pass); } } previousAstSize = astSize; astSize = NodeUtil.countAstSize(root); if (state == State.RUN_PASSES_NOT_RUN_IN_PREV_ITER) { if (lastIterMadeChanges && isAstSufficientlyChanging(previousAstSize, astSize)) { state = State.RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER; } else { return; } } else { checkState(state == State.RUN_PASSES_THAT_CHANGED_STH_IN_PREV_ITER); if (!lastIterMadeChanges || !isAstSufficientlyChanging(previousAstSize, astSize)) { state = State.RUN_PASSES_NOT_RUN_IN_PREV_ITER; } } } } finally { inLoop = false; compiler.removeChangeHandler(scopeHandler); } } /** * If two loop batches in a row made the code less than 0.05% smaller than the previous batches, * stop before the fixpoint. The 0.05% threshold is based on the following heuristic: 1% size * difference matters to our users. 0.1% size difference is borderline relevant. 0.05% * difference between loop batches is unlikely to grow the final output more than 0.1%. * *

Use this criterion only for the two code-removing loops. The code-motion loop may move * code around but not remove code, so this criterion is not correct for stopping early. * *

NOTE: the size heuristic is not robust when passes in the code-removing loop increase the * AST size; all passes in the loop must make the code smaller. Otherwise, what may seem like a * small size difference may indeed be big changes, and we miss it because we don't compute the * AST size after each pass. This can happen because inlineFunctions may increase code size, and * relies on peephole and inlineVariables to clean up. As long as these passes run after it in * the loop, we should be OK. */ private boolean isAstSufficientlyChanging(int oldAstSize, int newAstSize) { if (useSizeHeuristicToStopOptimizationLoop && this.isCodeRemovalLoop) { float percentChange = 100 * (Math.abs(newAstSize - oldAstSize) / (float) oldAstSize); if (percentChange < 0.05) { this.howmanyIterationsUnderThreshold++; } else { this.howmanyIterationsUnderThreshold = 0; } return this.howmanyIterationsUnderThreshold < 2; } return true; } boolean isPopulated() { return !myPasses.isEmpty(); } private boolean isCodeRemovalLoop() { for (NamedPass pass : this.myPasses) { if (CODE_REMOVING_PASSES.contains(pass.name)) { return true; } } return false; } } /** * An object used when running many NamedPass loopable passes as a Loop pass, to keep track of how * far along we are. */ static class ProgressRange { public final double initialValue; public final double maxValue; public ProgressRange(double initialValue, double maxValue) { this.initialValue = initialValue; this.maxValue = maxValue; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy