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

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

There is a newer version: 9.0.8
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 (Exception 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