com.google.javascript.jscomp.PhaseOptimizer Maven / Gradle / Ivy
Show all versions of com.liferay.frontend.js.minifier
/*
* 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;
}
}
}