added `dependencyStats`: a tabular console output showing an overview of jar-sizes, fixes #83

This commit is contained in:
Johannes Rudolph 2016-01-08 18:30:01 +01:00
parent f7e3a49e93
commit c9d1f81149
7 changed files with 94 additions and 4 deletions

View File

@ -29,6 +29,7 @@ This plugin is an auto-plugin which will be automatically enabled starting from
* `whatDependsOn <organization> <module> <revision>`: 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-<config>.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-<config>.dot`.

View File

@ -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",

View File

@ -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 {

View File

@ -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)))
}

View File

@ -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)
}

View File

@ -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")

View File

@ -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
}
}