Skip to content

Commit 0c6f0b9

Browse files
authored
Improve visualize user experience (#3438)
* Swap over from graphviz-java's default J2V8 engine to https://github.com/caoccao/Javet since J2V8 is unmaintained and does not support mac-arm64. This allows `visualize` to be used on M1 Macbooks without needing to install `dot`/`graphviz`. The implementation was adapted from https://github.com/nidi3/graphviz-java/blob/master/graphviz-java/src/main/java/guru/nidi/graphviz/engine/V8JavascriptEngine.java but with the J2V8 code adjusted to fit Janet * Print out the `visualize` output by default without needing `show` * Bundle `logback` to make the annoying log4j warnings go away Tested OS-X locally on my machine after uninstalling graphviz, Windows and Linux are exercised in CI (GH actions doesn't have graphviz installed)
1 parent d4733e9 commit 0c6f0b9

File tree

8 files changed

+75
-9
lines changed

8 files changed

+75
-9
lines changed

build.sc

+14-3
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,19 @@ object Deps {
128128
val castor = ivy"com.lihaoyi::castor:0.3.0"
129129
val fastparse = ivy"com.lihaoyi::fastparse:3.1.1"
130130
val flywayCore = ivy"org.flywaydb:flyway-core:8.5.13"
131-
val graphvizJava = ivy"guru.nidi:graphviz-java-all-j2v8:0.18.1"
131+
val graphvizJava = Seq(
132+
ivy"guru.nidi:graphviz-java-min-deps:0.18.1",
133+
ivy"org.webjars.npm:viz.js-graphviz-java:2.1.3",
134+
ivy"org.apache.xmlgraphics:batik-rasterizer:1.17"
135+
)
132136
val junixsocket = ivy"com.kohlschutter.junixsocket:junixsocket-core:2.10.0"
133137

134138
val jgraphtCore = ivy"org.jgrapht:jgrapht-core:1.4.0" // 1.5.0+ dont support JDK8
139+
val javet = Seq(
140+
ivy"com.caoccao.javet:javet:3.1.5",
141+
ivy"com.caoccao.javet:javet-linux-arm64:3.1.5",
142+
ivy"com.caoccao.javet:javet-macos:3.1.5",
143+
)
135144

136145
val jline = ivy"org.jline:jline:3.26.3"
137146
val jnaVersion = "5.14.0"
@@ -180,6 +189,7 @@ object Deps {
180189
val fansi = ivy"com.lihaoyi::fansi:0.5.0"
181190
val jarjarabrams = ivy"com.eed3si9n.jarjarabrams::jarjar-abrams-core:1.14.0"
182191
val requests = ivy"com.lihaoyi::requests:0.9.0"
192+
val logback = ivy"ch.qos.logback:logback-classic:1.2.11"
183193
val sonatypeCentralClient = ivy"com.lumidion::sonatype-central-client-requests:0.3.0"
184194

185195
object RuntimeDeps {
@@ -579,7 +589,8 @@ object main extends MillStableScalaModule with BuildInfo {
579589
Deps.windowsAnsi,
580590
Deps.mainargs,
581591
Deps.coursierInterface,
582-
Deps.requests
592+
Deps.requests,
593+
Deps.logback
583594
)
584595

585596
def compileIvyDeps = Agg(Deps.scalaReflect(scalaVersion()))
@@ -748,7 +759,7 @@ object main extends MillStableScalaModule with BuildInfo {
748759
}
749760
object graphviz extends MillPublishScalaModule {
750761
def moduleDeps = Seq(main, scalalib)
751-
def ivyDeps = Agg(Deps.graphvizJava, Deps.jgraphtCore)
762+
def ivyDeps = Agg(Deps.jgraphtCore) ++ Deps.graphvizJava ++ Deps.javet
752763
}
753764

754765

docs/modules/ROOT/pages/Case_Study_Mill_vs_Gradle.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ remains faster than Gradle by about 2.0x.
186186
Another area that Mill does better than Gradle is providing builtin tools for you to understand
187187
what your build is doing. For example, the Mockito project build discussed has 22 submodules
188188
and associated test suites, but how do these different modules depend on each other? With
189-
Mill, you can run `./mill show visualize __.compile`, and it will show you how the
189+
Mill, you can run `./mill visualize __.compile`, and it will show you how the
190190
`compile` task of each module depends on the others:
191191

192192
image::MockitoCompileGraph.svg[]

docs/modules/ROOT/pages/Case_Study_Mill_vs_Maven.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ rather than configuring a bunch of third-party plugins to try and achieve what y
596596
Another area that Mill does better than Maven is providing builtin tools for you to understand
597597
what your build is doing. For example, the Netty project build discussed has 47 submodules
598598
and associated test suites, but how do these different modules depend on each other? With
599-
Mill, you can run `./mill show visualize __.compile`, and it will show you how the
599+
Mill, you can run `./mill visualize __.compile`, and it will show you how the
600600
`compile` task of each module depends on the others:
601601

602602
image::NettyCompileGraph.svg[]

docs/modules/ROOT/pages/Case_Study_Mill_vs_SBT.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ wondering what your build tool is doing.
233233
Another area that Mill does better than SBT is providing builtin tools for you to understand
234234
what your build is doing. For example, the Gatling project build discussed has 21 submodules
235235
and associated test suites, but how do these different modules depend on each other? With
236-
Mill, you can run `./mill show visualize __.compile`, and it will show you how the
236+
Mill, you can run `./mill visualize __.compile`, and it will show you how the
237237
`compile` task of each module depends on the others:
238238

239239
image::GatlingCompileGraph.svg[]

example/scalalib/basic/4-builtin-commands/build.sc

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ foo.compileClasspath
291291
// == visualize
292292
//
293293
/** Usage
294-
> mill show visualize foo._
294+
> mill visualize foo._
295295
[
296296
".../out/visualize.dest/out.txt",
297297
".../out/visualize.dest/out.dot",

main/graphviz/src/mill/main/graphviz/GraphvizTools.scala

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package mill.main.graphviz
22

3+
import com.caoccao.javet.annotations.V8Function
4+
import com.caoccao.javet.interception.logging.JavetStandardConsoleInterceptor
5+
import com.caoccao.javet.interop.{V8Host, V8Runtime}
36
import guru.nidi.graphviz.attribute.Rank.RankDir
47
import guru.nidi.graphviz.attribute.{Rank, Shape, Style}
8+
import guru.nidi.graphviz.engine.{AbstractJavascriptEngine, AbstractJsGraphvizEngine, ResultHandler}
59
import mill.api.PathRef
610
import mill.define.NamedTask
711
import org.jgrapht.graph.{DefaultEdge, SimpleDirectedGraph}
12+
import org.slf4j.LoggerFactory
13+
import org.slf4j.Logger
814

915
object GraphvizTools {
1016

@@ -61,17 +67,58 @@ object GraphvizTools {
6167

6268
g = g.graphAttr().`with`(Rank.dir(RankDir.LEFT_TO_RIGHT))
6369

64-
val gv = Graphviz.fromGraph(g).totalMemory(100 * 1000 * 1000)
70+
Graphviz.useEngine(new AbstractJsGraphvizEngine(true, () => new V8JavascriptEngine()) {})
71+
val gv = Graphviz.fromGraph(g).totalMemory(128 * 1024 * 1024)
6572
val outputs = Seq(
6673
Format.PLAIN -> "out.txt",
6774
Format.XDOT -> "out.dot",
6875
Format.JSON -> "out.json",
6976
Format.PNG -> "out.png",
7077
Format.SVG -> "out.svg"
7178
)
79+
7280
for ((fmt, name) <- outputs) {
7381
gv.render(fmt).toFile((dest / name).toIO)
7482
}
7583
outputs.map(x => mill.PathRef(dest / x._2))
7684
}
7785
}
86+
87+
class V8JavascriptEngine() extends AbstractJavascriptEngine {
88+
val LOG: Logger = LoggerFactory.getLogger(classOf[V8JavascriptEngine])
89+
val v8Runtime: V8Runtime = V8Host.getV8Instance().createV8Runtime()
90+
LOG.info("Starting V8 runtime...")
91+
LOG.info("Started V8 runtime. Initializing javascript...")
92+
val resultHandler = new ResultHandler
93+
val javetStandardConsoleInterceptor = new JavetStandardConsoleInterceptor(v8Runtime)
94+
javetStandardConsoleInterceptor.register(v8Runtime.getGlobalObject)
95+
96+
class ResultHandlerInterceptor(resultHandler: ResultHandler) {
97+
@V8Function
98+
def result(s: String): Unit = resultHandler.setResult(s)
99+
100+
@V8Function
101+
def error(s: String): Unit = resultHandler.setError(s)
102+
103+
@V8Function
104+
def log(s: String): Unit = resultHandler.log(s)
105+
}
106+
val v8ValueObject = v8Runtime.createV8ValueObject
107+
v8Runtime.getGlobalObject.set("resultHandlerInterceptor", v8ValueObject)
108+
v8ValueObject.bind(new ResultHandlerInterceptor(resultHandler))
109+
110+
v8Runtime.getExecutor(
111+
"var result = resultHandlerInterceptor.result; " +
112+
"var error = resultHandlerInterceptor.error; " +
113+
"var log = resultHandlerInterceptor.log; "
114+
).execute()
115+
116+
LOG.info("Initialized javascript.")
117+
118+
override protected def execute(js: String): String = {
119+
v8Runtime.getExecutor(js).execute()
120+
resultHandler.waitFor
121+
}
122+
123+
override def close(): Unit = v8Runtime.close()
124+
}

main/src/mill/main/MainModule.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,11 @@ trait MainModule extends BaseModule0 {
435435
): Result[Seq[PathRef]] = {
436436
val (in, out) = vizWorker
437437
in.put((rs, allRs, ctx.dest))
438-
out.take()
438+
val res = out.take()
439+
res.map { v =>
440+
println(upickle.default.write(v.map(_.path.toString()), indent = 2))
441+
v
442+
}
439443
}
440444

441445
Resolve.Tasks.resolve(

readme.adoc

+4
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ as excluded {link-pr}/3329[#3329]
362362
** Optimizations to Mill evaluation logic to reduce fixed overhead of running Mill
363363
on large projects {link-pr}/3388[#3388]
364364

365+
** Improvements to `visualize` and `visualizePlan` such that they no longer need to be
366+
prefixed with `show` and no longer need a separate `graphviz`/`dot` install on Mac-OSX
367+
{link-pr}/3438[#3438]
368+
365369

366370
[#0-11-12]
367371
=== 0.11.12 - 2024-08-20

0 commit comments

Comments
 (0)