Skip to content

Commit e8b5640

Browse files
committed
update 3
1 parent cce94ef commit e8b5640

File tree

4 files changed

+58
-124
lines changed

4 files changed

+58
-124
lines changed

core/codesig/src/mill/codesig/ReachabilityAnalysis.scala

+8-8
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ class CallGraphAnalysis(
8888
}
8989
}
9090

91-
def calculateInvalidClassName(
91+
def calculateInvalidatedClassNames(
9292
prevTransitiveCallGraphHashesOpt: => Option[Map[String, Int]]
9393
): Set[String] = {
9494
prevTransitiveCallGraphHashesOpt match {
9595
case Some(prevTransitiveCallGraphHashes) =>
96-
CallGraphAnalysis.invalidClassNames(
96+
CallGraphAnalysis.invalidatedClassNames(
9797
prevTransitiveCallGraphHashes,
9898
transitiveCallGraphHashes0,
9999
indexToNodes,
@@ -162,7 +162,7 @@ object CallGraphAnalysis {
162162
/**
163163
* Get all class names that have their hashcode changed compared to prevTransitiveCallGraphHashes
164164
*/
165-
def invalidClassNames(
165+
def invalidatedClassNames(
166166
prevTransitiveCallGraphHashes: Map[String, Int],
167167
transitiveCallGraphHashes0: Array[(CallGraphAnalysis.Node, Int)],
168168
indexToNodes: Array[Node],
@@ -172,21 +172,21 @@ object CallGraphAnalysis {
172172

173173
val jsonValueQueue = mutable.ArrayDeque[(Int, SpanningForest.Node)]()
174174
jsonValueQueue.appendAll(rootNode.values.toSeq)
175-
val invalidClassNames = Set.newBuilder[String]
175+
val builder = Set.newBuilder[String]
176176

177177
while (jsonValueQueue.nonEmpty) {
178178
val (nodeIndex, node) = jsonValueQueue.removeHead()
179179
node.values.foreach { case (childIndex, childNode) =>
180180
jsonValueQueue.append((childIndex, childNode))
181181
}
182182
indexToNodes(nodeIndex) match {
183-
case CallGraphAnalysis.LocalDef(methodDef) => invalidClassNames.addOne(methodDef.cls.name)
184-
case CallGraphAnalysis.Call(methodCall) => invalidClassNames.addOne(methodCall.cls.name)
185-
case CallGraphAnalysis.ExternalClsCall(externalCls) => invalidClassNames.addOne(externalCls.name)
183+
case CallGraphAnalysis.LocalDef(methodDef) => builder.addOne(methodDef.cls.name)
184+
case CallGraphAnalysis.Call(methodCall) => builder.addOne(methodCall.cls.name)
185+
case CallGraphAnalysis.ExternalClsCall(externalCls) => builder.addOne(externalCls.name)
186186
}
187187
}
188188

189-
invalidClassNames.result()
189+
builder.result()
190190
}
191191

192192
def indexGraphEdges(

runner/src/mill/runner/MillBuildRootModule.scala

+37-1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,42 @@ abstract class MillBuildRootModule()(implicit
142142

143143
@internal
144144
override protected def callGraphAnalysisIgnoreCalls(callSiteOpt: Option[MethodDef], calledSig: MethodSig): Boolean = {
145+
// We can ignore all calls to methods that look like Targets when traversing
146+
// the call graph. We can do this because we assume `def` Targets are pure,
147+
// and so any changes in their behavior will be picked up by the runtime build
148+
// graph evaluator without needing to be accounted for in the post-compile
149+
// bytecode callgraph analysis.
150+
def isSimpleTarget(desc: mill.codesig.JvmModel.Desc) =
151+
(desc.ret.pretty == classOf[mill.define.Target[?]].getName ||
152+
desc.ret.pretty == classOf[mill.define.Worker[?]].getName) &&
153+
desc.args.isEmpty
154+
155+
// We avoid ignoring method calls that are simple trait forwarders, because
156+
// we need the trait forwarders calls to be counted in order to wire up the
157+
// method definition that a Target is associated with during evaluation
158+
// (e.g. `myModuleObject.myTarget`) with its implementation that may be defined
159+
// somewhere else (e.g. `trait MyModuleTrait{ def myTarget }`). Only that one
160+
// step is necessary, after that the runtime build graph invalidation logic can
161+
// take over
162+
def isForwarderCallsiteOrLambda =
163+
callSiteOpt.nonEmpty && {
164+
val callSiteSig = callSiteOpt.get.sig
165+
166+
(callSiteSig.name == (calledSig.name + "$") &&
167+
callSiteSig.static &&
168+
callSiteSig.desc.args.size == 1)
169+
|| (
170+
// In Scala 3, lambdas are implemented by private instance methods,
171+
// not static methods, so they fall through the crack of "isSimpleTarget".
172+
// Here make the assumption that a zero-arg lambda called from a simpleTarget,
173+
// should in fact be tracked. e.g. see `integration.invalidation[codesig-hello]`,
174+
// where the body of the `def foo` target is a zero-arg lambda i.e. the argument
175+
// of `Cacher.cachedTarget`.
176+
// To be more precise I think ideally we should capture more information in the signature
177+
isSimpleTarget(callSiteSig.desc) && calledSig.name.contains("$anonfun")
178+
)
179+
}
180+
145181
// We ignore Commands for the same reason as we ignore Targets, and also because
146182
// their implementations get gathered up all the via the `Discover` macro, but this
147183
// is primarily for use as external entrypoints and shouldn't really be counted as
@@ -160,7 +196,7 @@ abstract class MillBuildRootModule()(implicit
160196
calledSig.name == "millDiscover" ||
161197
callSiteOpt.exists(_.sig.name == "millDiscover")
162198

163-
super.callGraphAnalysisIgnoreCalls(callSiteOpt, calledSig) || isCommand || isMillDiscover
199+
(isSimpleTarget(calledSig.desc) && !isForwarderCallsiteOrLambda) || isCommand || isMillDiscover
164200
}
165201

166202
def codeSignatures: T[Map[String, Int]] = Task(persistent = true) {

scalalib/src/mill/scalalib/JavaModule.scala

+13-49
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import mill.util.Jvm
2020

2121
import os.Path
2222
import scala.util.Try
23+
import scala.annotation.unused
2324

2425
/**
2526
* Core configuration required to compile a single Java compilation target
@@ -108,6 +109,7 @@ trait JavaModule
108109
def testQuick(args: String*): Command[(String, Seq[TestResult])] = Task.Command(persistent = true) {
109110
val quicktestFailedClassesLog = Task.dest / "quickTestFailedClasses.json"
110111
val transitiveCallGraphHashes0 = Task.dest / "transitiveCallGraphHashes0.json"
112+
val invalidatedClassNamesLog = Task.dest / "invalidatedClassNames.json"
111113

112114
val classFiles: Seq[os.Path] = callGraphAnalysisClasspath()
113115
.flatMap(os.walk(_).filter(_.ext == "class"))
@@ -120,7 +122,7 @@ trait JavaModule
120122
ignoreCall = (callSiteOpt, calledSig) => callGraphAnalysisIgnoreCalls(callSiteOpt, calledSig)
121123
)
122124
val testClasses = testForkGrouping()
123-
val (quickTestClassLists, invalidClassNames) = if (!os.exists(transitiveCallGraphHashes0)) {
125+
val (quickTestClassLists, invalidatedClassNames) = if (!os.exists(transitiveCallGraphHashes0)) {
124126
// cannot calcuate invalid classes, so test all classes
125127
testClasses -> Set.empty[String]
126128
} else {
@@ -133,15 +135,15 @@ trait JavaModule
133135
}.getOrElse(Seq.empty[String]).toSet
134136
}
135137

136-
val invalidClassNames = callAnalysis.calculateInvalidClassName {
138+
val invalidatedClassNames = callAnalysis.calculateInvalidatedClassNames {
137139
Some(upickle.default.read[Map[String, Int]](os.read.stream(transitiveCallGraphHashes0)))
138140
}
139141

140-
val testingClasses = invalidClassNames ++ failedTestClasses
142+
val testingClasses = invalidatedClassNames ++ failedTestClasses
141143

142144
testClasses
143145
.map(_.filter(testingClasses.contains))
144-
.filter(_.nonEmpty) -> invalidClassNames
146+
.filter(_.nonEmpty) -> invalidatedClassNames
145147
}
146148

147149
// Clean up the directory for test runners
@@ -171,7 +173,7 @@ trait JavaModule
171173

172174
val results = testModuleUtil.runTests()
173175

174-
val badTestClasses = results match {
176+
val badTestClasses = (results match {
175177
case Result.Failure(_) =>
176178
// Consider all quick testing classes as failed
177179
quickTestClassLists.flatten
@@ -180,11 +182,11 @@ trait JavaModule
180182
results
181183
.filter(testResult => Set("Error", "Failure").contains(testResult.status))
182184
.map(_.fullyQualifiedName)
183-
}
185+
}).distinct
184186

185-
os.write.over(quicktestFailedClassesLog, upickle.default.write(badTestClasses.distinct))
187+
os.write.over(quicktestFailedClassesLog, upickle.default.write(badTestClasses))
186188
os.write.over(transitiveCallGraphHashes0, upickle.default.write(callAnalysis.transitiveCallGraphHashes0))
187-
os.write.over(Task.dest / "invalidClasses.json", upickle.default.write(invalidClassNames))
189+
os.write.over(invalidatedClassNamesLog, upickle.default.write(invalidatedClassNames))
188190

189191
results match {
190192
case Result.Failure(errMsg) => Result.Failure(errMsg)
@@ -1486,47 +1488,9 @@ trait JavaModule
14861488

14871489
@internal
14881490
protected def callGraphAnalysisIgnoreCalls(
1489-
callSiteOpt: Option[mill.codesig.JvmModel.MethodDef],
1490-
calledSig: mill.codesig.JvmModel.MethodSig
1491-
): Boolean = {
1492-
// We can ignore all calls to methods that look like Targets when traversing
1493-
// the call graph. We can do this because we assume `def` Targets are pure,
1494-
// and so any changes in their behavior will be picked up by the runtime build
1495-
// graph evaluator without needing to be accounted for in the post-compile
1496-
// bytecode callgraph analysis.
1497-
def isSimpleTarget(desc: mill.codesig.JvmModel.Desc) =
1498-
(desc.ret.pretty == classOf[mill.define.Target[?]].getName ||
1499-
desc.ret.pretty == classOf[mill.define.Worker[?]].getName) &&
1500-
desc.args.isEmpty
1501-
1502-
// We avoid ignoring method calls that are simple trait forwarders, because
1503-
// we need the trait forwarders calls to be counted in order to wire up the
1504-
// method definition that a Target is associated with during evaluation
1505-
// (e.g. `myModuleObject.myTarget`) with its implementation that may be defined
1506-
// somewhere else (e.g. `trait MyModuleTrait{ def myTarget }`). Only that one
1507-
// step is necessary, after that the runtime build graph invalidation logic can
1508-
// take over
1509-
def isForwarderCallsiteOrLambda =
1510-
callSiteOpt.nonEmpty && {
1511-
val callSiteSig = callSiteOpt.get.sig
1512-
1513-
(callSiteSig.name == (calledSig.name + "$") &&
1514-
callSiteSig.static &&
1515-
callSiteSig.desc.args.size == 1)
1516-
|| (
1517-
// In Scala 3, lambdas are implemented by private instance methods,
1518-
// not static methods, so they fall through the crack of "isSimpleTarget".
1519-
// Here make the assumption that a zero-arg lambda called from a simpleTarget,
1520-
// should in fact be tracked. e.g. see `integration.invalidation[codesig-hello]`,
1521-
// where the body of the `def foo` target is a zero-arg lambda i.e. the argument
1522-
// of `Cacher.cachedTarget`.
1523-
// To be more precise I think ideally we should capture more information in the signature
1524-
isSimpleTarget(callSiteSig.desc) && calledSig.name.contains("$anonfun")
1525-
)
1526-
}
1527-
1528-
isSimpleTarget(calledSig.desc) && !isForwarderCallsiteOrLambda
1529-
}
1491+
@unused callSiteOpt: Option[mill.codesig.JvmModel.MethodDef],
1492+
@unused calledSig: mill.codesig.JvmModel.MethodSig
1493+
): Boolean = false
15301494

15311495
@internal
15321496
protected def callGraphAnalysisClasspath: Task[Seq[os.Path]] = Task.Anon {

scalalib/src/mill/scalalib/PrefixTrie.scala

-66
This file was deleted.

0 commit comments

Comments
 (0)