Skip to content

Commit

Permalink
feat: improvements relating to generating project graphs.
Browse files Browse the repository at this point in the history
1. improve output of ProjectGraphTask.
2. generate merged graph of all variants in a project.
3. generate merged graph for entire build.
4. incubating support for generating work plan.
  • Loading branch information
autonomousapps committed Dec 3, 2024
1 parent c77c27b commit 6e8d8d5
Show file tree
Hide file tree
Showing 22 changed files with 639 additions and 104 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ local.properties
.DS_STORE

.bash_history
*.salive
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ abstract class AbstractProject extends AbstractGradleProject {
/** Applies the 'java-library' and 'com.autonomousapps.dependency-analysis' plugins. */
protected static final List<Plugin> javaLibrary = [Plugin.javaLibrary, Plugins.dependencyAnalysisNoVersion]

/** Applies the 'java', 'application', and 'com.autonomousapps.dependency-analysis' plugins. */
protected static final List<Plugin> javaApp = [Plugin.java, Plugin.application, Plugins.dependencyAnalysisNoVersion]

protected final DependencyProvider dependencies
protected final PluginProvider plugins

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.WorkPlanProject
import spock.lang.PendingFeature

import static com.autonomousapps.utils.Runner.build
import static com.google.common.truth.Truth.assertThat

final class WorkPlanSpec extends AbstractJvmSpec {

@PendingFeature
def "can generate work plan (#gradleVersion)"() {
given:
def project = new WorkPlanProject()
gradleProject = project.gradleProject
when:
build(gradleVersion, gradleProject.rootDir, ':generateWorkPlan')
then:
assertThat(true).isFalse()
where:
gradleVersion << gradleVersions()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.autonomousapps.jvm.projects

import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject

import static com.autonomousapps.kit.gradle.Dependency.implementation
import static com.autonomousapps.kit.gradle.Dependency.project

final class WorkPlanProject extends AbstractProject {

final GradleProject gradleProject

WorkPlanProject() {
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
// alpha
.withSubproject('alpha:app') { p ->
p.withBuildScript { bs ->
bs.plugins = javaApp + plugins.javaTestFixtures
bs.dependencies = [
project('implementation', ':alpha:lib'),
project('implementation', ':beta:app').onTestFixtures(),
]
}
}
.withSubproject('alpha:lib') { p ->
p.withBuildScript { bs ->
bs.plugins = javaLibrary
bs.dependencies = [
project('api', ':alpha:core'),
]
}
}
.withSubproject('alpha:core') { p ->
p.withBuildScript { bs ->
bs.plugins = javaLibrary
bs.dependencies(implementation('com.squareup.misk:misk:2023.10.18.080259-adcfb84'))
}
}

// beta
.withSubproject('beta:app') { p ->
p.withBuildScript { bs ->
bs.plugins = javaApp + plugins.javaTestFixtures
bs.dependencies = [
project('implementation', ':beta:lib'),
]
}
}
.withSubproject('beta:lib') { p ->
p.withBuildScript { bs ->
bs.plugins = javaLibrary
bs.dependencies = [
project('api', ':beta:core'),
]
}
}
.withSubproject('beta:core') { p ->
p.withBuildScript { bs ->
bs.plugins = javaLibrary
}
}

// gamma
.withSubproject('gamma:app') { p ->
p.withBuildScript { bs ->
bs.plugins = javaApp + plugins.javaTestFixtures
bs.dependencies = [
project('implementation', ':gamma:lib'),
]
}
}
.withSubproject('gamma:lib') { p ->
p.withBuildScript { bs ->
bs.plugins = javaLibrary
bs.dependencies = [
project('api', ':gamma:core'),
]
}
}
.withSubproject('gamma:core') { p ->
p.withBuildScript { bs ->
bs.plugins = javaLibrary
}
}
.write()
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ internal class NoVariantOutputPaths(private val project: Project) {

val locationsPath = file("$ROOT_DIR/declarations.json")
val resolvedDepsPath = file("$ROOT_DIR/resolved-dependencies-report.txt")
val mergedProjectGraphPath = file("$ROOT_DIR/merged-project-graph.json")

/*
* Advice-related tasks.
Expand All @@ -99,11 +100,14 @@ internal class NoVariantOutputPaths(private val project: Project) {
internal class RootOutputPaths(private val project: Project) {

private fun file(path: String): Provider<RegularFile> = project.layout.buildDirectory.file(path)
private fun dir(path: String): Provider<Directory> = project.layout.buildDirectory.dir(path)

val duplicateDependenciesPath = file("$ROOT_DIR/duplicate-dependencies-report.json")
val buildHealthPath = file("$ROOT_DIR/build-health-report.json")
val consoleReportPath = file("$ROOT_DIR/build-health-report.txt")
val shouldFailPath = file("$ROOT_DIR/should-fail.txt")

val workPlanDir = dir("$ROOT_DIR/work-plan")
}

internal class RedundantSubPluginOutputPaths(private val project: Project) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal interface DagpArtifacts : Named {
val declarableName: String,
val artifactName: String,
) {
COMBINED_GRAPH("combinedGraph", "combined-graph"),
PROJECT_HEALTH("projectHealth", "project-health"),
RESOLVED_DEPS("resolvedDeps", "resolved-deps"),
}
Expand Down
69 changes: 64 additions & 5 deletions src/main/kotlin/com/autonomousapps/internal/graph/GraphWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal.graph

import com.autonomousapps.graph.Graphs.root
import com.autonomousapps.graph.Graphs.shortestPaths
import com.autonomousapps.graph.Graphs.topological
import com.autonomousapps.internal.utils.appendReproducibleNewLine
import com.autonomousapps.model.Coordinates
import com.autonomousapps.model.ProjectCoordinates
import com.google.common.graph.Graph
import com.google.common.graph.Traverser

@Suppress("UnstableApiUsage")
internal object GraphWriter {
internal class GraphWriter(private val buildPath: String) {

fun toDot(graph: Graph<Coordinates>) = buildString {
val projectNodes = graph.nodes()
fun toDot(graph: Graph<Coordinates>): String = buildString {
val projectNodes = graph.nodes().asSequence()
// Maybe transform an IncludedBuildCoordinates into its resolvedProject for more human-readable reporting
.map { it.maybeProjectCoordinates(buildPath) }
.filterIsInstance<ProjectCoordinates>()
.map { it.gav() }
.toList()

appendReproducibleNewLine("strict digraph DependencyGraph {")
appendReproducibleNewLine(" ratio=0.6;")
Expand All @@ -28,8 +35,8 @@ internal object GraphWriter {

// the graph itself
graph.edges().forEach { edge ->
val source = edge.nodeU()
val target = edge.nodeV()
val source = edge.nodeU().maybeProjectCoordinates(buildPath)
val target = edge.nodeV().maybeProjectCoordinates(buildPath)
val style =
if (source is ProjectCoordinates && target is ProjectCoordinates) " [style=bold color=\"#FF6347\" weight=8]"
else ""
Expand All @@ -38,4 +45,56 @@ internal object GraphWriter {
}
append("}")
}

/**
* Returns the [graph] sorted into topological order, ascending. Each node in the graph is paired with its depth.
*
* TODO(tsr): shortest path is wrong, I need the _longest_ path.
*/
fun topological(graph: Graph<Coordinates>): String {
val root = graph.root()
val top = graph.topological(root)
// val paths = graph.shortestPaths(root)

return buildString {
top.forEach { node ->
appendLine(node.maybeProjectCoordinates(buildPath).gav())
// append(" ")
// appendLine("${paths.distanceTo(node)}")
}
}
}

// TODO replace with Hu's algorithm (for scheduling concurrent work), and push relevant bits into Graphs.kt.
fun workPlan(graph: Graph<Coordinates>): String {
val traverser = Traverser.forGraph(graph)
val topological = graph.topological(graph.root()).toList()
val plan = mutableListOf<MutableList<Coordinates>>()

var list = mutableListOf(topological.first())
topological
.drop(1)
.forEach { node ->
val hasPathTo = list.any { traverser.breadthFirst(it).contains(node) }
if (!hasPathTo) {
list.add(node)
} else {
plan.add(list)
list = mutableListOf(node)
}
}

// add final list
plan.add(list)

return buildString {
plan.forEachIndexed { i, batch ->
appendLine("$i (${batch.size} items)")
batch.forEach { node ->
append(" ")
appendLine(node.maybeProjectCoordinates(buildPath).gav())
}
}
}
}
}
65 changes: 65 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/graph/graphs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@file:Suppress("UnstableApiUsage")

package com.autonomousapps.internal.graph

import com.autonomousapps.model.Coordinates
import com.autonomousapps.model.IncludedBuildCoordinates
import com.autonomousapps.model.ProjectCoordinates
import com.google.common.graph.ElementOrder
import com.google.common.graph.Graph
import com.google.common.graph.GraphBuilder
import com.google.common.graph.ImmutableGraph

internal operator fun <T> Graph<T>.plus(other: Graph<T>): Graph<T> where T : Any {
val builder = newGraphBuilder<T>()

nodes().forEach { builder.addNode(it) }
edges().forEach { edge -> builder.putEdge(edge.source(), edge.target()) }
other.nodes().forEach { builder.addNode(it) }
other.edges().forEach { edge -> builder.putEdge(edge.source(), edge.target()) }

return builder.build()
}

/**
* Flattens a graph by stripping out the
* [GradleVariantIdentification][com.autonomousapps.model.GradleVariantIdentification], which essentially combines nodes
* to different variants of a module into a single node. This simplifies reporting on certain scenarios where the module
* itself is the unit of analysis, rather than the variant.
*/
internal fun Graph<Coordinates>.stripVariants(buildPath: String): Graph<Coordinates> {
val builder = newGraphBuilder<Coordinates>()

nodes().forEach { builder.addNode(it.maybeProjectCoordinates(buildPath).flatten()) }
edges().forEach { edge ->
val source = edge.source().maybeProjectCoordinates(buildPath).flatten()
val target = edge.target().maybeProjectCoordinates(buildPath).flatten()

// In the un-flattened graphs, self-loops are sort of possible because nodes with different capabilities are
// different.
if (source != target) {
builder.putEdge(source, target)
}
}

return builder.build()
}

internal fun <T> newGraphBuilder(): ImmutableGraph.Builder<T> {
return GraphBuilder.directed()
.allowsSelfLoops(false)
.incidentEdgeOrder(ElementOrder.stable<T>())
.immutable()
}

/**
* Might transform [this][Coordinates] into [ProjectCoordinates], if it is an [IncludedBuildCoordinates] that is from
* "this" build (with buildPath == [buildPath]).
*/
internal fun Coordinates.maybeProjectCoordinates(buildPath: String): Coordinates {
return if (this is IncludedBuildCoordinates && isForBuild(buildPath)) resolvedProject else this
}

private fun Coordinates.flatten(): Coordinates {
return if (this is ProjectCoordinates) flatten() else this
}
Loading

0 comments on commit 6e8d8d5

Please sign in to comment.