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.
*
* @author [email protected] (Nick Santos)
* @author [email protected] (Dimitris Vardoulakis)
*/
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;
private double progress = 0.0;
private double progressStep = 0.0;
private ProgressRange progressRange;
// 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
}
/**
* NOTE(dimvar): There used to be some code that tried various orderings of loopable passes and
* picked the fastest one. This code became stale gradually and I decided to remove it. It was
* also never tried after the new pass scheduler was written. If we need to revisit this order in
* the future, we should write new code to do it.
*
*
It is important that inlineVariables and peepholeOptimizations run after inlineFunctions,
* because inlineFunctions relies on them to clean up patterns it introduces. This affects our
* size-based loop-termination heuristic.
*/
@VisibleForTesting
static final ImmutableList OPTIMAL_ORDER =
ImmutableList.of(
PassNames.INLINE_FUNCTIONS,
PassNames.INLINE_VARIABLES,
PassNames.DEAD_ASSIGNMENT_ELIMINATION,
PassNames.COLLAPSE_OBJECT_LITERALS,
PassNames.REMOVE_UNUSED_CODE,
PassNames.REMOVE_UNUSED_PROTOTYPE_PROPERTIES,
PassNames.REMOVE_UNUSED_CLASS_PROPERTIES,
PassNames.PEEPHOLE_OPTIMIZATIONS,
PassNames.REMOVE_UNREACHABLE_CODE);
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;
}
}
PhaseOptimizer withProgress(ProgressRange range) {
this.progressRange = range;
return this;
}
/**
* 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.isOneTimePass()) {
if (currentLoop.isPopulated()) {
passes.add(currentLoop);
currentLoop = new Loop();
}
addOneTimePass(factory);
} else {
currentLoop.addLoopedPass(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) {
progress = 0.0;
progressStep = 0.0;
if (progressRange != null) {
progressStep = (progressRange.maxValue - progressRange.initialValue)
/ passes.size();
progress = progressRange.initialValue;
}
// 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 (Thread.interrupted()) {
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) {
if (!factory.featureSet().contains(compiler.getFeatureSet())) {
logger.warning("Skipping pass " + name);
logger.info(
"pass supports: " + factory.featureSet()
+ "\ncurrent AST contains: " + compiler.getFeatureSet());
return;
}
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.isOneTimePass());
}
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 {
if (progressRange == null) {
compiler.setProgress(-1, name);
} else {
progress += progressStep;
compiler.setProgress(progress, name);
}
// 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;
optimizePasses();
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<>();
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))
|| (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);
lastIterMadeChanges = true;
} else {
madeChanges.remove(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;
}
/** Re-arrange the passes in an optimal order. */
private void optimizePasses() {
// It's important that this ordering is deterministic, so that
// multiple compiles with the same input produce exactly the same
// results.
//
// To do this, grab any passes we recognize, and move them to the end
// in an "optimal" order.
List optimalPasses = new ArrayList<>();
for (String passInOptimalOrder : OPTIMAL_ORDER) {
for (NamedPass loopablePass : myPasses) {
if (loopablePass.name.equals(passInOptimalOrder)) {
optimalPasses.add(loopablePass);
break;
}
}
}
myPasses.removeAll(optimalPasses);
myPasses.addAll(optimalPasses);
}
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;
}
}
}