diff --git a/README.md b/README.md index ab20fbaa2..af98ed9ff 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This plugin is an auto-plugin which will be automatically enabled starting from * `whatDependsOn `: Find out what depends on an artifact. Shows a reverse dependency tree for the selected module. * `dependencyLicenseInfo`: show dependencies grouped by declared license + * `dependencyStats`: Shows a table with each module a row with (transitive) Jar sizes and number of dependencies * `dependencyGraphMl`: Generates a `.graphml` file with the project's dependencies to `target/dependencies-.graphml`. Use e.g. [yEd](http://www.yworks.com/en/products_yed_about.html) to format the graph to your needs. * `dependencyDot`: Generates a .dot file with the project's dependencies to `target/dependencies-.dot`. diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala index a5b33d26e..ace01d160 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala @@ -55,6 +55,8 @@ trait DependencyGraphKeys { "Prints an ascii tree of all the dependencies to the console") val dependencyList = TaskKey[Unit]("dependency-list", "Prints a list of all dependencies to the console") + val dependencyStats = TaskKey[Unit]("dependency-stats", + "Prints statistics for all dependencies to the console") val ivyReportFunction = TaskKey[String ⇒ File]("ivy-report-function", "A function which returns the file containing the ivy report from the ivy cache for a given configuration") val ivyReport = TaskKey[File]("ivy-report", diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala index 776eef144..e954de208 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala @@ -86,7 +86,8 @@ object DependencyGraphSettings { java.awt.Desktop.getDesktop.browse(uri) uri }, - dependencyList <<= (moduleGraph, streams).map((graph, streams) ⇒ streams.log.info(rendering.FlatList.render(graph, _.id.idString))), + dependencyList <<= printFromGraph(rendering.FlatList.render(_, _.id.idString)), + dependencyStats <<= printFromGraph(rendering.Statistics.renderModuleStatsList), dependencyDotHeader := """digraph "dependency-graph" { | graph[rankdir="LR"] | edge [ @@ -159,6 +160,9 @@ object DependencyGraphSettings { def print(key: TaskKey[String]) = (streams, key) map (_.log.info(_)) + def printFromGraph(f: ModuleGraph ⇒ String) = + (streams, moduleGraph) map ((streams, graph) ⇒ streams.log.info(f(graph))) + def showLicenseInfo(graph: ModuleGraph, streams: TaskStreams) { val output = graph.nodes.filter(_.isUsed).groupBy(_.license).toSeq.sortBy(_._1).map { diff --git a/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala b/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala index 432fb80e0..f5596269a 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/backend/SbtUpdateReport.scala @@ -34,7 +34,13 @@ object SbtUpdateReport { def moduleEdge(chosenVersion: Option[String])(report: ModuleReport): (Module, Seq[Edge]) = { val evictedByVersion = if (report.evicted) chosenVersion else None - (Module(report.module, license = report.licenses.headOption.map(_._1), evictedByVersion = evictedByVersion, error = report.problem), + val jarFile = report.artifacts.find(_._1.`type` == "jar").orElse(report.artifacts.find(_._1.extension == "jar")).map(_._2) + (Module( + id = report.module, + license = report.licenses.headOption.map(_._1), + evictedByVersion = evictedByVersion, + jarFile = jarFile, + error = report.problem), report.callers.map(caller ⇒ Edge(caller.caller, report.module))) } diff --git a/src/main/scala/net/virtualvoid/sbt/graph/model.scala b/src/main/scala/net/virtualvoid/sbt/graph/model.scala index e833d34ab..06c2b5bac 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/model.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/model.scala @@ -16,6 +16,8 @@ package net.virtualvoid.sbt.graph +import java.io.File + import scala.collection.mutable.{ MultiMap, HashMap, Set } case class ModuleId(organisation: String, @@ -27,6 +29,7 @@ case class Module(id: ModuleId, license: Option[String] = None, extraInfo: String = "", evictedByVersion: Option[String] = None, + jarFile: Option[File] = None, error: Option[String] = None) { def hadError: Boolean = error.isDefined def isUsed: Boolean = !isEvicted @@ -53,12 +56,15 @@ case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) { } m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).withDefaultValue(Nil) } + + def roots: Seq[Module] = + nodes.filter(n ⇒ !edges.exists(_._2 == n.id)).sortBy(_.id.idString) } import sbinary.{ Format, DefaultProtocol } object ModuleGraphProtocol extends DefaultProtocol { implicit def seqFormat[T: Format]: Format[Seq[T]] = wrap[Seq[T], List[T]](_.toList, _.toSeq) implicit val ModuleIdFormat: Format[ModuleId] = asProduct3(ModuleId)(ModuleId.unapply(_).get) - implicit val ModuleFormat: Format[Module] = asProduct5(Module)(Module.unapply(_).get) + implicit val ModuleFormat: Format[Module] = asProduct6(Module)(Module.unapply(_).get) implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph.apply _)(ModuleGraph.unapply(_).get) } diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala index 0843affbf..c2670e6a2 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala @@ -25,7 +25,7 @@ object AsciiTree { val deps = graph.dependencyMap // there should only be one root node (the project itself) - val roots = graph.nodes.filter(n ⇒ !graph.edges.exists(_._2 == n.id)).sortBy(_.id.idString) + val roots = graph.roots roots.map { root ⇒ AsciiTreeLayout.toAscii[Module](root, node ⇒ deps.getOrElse(node.id, Seq.empty[Module]), displayModule) }.mkString("\n") diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala new file mode 100644 index 000000000..5a82e922b --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/Statistics.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2016 Johannes Rudolph + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.virtualvoid.sbt.graph +package rendering + +object Statistics { + def renderModuleStatsList(graph: ModuleGraph): String = { + case class ModuleStats( + id: ModuleId, + numDirectDependencies: Int, + numTransitiveDependencies: Int, + selfSize: Option[Long], + transitiveSize: Long, + transitiveDependencyStats: Map[ModuleId, ModuleStats]) { + def transitiveStatsWithSelf: Map[ModuleId, ModuleStats] = transitiveDependencyStats + (id -> this) + } + + def statsFor(moduleId: ModuleId): ModuleStats = { + val directDependencies = graph.dependencyMap(moduleId).filterNot(_.isEvicted).map(_.id) + val dependencyStats = directDependencies.map(statsFor).flatMap(_.transitiveStatsWithSelf).toMap + val selfSize = graph.module(moduleId).jarFile.filter(_.exists).map(_.length) + val numDirectDependencies = directDependencies.size + val numTransitiveDependencies = dependencyStats.size + val transitiveSize = selfSize.getOrElse(0L) + dependencyStats.map(_._2.selfSize.getOrElse(0L)).sum + + ModuleStats(moduleId, numDirectDependencies, numTransitiveDependencies, selfSize, transitiveSize, dependencyStats) + } + + def format(stats: ModuleStats): String = { + import stats._ + def mb(bytes: Long): Double = bytes.toDouble / 1000000 + val selfSize = + stats.selfSize match { + case Some(size) ⇒ f"${mb(size)}%7.3f" + case None ⇒ "-------" + } + f"${mb(transitiveSize)}%7.3f MB $selfSize MB $numTransitiveDependencies%4d $numDirectDependencies%4d ${id.idString}%s" + } + + val allStats = + graph.roots.flatMap(r ⇒ statsFor(r.id).transitiveStatsWithSelf).toMap.values.toSeq + .sortBy(s ⇒ (-s.transitiveSize, -s.numTransitiveDependencies)) + + val header = " TotSize JarSize #TDe #Dep Module\n" + + header + + allStats.map(format).mkString("\n") + + """ + | + |Columns are + | - Jar-Size including dependencies + | - Jar-Size + | - Number of transitive dependencies + | - Number of direct dependencies + | - ModuleID""".stripMargin + } +}