@@ -2,18 +2,17 @@ package mill.codesig
22
33import mill .codesig .JvmModel .*
44import mill .internal .{SpanningForest , Tarjans }
5- import ujson .Obj
5+ import ujson .{ Obj , Arr }
66import upickle .default .{Writer , writer }
77
88import scala .collection .immutable .SortedMap
9+ import scala .collection .mutable
910
1011class CallGraphAnalysis (
11- localSummary : LocalSummary ,
12- resolved : ResolvedCalls ,
13- externalSummary : ExternalSummary ,
14- ignoreCall : (Option [MethodDef ], MethodSig ) => Boolean ,
15- logger : Logger ,
16- prevTransitiveCallGraphHashesOpt : () => Option [Map [String , Int ]]
12+ val localSummary : LocalSummary ,
13+ val resolved : ResolvedCalls ,
14+ val externalSummary : ExternalSummary ,
15+ ignoreCall : (Option [MethodDef ], MethodSig ) => Boolean
1716)(implicit st : SymbolTable ) {
1817
1918 val methods : Map [MethodDef , LocalSummary .MethodInfo ] = for {
@@ -40,17 +39,13 @@ class CallGraphAnalysis(
4039 lazy val methodCodeHashes : SortedMap [String , Int ] =
4140 methods.map { case (k, vs) => (k.toString, vs.codeHash) }.to(SortedMap )
4241
43- logger.mandatoryLog(methodCodeHashes)
44-
4542 lazy val prettyCallGraph : SortedMap [String , Array [CallGraphAnalysis .Node ]] = {
4643 indexGraphEdges.zip(indexToNodes).map { case (vs, k) =>
4744 (k.toString, vs.map(indexToNodes))
4845 }
4946 .to(SortedMap )
5047 }
5148
52- logger.mandatoryLog(prettyCallGraph)
53-
5449 def transitiveCallGraphValues [V : scala.reflect.ClassTag ](
5550 nodeValues : Array [V ],
5651 reduce : (V , V ) => V ,
@@ -78,44 +73,45 @@ class CallGraphAnalysis(
7873 .collect { case (CallGraphAnalysis .LocalDef (d), v) => (d.toString, v) }
7974 .to(SortedMap )
8075
81- logger.mandatoryLog(transitiveCallGraphHashes0)
82- logger.log(transitiveCallGraphHashes)
83-
84- lazy val spanningInvalidationTree : Obj = prevTransitiveCallGraphHashesOpt() match {
85- case Some (prevTransitiveCallGraphHashes) =>
86- CallGraphAnalysis .spanningInvalidationTree(
87- prevTransitiveCallGraphHashes,
88- transitiveCallGraphHashes0,
89- indexToNodes,
90- indexGraphEdges
91- )
92- case None => ujson.Obj ()
76+ def calculateSpanningInvalidationTree (
77+ prevTransitiveCallGraphHashesOpt : => Option [Map [String , Int ]]
78+ ): Obj = {
79+ prevTransitiveCallGraphHashesOpt match {
80+ case Some (prevTransitiveCallGraphHashes) =>
81+ CallGraphAnalysis .spanningInvalidationTree(
82+ prevTransitiveCallGraphHashes,
83+ transitiveCallGraphHashes0,
84+ indexToNodes,
85+ indexGraphEdges
86+ )
87+ case None => ujson.Obj ()
88+ }
9389 }
9490
95- logger.mandatoryLog(spanningInvalidationTree)
91+ def calculateInvalidClassName (
92+ prevTransitiveCallGraphHashesOpt : => Option [Map [String , Int ]]
93+ ): Set [String ] = {
94+ prevTransitiveCallGraphHashesOpt match {
95+ case Some (prevTransitiveCallGraphHashes) =>
96+ CallGraphAnalysis .invalidClassNames(
97+ prevTransitiveCallGraphHashes,
98+ transitiveCallGraphHashes0,
99+ indexToNodes,
100+ indexGraphEdges
101+ )
102+ case None => Set .empty
103+ }
104+ }
96105}
97106
98107object CallGraphAnalysis {
99108
100- /**
101- * Computes the minimal spanning forest of the that covers the nodes in the
102- * call graph whose transitive call graph hashes has changed since the last
103- * run, rendered as a JSON dictionary tree. This provides a great "debug
104- * view" that lets you easily Cmd-F to find a particular node and then trace
105- * it up the JSON hierarchy to figure out what upstream node was the root
106- * cause of the change in the callgraph.
107- *
108- * There are typically multiple possible spanning forests for a given graph;
109- * one is chosen arbitrarily. This is usually fine, since when debugging you
110- * typically are investigating why there's a path to a node at all where none
111- * should exist, rather than trying to fully analyse all possible paths
112- */
113- def spanningInvalidationTree (
109+ private def getSpanningForest (
114110 prevTransitiveCallGraphHashes : Map [String , Int ],
115111 transitiveCallGraphHashes0 : Array [(CallGraphAnalysis .Node , Int )],
116112 indexToNodes : Array [Node ],
117113 indexGraphEdges : Array [Array [Int ]]
118- ): ujson. Obj = {
114+ ) = {
119115 val transitiveCallGraphHashes0Map = transitiveCallGraphHashes0.toMap
120116
121117 val nodesWithChangedHashes = indexGraphEdges
@@ -135,12 +131,64 @@ object CallGraphAnalysis {
135131 val reverseGraphEdges =
136132 indexGraphEdges.indices.map(reverseGraphMap.getOrElse(_, Array [Int ]())).toArray
137133
134+ SpanningForest .apply(reverseGraphEdges, nodesWithChangedHashes, false )
135+ }
136+
137+ /**
138+ * Computes the minimal spanning forest of the that covers the nodes in the
139+ * call graph whose transitive call graph hashes has changed since the last
140+ * run, rendered as a JSON dictionary tree. This provides a great "debug
141+ * view" that lets you easily Cmd-F to find a particular node and then trace
142+ * it up the JSON hierarchy to figure out what upstream node was the root
143+ * cause of the change in the callgraph.
144+ *
145+ * There are typically multiple possible spanning forests for a given graph;
146+ * one is chosen arbitrarily. This is usually fine, since when debugging you
147+ * typically are investigating why there's a path to a node at all where none
148+ * should exist, rather than trying to fully analyse all possible paths
149+ */
150+ def spanningInvalidationTree (
151+ prevTransitiveCallGraphHashes : Map [String , Int ],
152+ transitiveCallGraphHashes0 : Array [(CallGraphAnalysis .Node , Int )],
153+ indexToNodes : Array [Node ],
154+ indexGraphEdges : Array [Array [Int ]]
155+ ): ujson.Obj = {
138156 SpanningForest .spanningTreeToJsonTree(
139- SpanningForest .apply(reverseGraphEdges, nodesWithChangedHashes, false ),
157+ getSpanningForest(prevTransitiveCallGraphHashes, transitiveCallGraphHashes0, indexToNodes, indexGraphEdges ),
140158 k => indexToNodes(k).toString
141159 )
142160 }
143161
162+ /**
163+ * Get all class names that have their hashcode changed compared to prevTransitiveCallGraphHashes
164+ */
165+ def invalidClassNames (
166+ prevTransitiveCallGraphHashes : Map [String , Int ],
167+ transitiveCallGraphHashes0 : Array [(CallGraphAnalysis .Node , Int )],
168+ indexToNodes : Array [Node ],
169+ indexGraphEdges : Array [Array [Int ]]
170+ ): Set [String ] = {
171+ val rootNode = getSpanningForest(prevTransitiveCallGraphHashes, transitiveCallGraphHashes0, indexToNodes, indexGraphEdges)
172+
173+ val jsonValueQueue = mutable.ArrayDeque [(Int , SpanningForest .Node )]()
174+ jsonValueQueue.appendAll(rootNode.values.toSeq)
175+ val invalidClassNames = Set .newBuilder[String ]
176+
177+ while (jsonValueQueue.nonEmpty) {
178+ val (nodeIndex, node) = jsonValueQueue.removeHead()
179+ node.values.foreach { case (childIndex, childNode) =>
180+ jsonValueQueue.append((childIndex, childNode))
181+ }
182+ 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)
186+ }
187+ }
188+
189+ invalidClassNames.result()
190+ }
191+
144192 def indexGraphEdges (
145193 indexToNodes : Array [Node ],
146194 methods : Map [MethodDef , LocalSummary .MethodInfo ],
0 commit comments