diff options
Diffstat (limited to 'tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala')
-rw-r--r-- | tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala new file mode 100644 index 0000000..646484b --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSOptimizer.scala @@ -0,0 +1,552 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.optimizer + +import scala.annotation.{switch, tailrec} + +import scala.collection.mutable +import scala.collection.immutable.{Seq, Traversable} + +import java.net.URI + +import scala.scalajs.ir +import ir.Infos +import ir.ClassKind + +import scala.scalajs.tools.logging._ +import scala.scalajs.tools.io._ +import scala.scalajs.tools.classpath._ +import scala.scalajs.tools.sourcemap._ +import scala.scalajs.tools.corelib._ + +import scala.scalajs.tools.sem.Semantics + +import scala.scalajs.tools.javascript +import javascript.{Trees => js} + +/** Scala.js optimizer: does type-aware global dce. */ +class ScalaJSOptimizer( + semantics: Semantics, + optimizerFactory: (Semantics) => GenIncOptimizer) { + import ScalaJSOptimizer._ + + private val classEmitter = new javascript.ScalaJSClassEmitter(semantics) + + private[this] var persistentState: PersistentState = new PersistentState + private[this] var optimizer: GenIncOptimizer = optimizerFactory(semantics) + + def this(semantics: Semantics) = this(semantics, new IncOptimizer(_)) + + /** Applies Scala.js-specific optimizations to a CompleteIRClasspath. + * See [[ScalaJSOptimizer.Inputs]] for details about the required and + * optional inputs. + * See [[ScalaJSOptimizer.OutputConfig]] for details about the configuration + * for the output of this method. + * Returns a [[CompleteCIClasspath]] containing the result of the + * optimizations. + * + * analyzes, dead code eliminates and concatenates IR content + * - Maintains/establishes order + * - No IR in result + * - CoreJSLibs in result (since they are implicitly in the CompleteIRCP) + */ + def optimizeCP(inputs: Inputs[IRClasspath], outCfg: OutputConfig, + logger: Logger): LinkedClasspath = { + + val cp = inputs.input + + CacheUtils.cached(cp.version, outCfg.output, outCfg.cache) { + logger.info(s"Fast optimizing ${outCfg.output.path}") + optimizeIR(inputs.copy(input = inputs.input.scalaJSIR), outCfg, logger) + } + + new LinkedClasspath(cp.jsLibs, outCfg.output, cp.requiresDOM, cp.version) + } + + def optimizeIR(inputs: Inputs[Traversable[VirtualScalaJSIRFile]], + outCfg: OutputConfig, logger: Logger): Unit = { + + val builder = { + import outCfg._ + if (wantSourceMap) + new JSFileBuilderWithSourceMap(output.name, + output.contentWriter, + output.sourceMapWriter, + relativizeSourceMapBase) + else + new JSFileBuilder(output.name, output.contentWriter) + } + + builder.addLine("'use strict';") + CoreJSLibs.libs(semantics).foreach(builder.addFile _) + + optimizeIR(inputs, outCfg, builder, logger) + + builder.complete() + builder.closeWriters() + } + + def optimizeIR(inputs: Inputs[Traversable[VirtualScalaJSIRFile]], + outCfg: OptimizerConfig, builder: JSTreeBuilder, logger: Logger): Unit = { + + /* Handle tree equivalence: If we handled source maps so far, positions are + still up-to-date. Otherwise we need to flush the state if proper + positions are requested now. + */ + if (outCfg.wantSourceMap && !persistentState.wasWithSourceMap) + clean() + + persistentState.wasWithSourceMap = outCfg.wantSourceMap + + persistentState.startRun() + try { + import inputs._ + val allData = + GenIncOptimizer.logTime(logger, "Read info") { + readAllData(inputs.input, logger) + } + val (useOptimizer, refinedAnalyzer) = GenIncOptimizer.logTime( + logger, "Optimizations part") { + val analyzer = + GenIncOptimizer.logTime(logger, "Compute reachability") { + val analyzer = new Analyzer(logger, semantics, allData, + globalWarnEnabled = true, + isBeforeOptimizer = !outCfg.disableOptimizer) + analyzer.computeReachability(manuallyReachable, noWarnMissing) + analyzer + } + if (outCfg.checkIR) { + GenIncOptimizer.logTime(logger, "Check IR") { + if (analyzer.allAvailable) + checkIR(analyzer, logger) + else if (inputs.noWarnMissing.isEmpty) + sys.error("Could not check IR because there where linking errors.") + } + } + def getClassTreeIfChanged(encodedName: String, + lastVersion: Option[String]): Option[(ir.Trees.ClassDef, Option[String])] = { + val persistentFile = persistentState.encodedNameToPersistentFile(encodedName) + persistentFile.treeIfChanged(lastVersion) + } + + val useOptimizer = analyzer.allAvailable && !outCfg.disableOptimizer + + if (outCfg.batchMode) + optimizer = optimizerFactory(semantics) + + val refinedAnalyzer = if (useOptimizer) { + GenIncOptimizer.logTime(logger, "Inliner") { + optimizer.update(analyzer, getClassTreeIfChanged, + outCfg.wantSourceMap, logger) + } + GenIncOptimizer.logTime(logger, "Refined reachability analysis") { + val refinedData = computeRefinedData(allData, optimizer) + val refinedAnalyzer = new Analyzer(logger, semantics, refinedData, + globalWarnEnabled = false, + isBeforeOptimizer = false) + refinedAnalyzer.computeReachability(manuallyReachable, noWarnMissing) + refinedAnalyzer + } + } else { + if (inputs.noWarnMissing.isEmpty && !outCfg.disableOptimizer) + logger.warn("Not running the inliner because there where linking errors.") + analyzer + } + (useOptimizer, refinedAnalyzer) + } + GenIncOptimizer.logTime(logger, "Write DCE'ed output") { + buildDCEedOutput(builder, refinedAnalyzer, useOptimizer) + } + } finally { + persistentState.endRun(outCfg.unCache) + logger.debug( + s"Inc. opt stats: reused: ${persistentState.statsReused} -- "+ + s"invalidated: ${persistentState.statsInvalidated} -- "+ + s"trees read: ${persistentState.statsTreesRead}") + } + } + + /** Resets all persistent state of this optimizer */ + def clean(): Unit = { + persistentState = new PersistentState + optimizer = optimizerFactory(semantics) + } + + private def readAllData(ir: Traversable[VirtualScalaJSIRFile], + logger: Logger): scala.collection.Seq[Infos.ClassInfo] = { + ir.map(persistentState.getPersistentIRFile(_).info).toSeq + } + + private def checkIR(analyzer: Analyzer, logger: Logger): Unit = { + val allClassDefs = for { + classInfo <- analyzer.classInfos.values + persistentIRFile <- persistentState.encodedNameToPersistentFile.get( + classInfo.encodedName) + } yield persistentIRFile.tree + val checker = new IRChecker(analyzer, allClassDefs.toSeq, logger) + if (!checker.check()) + sys.error(s"There were ${checker.errorCount} IR checking errors.") + } + + private def computeRefinedData( + allData: scala.collection.Seq[Infos.ClassInfo], + optimizer: GenIncOptimizer): scala.collection.Seq[Infos.ClassInfo] = { + + def refineMethodInfo(container: optimizer.MethodContainer, + methodInfo: Infos.MethodInfo): Infos.MethodInfo = { + container.methods.get(methodInfo.encodedName).fold(methodInfo) { + methodImpl => methodImpl.preciseInfo + } + } + + def refineMethodInfos(container: optimizer.MethodContainer, + methodInfos: List[Infos.MethodInfo]): List[Infos.MethodInfo] = { + methodInfos.map(m => refineMethodInfo(container, m)) + } + + def refineClassInfo(container: optimizer.MethodContainer, + info: Infos.ClassInfo): Infos.ClassInfo = { + val refinedMethods = refineMethodInfos(container, info.methods) + Infos.ClassInfo(info.name, info.encodedName, info.isExported, + info.ancestorCount, info.kind, info.superClass, info.ancestors, + Infos.OptimizerHints.empty, refinedMethods) + } + + for { + info <- allData + } yield { + info.kind match { + case ClassKind.Class | ClassKind.ModuleClass => + optimizer.getClass(info.encodedName).fold(info) { + cls => refineClassInfo(cls, info) + } + + case ClassKind.TraitImpl => + optimizer.getTraitImpl(info.encodedName).fold(info) { + impl => refineClassInfo(impl, info) + } + + case _ => + info + } + } + } + + private def buildDCEedOutput(builder: JSTreeBuilder, + analyzer: Analyzer, useInliner: Boolean): Unit = { + + def compareClassInfo(lhs: analyzer.ClassInfo, rhs: analyzer.ClassInfo) = { + if (lhs.ancestorCount != rhs.ancestorCount) lhs.ancestorCount < rhs.ancestorCount + else lhs.encodedName.compareTo(rhs.encodedName) < 0 + } + + def addPersistentFile(classInfo: analyzer.ClassInfo, + persistentFile: PersistentIRFile) = { + import ir.Trees._ + import javascript.JSDesugaring.{desugarJavaScript => desugar} + + val d = persistentFile.desugared + lazy val classDef = { + persistentState.statsTreesRead += 1 + persistentFile.tree + } + + def addTree(tree: js.Tree): Unit = + builder.addJSTree(tree) + + def addReachableMethods(emitFun: (String, MethodDef) => js.Tree): Unit = { + /* This is a bit convoluted because we have to: + * * avoid to use classDef at all if we already know all the needed methods + * * if any new method is needed, better to go through the defs once + */ + val methodNames = d.methodNames.getOrElseUpdate( + classDef.defs collect { + case MethodDef(Ident(encodedName, _), _, _, _) => encodedName + }) + val reachableMethods = methodNames.filter( + name => classInfo.methodInfos(name).isReachable) + if (reachableMethods.forall(d.methods.contains(_))) { + for (encodedName <- reachableMethods) { + addTree(d.methods(encodedName)) + } + } else { + classDef.defs.foreach { + case m: MethodDef if m.name.isInstanceOf[Ident] => + if (classInfo.methodInfos(m.name.name).isReachable) { + addTree(d.methods.getOrElseUpdate(m.name.name, + emitFun(classInfo.encodedName, m))) + } + case _ => + } + } + } + + if (classInfo.isImplClass) { + if (useInliner) { + for { + method <- optimizer.findTraitImpl(classInfo.encodedName).methods.values + if (classInfo.methodInfos(method.encodedName).isReachable) + } { + addTree(method.desugaredDef) + } + } else { + addReachableMethods(classEmitter.genTraitImplMethod) + } + } else if (!classInfo.hasMoreThanData) { + // there is only the data anyway + addTree(d.wholeClass.getOrElseUpdate( + classEmitter.genClassDef(classDef))) + } else { + if (classInfo.isAnySubclassInstantiated) { + addTree(d.constructor.getOrElseUpdate( + classEmitter.genConstructor(classDef))) + if (useInliner) { + for { + method <- optimizer.findClass(classInfo.encodedName).methods.values + if (classInfo.methodInfos(method.encodedName).isReachable) + } { + addTree(method.desugaredDef) + } + } else { + addReachableMethods(classEmitter.genMethod) + } + addTree(d.exportedMembers.getOrElseUpdate(js.Block { + classDef.defs collect { + case m: MethodDef if m.name.isInstanceOf[StringLiteral] => + classEmitter.genMethod(classInfo.encodedName, m) + case p: PropertyDef => + classEmitter.genProperty(classInfo.encodedName, p) + } + }(classDef.pos))) + } + if (classInfo.isDataAccessed) { + addTree(d.typeData.getOrElseUpdate(js.Block( + classEmitter.genInstanceTests(classDef), + classEmitter.genArrayInstanceTests(classDef), + classEmitter.genTypeData(classDef) + )(classDef.pos))) + } + if (classInfo.isAnySubclassInstantiated) + addTree(d.setTypeData.getOrElseUpdate( + classEmitter.genSetTypeData(classDef))) + if (classInfo.isModuleAccessed) + addTree(d.moduleAccessor.getOrElseUpdate( + classEmitter.genModuleAccessor(classDef))) + addTree(d.classExports.getOrElseUpdate( + classEmitter.genClassExports(classDef))) + } + } + + + for { + classInfo <- analyzer.classInfos.values.toSeq.sortWith(compareClassInfo) + if classInfo.isNeededAtAll + } { + val optPersistentFile = + persistentState.encodedNameToPersistentFile.get(classInfo.encodedName) + + // if we have a persistent file, this is not a dummy class + optPersistentFile.fold { + if (classInfo.isAnySubclassInstantiated) { + // Subclass will emit constructor that references this dummy class. + // Therefore, we need to emit a dummy parent. + builder.addJSTree( + classEmitter.genDummyParent(classInfo.encodedName)) + } + } { pf => addPersistentFile(classInfo, pf) } + } + } +} + +object ScalaJSOptimizer { + /** Inputs of the Scala.js optimizer. */ + final case class Inputs[T]( + /** The CompleteNCClasspath or the IR files to be packaged. */ + input: T, + /** Manual additions to reachability */ + manuallyReachable: Seq[ManualReachability] = Nil, + /** Elements we won't warn even if they don't exist */ + noWarnMissing: Seq[NoWarnMissing] = Nil + ) + + sealed abstract class ManualReachability + final case class ReachObject(name: String) extends ManualReachability + final case class Instantiate(name: String) extends ManualReachability + final case class ReachMethod(className: String, methodName: String, + static: Boolean) extends ManualReachability + + sealed abstract class NoWarnMissing + final case class NoWarnClass(className: String) extends NoWarnMissing + final case class NoWarnMethod(className: String, methodName: String) + extends NoWarnMissing + + /** Configurations relevant to the optimizer */ + trait OptimizerConfig { + /** Ask to produce source map for the output. Is used in the incremental + * optimizer to decide whether a position change should trigger re-inlining + */ + val wantSourceMap: Boolean + /** If true, performs expensive checks of the IR for the used parts. */ + val checkIR: Boolean + /** If true, the optimizer removes trees that have not been used in the + * last run from the cache. Otherwise, all trees that has been used once, + * are kept in memory. */ + val unCache: Boolean + /** If true, no optimizations are performed */ + val disableOptimizer: Boolean + /** If true, nothing is performed incrementally */ + val batchMode: Boolean + } + + /** Configuration for the output of the Scala.js optimizer. */ + final case class OutputConfig( + /** Writer for the output. */ + output: WritableVirtualJSFile, + /** Cache file */ + cache: Option[WritableVirtualTextFile] = None, + /** Ask to produce source map for the output */ + wantSourceMap: Boolean = false, + /** Base path to relativize paths in the source map. */ + relativizeSourceMapBase: Option[URI] = None, + /** If true, performs expensive checks of the IR for the used parts. */ + checkIR: Boolean = false, + /** If true, the optimizer removes trees that have not been used in the + * last run from the cache. Otherwise, all trees that has been used once, + * are kept in memory. */ + unCache: Boolean = true, + /** If true, no optimizations are performed */ + disableOptimizer: Boolean = false, + /** If true, nothing is performed incrementally */ + batchMode: Boolean = false + ) extends OptimizerConfig + + // Private helpers ----------------------------------------------------------- + + private final class PersistentState { + val files = mutable.Map.empty[String, PersistentIRFile] + val encodedNameToPersistentFile = + mutable.Map.empty[String, PersistentIRFile] + + var statsReused: Int = 0 + var statsInvalidated: Int = 0 + var statsTreesRead: Int = 0 + + var wasWithSourceMap: Boolean = true + + def startRun(): Unit = { + statsReused = 0 + statsInvalidated = 0 + statsTreesRead = 0 + for (file <- files.values) + file.startRun() + } + + def getPersistentIRFile(irFile: VirtualScalaJSIRFile): PersistentIRFile = { + val file = files.getOrElseUpdate(irFile.path, + new PersistentIRFile(irFile.path)) + if (file.updateFile(irFile)) + statsReused += 1 + else + statsInvalidated += 1 + encodedNameToPersistentFile += ((file.info.encodedName, file)) + file + } + + def endRun(unCache: Boolean): Unit = { + // "Garbage-collect" persisted versions of files that have disappeared + files.retain((_, f) => f.cleanAfterRun(unCache)) + encodedNameToPersistentFile.clear() + } + } + + private final class PersistentIRFile(val path: String) { + import ir.Trees._ + + private[this] var existedInThisRun: Boolean = false + private[this] var desugaredUsedInThisRun: Boolean = false + + private[this] var irFile: VirtualScalaJSIRFile = null + private[this] var version: Option[String] = None + private[this] var _info: Infos.ClassInfo = null + private[this] var _tree: ClassDef = null + private[this] var _desugared: Desugared = null + + def startRun(): Unit = { + existedInThisRun = false + desugaredUsedInThisRun = false + } + + def updateFile(irFile: VirtualScalaJSIRFile): Boolean = { + existedInThisRun = true + this.irFile = irFile + if (version.isDefined && version == irFile.version) { + // yeepeeh, nothing to do + true + } else { + version = irFile.version + _info = irFile.info + _tree = null + _desugared = null + false + } + } + + def info: Infos.ClassInfo = _info + + def desugared: Desugared = { + desugaredUsedInThisRun = true + if (_desugared == null) + _desugared = new Desugared + _desugared + } + + def tree: ClassDef = { + if (_tree == null) + _tree = irFile.tree + _tree + } + + def treeIfChanged(lastVersion: Option[String]): Option[(ClassDef, Option[String])] = { + if (lastVersion.isDefined && lastVersion == version) None + else Some((tree, version)) + } + + /** Returns true if this file should be kept for the next run at all. */ + def cleanAfterRun(unCache: Boolean): Boolean = { + irFile = null + if (unCache && !desugaredUsedInThisRun) + _desugared = null // free desugared if unused in this run + existedInThisRun + } + } + + private final class Desugared { + // for class kinds that are not decomposed + val wholeClass = new OneTimeCache[js.Tree] + + val constructor = new OneTimeCache[js.Tree] + val methodNames = new OneTimeCache[List[String]] + val methods = mutable.Map.empty[String, js.Tree] + val exportedMembers = new OneTimeCache[js.Tree] + val typeData = new OneTimeCache[js.Tree] + val setTypeData = new OneTimeCache[js.Tree] + val moduleAccessor = new OneTimeCache[js.Tree] + val classExports = new OneTimeCache[js.Tree] + } + + private final class OneTimeCache[A >: Null] { + private[this] var value: A = null + def getOrElseUpdate(v: => A): A = { + if (value == null) + value = v + value + } + } +} |