diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2017-10-22 18:50:45 -0700 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2017-10-22 18:50:45 -0700 |
commit | 3bfac52e0f81dfcdaf892d631ff14d7e9f9190cc (patch) | |
tree | 8283927f4a760307830aa9412913cb749e93b5d4 /src | |
parent | 991e404da10ce66a35206fe4e16784582503ee74 (diff) | |
download | mill-3bfac52e0f81dfcdaf892d631ff14d7e9f9190cc.tar.gz mill-3bfac52e0f81dfcdaf892d631ff14d7e9f9190cc.tar.bz2 mill-3bfac52e0f81dfcdaf892d631ff14d7e9f9190cc.zip |
- Basic dirty-checking of targets works; targets can now request to be recomputed independent of their inputs
- Split out `Target` and `TargetOps`, so `Target` can be a pristine description of what a subclass needs to implement
- Convert targets from `case class`es into normal `class`s, since none of the case class functionality is useful
- Disable parallel execution in tests, since we don't really need it right now and it jumbles up incremental test reporting
Diffstat (limited to 'src')
-rw-r--r-- | src/main/scala/forge/Evaluator.scala | 32 | ||||
-rw-r--r-- | src/main/scala/forge/Target.scala | 78 | ||||
-rw-r--r-- | src/main/scala/forge/Util.scala | 2 | ||||
-rw-r--r-- | src/test/scala/forge/ForgeTests.scala | 28 |
4 files changed, 98 insertions, 42 deletions
diff --git a/src/main/scala/forge/Evaluator.scala b/src/main/scala/forge/Evaluator.scala index fb45aa32..b0c8bf12 100644 --- a/src/main/scala/forge/Evaluator.scala +++ b/src/main/scala/forge/Evaluator.scala @@ -9,16 +9,16 @@ import scala.collection.mutable class Evaluator(workspacePath: jnio.Path, enclosingBase: DefCtx){ + val resultCache = mutable.Map.empty[Target[_], (Int, Any)] - - def apply[T](t: Target[T]) - (implicit enclosing: Enclosing): T = { + def evaluate(targets: Seq[Target[_]]): Evaluator.Results = { jnio.Files.createDirectories(workspacePath) - val sortedTargets = Evaluator.topoSortedTransitiveTargets(Seq(t)) + val sortedTargets = Evaluator.topoSortedTransitiveTargets(targets) + val evaluated = mutable.Buffer.empty[Target[_]] val results = mutable.Map.empty[Target[_], Any] for (target <- sortedTargets){ - val inputResults = target.inputs.map(results) + val inputResults = target.inputs.map(results).toIndexedSeq val targetDestPath = target.defCtx.staticEnclosing match{ case Some(enclosingStr) => @@ -30,15 +30,19 @@ class Evaluator(workspacePath: jnio.Path, case None => jnio.Files.createTempDirectory(null) } + val inputsHash = inputResults.hashCode + resultCache.get(target) match{ + case Some((hash, res)) if hash == inputsHash && !target.dirty => + results(target) = res + case _ => + evaluated.append(target) + val res = target.evaluate(new Args(inputResults, targetDestPath)) + resultCache(target) = (inputsHash, res) + results(target) = res + } - - - - results(target) = target.evaluate( - new Args(inputResults.toIndexedSeq, targetDestPath) - ) } - results(t).asInstanceOf[T] + Evaluator.Results(targets.map(results), evaluated) } def deleteRec(path: jnio.Path) = { if (jnio.Files.exists(path)){ @@ -54,21 +58,23 @@ class Evaluator(workspacePath: jnio.Path, object Evaluator{ + case class Results(values: Seq[Any], evaluated: Seq[Target[_]]) /** * Takes the given targets, finds */ def topoSortedTransitiveTargets(sourceTargets: Seq[Target[_]]) = { val transitiveTargetSet = mutable.Set.empty[Target[_]] + val transitiveTargets = mutable.Buffer.empty[Target[_]] def rec(t: Target[_]): Unit = { if (transitiveTargetSet.contains(t)) () // do nothing else { transitiveTargetSet.add(t) + transitiveTargets.append(t) t.inputs.foreach(rec) } } sourceTargets.foreach(rec) - val transitiveTargets = transitiveTargetSet.toVector val targetIndices = transitiveTargets.zipWithIndex.toMap val numberedEdges = diff --git a/src/main/scala/forge/Target.scala b/src/main/scala/forge/Target.scala index b2f837a7..acbb8449 100644 --- a/src/main/scala/forge/Target.scala +++ b/src/main/scala/forge/Target.scala @@ -1,72 +1,94 @@ package forge import java.nio.{file => jnio} - -trait Target[T]{ +trait TargetOps[T]{ this: Target[T] => val defCtx: DefCtx - - override def toString = defCtx.staticEnclosing match{ - case None => this.getClass.getSimpleName + "@" + Integer.toHexString(System.identityHashCode(this)) - case Some(s) => this.getClass.getName + "@" + s - } - val inputs: Seq[Target[_]] - def evaluate(args: Args): T - def map[V](f: T => V)(implicit defCtx: DefCtx) = { - Target.Mapped(this, f, defCtx) + new Target.Mapped(this, f, defCtx) } def zip[V](other: Target[V])(implicit defCtx: DefCtx) = { - Target.Zipped(this, other, defCtx) + new Target.Zipped(this, other, defCtx) } def ~[V, R](other: Target[V]) (implicit s: Implicits.Sequencer[T, V, R], defCtx: DefCtx): Target[R] = { this.zip(other).map(s.apply _ tupled) } + override def toString = defCtx.staticEnclosing match{ + case None => this.getClass.getSimpleName + "@" + Integer.toHexString(System.identityHashCode(this)) + case Some(s) => this.getClass.getName + "@" + s + } +} +trait Target[T] extends TargetOps[T]{ + /** + * Where in the Scala codebase was this target defined? + */ + val defCtx: DefCtx + /** + * What other Targets does this Target depend on? + */ + val inputs: Seq[Target[_]] + + /** + * Evaluate this target + */ + def evaluate(args: Args): T + + /** + * Even if this target's inputs did not change, does it need to re-evaluate + * anyway? e.g. if the files this target represents on disk changed + */ + def dirty: Boolean = false + } object Target{ - def test(inputs: Target[Int]*)(implicit defCtx: DefCtx) = Test(inputs, defCtx) + def test(inputs: Target[Int]*)(implicit defCtx: DefCtx) = new Test(inputs, defCtx) /** * A dummy target that takes any number of inputs, and whose output can be * controlled externally, so you can construct arbitrary dataflow graphs and * test how changes propagate. */ - case class Test(inputs: Seq[Target[Int]], defCtx: DefCtx) extends Target[Int]{ - var counter = 1 - def evaluate(args: Args) = counter + args.args.map(_.asInstanceOf[Int]).sum + class Test(val inputs: Seq[Target[Int]], val defCtx: DefCtx) extends Target[Int]{ + var counter = 0 + var lastCounter = counter + def evaluate(args: Args) = { + lastCounter = counter + counter + args.args.map(_.asInstanceOf[Int]).sum + } + override def dirty = lastCounter != counter } def traverse[T](source: Seq[Target[T]])(implicit defCtx: DefCtx) = { - Traverse[T](source, defCtx) + new Traverse[T](source, defCtx) } - case class Traverse[T](inputs: Seq[Target[T]], defCtx: DefCtx) extends Target[Seq[T]]{ + class Traverse[T](val inputs: Seq[Target[T]], val defCtx: DefCtx) extends Target[Seq[T]]{ def evaluate(args: Args) = { for (i <- 0 until args.length) yield args(i).asInstanceOf[T] } } - case class Mapped[T, V](source: Target[T], f: T => V, - defCtx: DefCtx) extends Target[V]{ + class Mapped[T, V](source: Target[T], f: T => V, + val defCtx: DefCtx) extends Target[V]{ def evaluate(args: Args) = f(args(0)) val inputs = List(source) } - case class Zipped[T, V](source1: Target[T], - source2: Target[V], - defCtx: DefCtx) extends Target[(T, V)]{ + class Zipped[T, V](source1: Target[T], + source2: Target[V], + val defCtx: DefCtx) extends Target[(T, V)]{ def evaluate(args: Args) = (args(0), args(0)) val inputs = List(source1, source1) } - def path(path: jnio.Path)(implicit defCtx: DefCtx) = Path(path, defCtx) - case class Path(path: jnio.Path, defCtx: DefCtx) extends Target[jnio.Path]{ + def path(path: jnio.Path)(implicit defCtx: DefCtx) = new Path(path, defCtx) + class Path(path: jnio.Path, val defCtx: DefCtx) extends Target[jnio.Path]{ def evaluate(args: Args) = path val inputs = Nil } - case class Subprocess(inputs: Seq[Target[_]], - command: Args => Seq[String], - defCtx: DefCtx) extends Target[Subprocess.Result] { + class Subprocess(val inputs: Seq[Target[_]], + command: Args => Seq[String], + val defCtx: DefCtx) extends Target[Subprocess.Result] { def evaluate(args: Args) = { jnio.Files.createDirectories(args.dest) diff --git a/src/main/scala/forge/Util.scala b/src/main/scala/forge/Util.scala index 89a3eafe..7beffd4e 100644 --- a/src/main/scala/forge/Util.scala +++ b/src/main/scala/forge/Util.scala @@ -11,7 +11,7 @@ import scala.collection.JavaConverters._ object Util{ def compileAll(sources: Target[Seq[jnio.Path]]) (implicit defCtx: DefCtx) = { - Target.Subprocess( + new Target.Subprocess( Seq(sources), args => Seq("javac") ++ diff --git a/src/test/scala/forge/ForgeTests.scala b/src/test/scala/forge/ForgeTests.scala index 8da52958..a869ae4a 100644 --- a/src/test/scala/forge/ForgeTests.scala +++ b/src/test/scala/forge/ForgeTests.scala @@ -52,6 +52,34 @@ object ForgeTests extends TestSuite{ Diamond.down ) ) + + 'evaluate - { + def check(targets: Seq[Target[_]], + values: Seq[Any], + evaluated: Seq[Target[_]]) = { + val Evaluator.Results(returnedValues, returnedEvaluated) = evaluator.evaluate(targets) + assert( + returnedValues == values, + returnedEvaluated == evaluated + ) + + } + 'singleton - { + import Singleton._ + // First time the target is evaluated + check(Seq(single), values = Seq(0), evaluated = Seq(single)) + // Second time the value is already cached, so no evaluation needed + check(Seq(single), values = Seq(0), evaluated = Seq()) + Singleton.single.counter += 1 + // After incrementing the counter, it forces re-evaluation + check(Seq(single), values = Seq(1), evaluated = Seq(single)) + // Then it's cached again + check(Seq(single), values = Seq(1), evaluated = Seq()) + } +// 'pair - { +// +// } + } } // 'full - { |