Skip to content

Commit c217120

Browse files
authored
Add selective.resolveTree task to help debug selective execution misbehaviors (#4349)
Also lots of general refactoring for the SelectiveExecution/SpanningForest logic I need this myself to help debug #4343
1 parent 04699f1 commit c217120

File tree

11 files changed

+259
-137
lines changed

11 files changed

+259
-137
lines changed

build.mill

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,8 @@ trait MillStableScalaModule extends MillPublishScalaModule with Mima {
609609
ProblemFilter.exclude[Problem]("*.bspJvmBuildTarget"),
610610
ProblemFilter.exclude[Problem]("mill.scalalib.RunModule#RunnerImpl.*"),
611611
ProblemFilter.exclude[Problem]("mill.util.PromptLogger#*"),
612-
ProblemFilter.exclude[Problem]("mill.util.PromptLoggerUtil.*")
612+
ProblemFilter.exclude[Problem]("mill.util.PromptLoggerUtil.*"),
613+
ProblemFilter.exclude[Problem]("mill.main.SelectiveExecution*")
613614
)
614615
def mimaPreviousVersions: T[Seq[String]] = Settings.mimaBaseVersions
615616

example/large/selective/9-selective-execution/build.mill

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,50 @@ Test run bar.BarTests finished: 0 failed, 0 ignored, 1 total, ...
133133
// "foo.test" [color=red penwidth=2]
134134
// }
135135
// ```
136+
//
137+
// For a more detailed report of how the changed inputs resulted in the selected tasks
138+
// being chosen, you can also use `selective.resolveTree` to print out the selected tasks
139+
// as a JSON tree illustrating the relationships between the invalidated inputs (at the root
140+
// of the tree) and the selected tasks (at the leaves of the tree)
141+
//
142+
143+
/** Usage
144+
145+
> mill selective.resolveTree __.test
146+
{
147+
"bar.sources": {
148+
"bar.test.sources": {
149+
"bar.test.allSources": {
150+
"bar.test.allSourceFiles": {
151+
"bar.test.compile": {
152+
"bar.test.localRunClasspath": {
153+
"bar.test.testClasspath": {
154+
"bar.test.test": {}
155+
}
156+
}
157+
}
158+
}
159+
}
160+
},
161+
"bar.allSources": {
162+
"bar.allSourceFiles": {
163+
"bar.compile": {
164+
"bar.localRunClasspath": {
165+
"bar.localClasspath": {
166+
"foo.test.transitiveLocalClasspath": {
167+
"foo.test.runClasspath": {
168+
"foo.test.test": {}
169+
}
170+
}
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
*/
136180

137181
// Similarly, if we make a change `qux/`, using selective execution will only run tests
138182
// in `qux.test`, and skip those in `foo.test` and `bar.test`.

main/codesig/src/ResolvedCalls.scala

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mill.codesig
22
import JvmModel._
33
import JType.{Cls => JCls}
4+
import mill.util.SpanningForest
45
import mill.util.SpanningForest.breadthFirst
56
import upickle.default.{ReadWriter, macroRW}
67

@@ -31,12 +32,7 @@ object ResolvedCalls {
3132
val allDirectAncestors =
3233
localSummary.mapValues(_.directAncestors) ++ externalSummary.directAncestors
3334

34-
val directDescendents = {
35-
allDirectAncestors
36-
.toVector
37-
.flatMap { case (k, vs) => vs.map((_, k)) }
38-
.groupMap(_._1)(_._2)
39-
}
35+
val directDescendents = SpanningForest.reverseEdges(allDirectAncestors)
4036

4137
// Given an external class, what are the local classes that inherit from it,
4238
// and what local methods may end up being called by the external class code
@@ -122,16 +118,13 @@ object ResolvedCalls {
122118

123119
val allSamImplementors0 = allSamDefiners
124120
.toSeq
125-
.flatMap { case (cls, sig) =>
126-
breadthFirst(Seq(cls))(cls => directDescendents.getOrElse(cls, Nil)).map(_ -> sig)
121+
.map { case (cls, sig) =>
122+
sig -> breadthFirst(Seq(cls))(cls => directDescendents.getOrElse(cls, Nil))
127123
}
128124

129-
val allSamImplementors = allSamImplementors0
130-
.groupMap(_._1)(_._2)
131-
.view.mapValues(_.toSet)
132-
.toMap
125+
val allSamImplementors = mill.util.SpanningForest.reverseEdges(allSamImplementors0)
133126

134-
allSamImplementors
127+
allSamImplementors.mapValues(_.toSet).toMap
135128
}
136129

137130
val localCalls = {

main/eval/src/mill/eval/EvaluatorCore.scala

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,13 @@ private[mill] trait EvaluatorCore extends GroupEvaluator {
7272

7373
val threadNumberer = new ThreadNumberer()
7474
val (sortedGroups, transitive) = Plan.plan(goals)
75-
val interGroupDeps = findInterGroupDeps(sortedGroups)
75+
val interGroupDeps = EvaluatorCore.findInterGroupDeps(sortedGroups)
7676
val terminals0 = sortedGroups.keys().toVector
7777
val failed = new AtomicBoolean(false)
7878
val count = new AtomicInteger(1)
7979
val indexToTerminal = sortedGroups.keys().toArray
80-
val terminalToIndex = indexToTerminal.zipWithIndex.toMap
8180

82-
EvaluatorLogs.logDependencyTree(interGroupDeps, indexToTerminal, terminalToIndex, outPath)
81+
EvaluatorLogs.logDependencyTree(interGroupDeps, indexToTerminal, outPath)
8382

8483
// Prepare a lookup tables up front of all the method names that each class owns,
8584
// and the class hierarchy, so during evaluation it is cheap to look up what class
@@ -231,7 +230,6 @@ private[mill] trait EvaluatorCore extends GroupEvaluator {
231230
EvaluatorLogs.logInvalidationTree(
232231
interGroupDeps,
233232
indexToTerminal,
234-
terminalToIndex,
235233
outPath,
236234
uncached,
237235
changedValueHash
@@ -262,8 +260,10 @@ private[mill] trait EvaluatorCore extends GroupEvaluator {
262260
results.map { case (k, v) => (k, v.map(_._1)) }
263261
)
264262
}
263+
}
265264

266-
private def findInterGroupDeps(sortedGroups: MultiBiMap[Terminal, Task[_]])
265+
private[mill] object EvaluatorCore {
266+
def findInterGroupDeps(sortedGroups: MultiBiMap[Terminal, Task[_]])
267267
: Map[Terminal, Seq[Terminal]] = {
268268
sortedGroups
269269
.items()
@@ -277,10 +277,6 @@ private[mill] trait EvaluatorCore extends GroupEvaluator {
277277
}
278278
.toMap
279279
}
280-
}
281-
282-
private[mill] object EvaluatorCore {
283-
284280
case class Results(
285281
rawValues: Seq[Result[Val]],
286282
evaluated: Agg[Task[_]],

main/eval/src/mill/eval/EvaluatorLogs.scala

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,33 @@ private[mill] object EvaluatorLogs {
1010
def logDependencyTree(
1111
interGroupDeps: Map[Terminal, Seq[Terminal]],
1212
indexToTerminal: Array[Terminal],
13-
terminalToIndex: Map[Terminal, Int],
1413
outPath: os.Path
1514
): Unit = {
15+
val (vertexToIndex, edgeIndices) =
16+
SpanningForest.graphMapToIndices(indexToTerminal, interGroupDeps)
17+
1618
SpanningForest.writeJsonFile(
1719
outPath / OutFiles.millDependencyTree,
18-
indexToTerminal.map(t => interGroupDeps.getOrElse(t, Nil).map(terminalToIndex).toArray),
20+
edgeIndices,
1921
indexToTerminal.indices.toSet,
2022
indexToTerminal(_).render
2123
)
2224
}
2325
def logInvalidationTree(
2426
interGroupDeps: Map[Terminal, Seq[Terminal]],
2527
indexToTerminal: Array[Terminal],
26-
terminalToIndex: Map[Terminal, Int],
2728
outPath: os.Path,
2829
uncached: ConcurrentHashMap[Terminal, Unit],
2930
changedValueHash: ConcurrentHashMap[Terminal, Unit]
3031
): Unit = {
31-
32-
val reverseInterGroupDeps = interGroupDeps
33-
.iterator
34-
.flatMap { case (k, vs) => vs.map(_ -> k) }
35-
.toSeq
36-
.groupMap(_._1)(_._2)
32+
val reverseInterGroupDeps = SpanningForest.reverseEdges(interGroupDeps)
3733

3834
val changedTerminalIndices = changedValueHash.keys().asScala.toSet
39-
val downstreamIndexEdges = indexToTerminal
40-
.map(t =>
41-
if (changedTerminalIndices(t))
42-
reverseInterGroupDeps.getOrElse(t, Nil).map(terminalToIndex).toArray
43-
else Array.empty[Int]
44-
)
35+
36+
val (vertexToIndex, downstreamIndexEdges) = SpanningForest.graphMapToIndices(
37+
indexToTerminal,
38+
reverseInterGroupDeps.filterKeys(changedTerminalIndices).toMap
39+
)
4540

4641
val edgeSourceIndices = downstreamIndexEdges
4742
.zipWithIndex
@@ -53,7 +48,7 @@ private[mill] object EvaluatorLogs {
5348
downstreamIndexEdges,
5449
uncached.keys().asScala
5550
.flatMap { uncachedTask =>
56-
val uncachedIndex = terminalToIndex(uncachedTask)
51+
val uncachedIndex = vertexToIndex(uncachedTask)
5752
Option.when(
5853
// Filter out input and source tasks which do not cause downstream invalidations
5954
// from the invalidation tree, because most of them are un-interesting and the

main/src/mill/main/MainModule.scala

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package mill.main
22

33
import mill.api._
44
import mill.define._
5-
import mill.eval.{Evaluator, EvaluatorPaths}
5+
import mill.eval.{Evaluator, EvaluatorPaths, Terminal}
66
import mill.moduledefs.Scaladoc
77
import mill.resolve.SelectMode.Separated
88
import mill.resolve.{Resolve, SelectMode}
@@ -76,6 +76,23 @@ object MainModule {
7676
Result.Success(output)
7777
}
7878
}
79+
80+
def plan0(
81+
evaluator: Evaluator,
82+
tasks: Seq[String]
83+
): Either[String, Array[Terminal.Labelled[_]]] = {
84+
Resolve.Tasks.resolve(
85+
evaluator.rootModule,
86+
tasks,
87+
SelectMode.Multi
88+
) match {
89+
case Left(err) => Left(err)
90+
case Right(rs) =>
91+
val (sortedGroups, _) = evaluator.plan(rs)
92+
Right(sortedGroups.keys().collect { case r: Terminal.Labelled[_] => r }.toArray)
93+
}
94+
}
95+
7996
}
8097

8198
/**
@@ -121,7 +138,7 @@ trait MainModule extends BaseModule0 {
121138
*/
122139
def plan(evaluator: Evaluator, targets: String*): Command[Array[String]] =
123140
Task.Command(exclusive = true) {
124-
SelectiveExecution.plan0(evaluator, targets) match {
141+
MainModule.plan0(evaluator, targets) match {
125142
case Left(err) => Result.Failure(err)
126143
case Right(success) =>
127144
val renderedTasks = success.map(_.segments.render)
@@ -516,7 +533,7 @@ trait MainModule extends BaseModule0 {
516533
*/
517534
def visualizePlan(evaluator: Evaluator, targets: String*): Command[Seq[PathRef]] =
518535
Task.Command(exclusive = true) {
519-
SelectiveExecution.plan0(evaluator, targets) match {
536+
MainModule.plan0(evaluator, targets) match {
520537
case Left(err) => Result.Failure(err)
521538
case Right(planResults) => visualize0(
522539
evaluator,

main/src/mill/main/RunScript.scala

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,16 @@ object RunScript {
7272
if (
7373
selectiveExecutionEnabled && os.exists(evaluator.outPath / OutFiles.millSelectiveExecution)
7474
) {
75-
SelectiveExecution
76-
.diffMetadata(evaluator, targets.map(terminals(_).render).toSeq)
77-
.map { x =>
78-
val (selected, results) = x
79-
val selectedSet = selected.toSet
80-
(
81-
targets.filter(t => t.isExclusiveCommand || selectedSet(terminals(t).render)),
82-
results
83-
)
84-
}
85-
} else Right(targets -> Map.empty)
75+
val changedTasks = SelectiveExecution.computeChangedTasks0(evaluator, targets.toSeq)
76+
val selectedSet = changedTasks.downstreamTasks.map(_.ctx.segments.render).toSet
77+
(
78+
targets.filter(t => t.isExclusiveCommand || selectedSet(terminals(t).render)),
79+
changedTasks.results
80+
)
81+
} else (targets -> Map.empty)
8682

8783
selectedTargetsOrErr match {
88-
case Left(err) => (Nil, Left(err))
89-
case Right((selectedTargets, selectiveResults)) =>
84+
case (selectedTargets, selectiveResults) =>
9085
val evaluated: Results = evaluator.evaluate(selectedTargets, serialCommandExec = true)
9186
val watched = (evaluated.results.iterator ++ selectiveResults)
9287
.collect {

0 commit comments

Comments
 (0)