Skip to content

Commit

Permalink
Change StoreHelper utility class to a Reporter to coordinate the diff…
Browse files Browse the repository at this point in the history
…erent artifacts that are generated.

Add an HTML report visualizing the exported csv data
  • Loading branch information
janniclas committed Jul 23, 2024
1 parent 25813ad commit 4c276f0
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 69 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ we theoretically support all ecosystems currently implemented in the ORT and dep

### Usage

For development purposes, we recommend to import the project into IntelliJ and use one of the preconfigured
run configurations to get started.
Currently, we support two different commands: \
`create-dependency-graph`, which creates the dependency graphs for the given projects and annotates the graph's
nodes with their version information.\
`calculate-technical-lag`, which takes a directory with annotated dependency graphs as an input and calculates and
stores their corresponding technical lag.

Import the project to IntelliJ as a gradle project and add a custom run configuration.

For manual use run `./gradlew run` to get a list of all available commands. Run `./gradlew run --args "{{COMMAND_NAME}} --help"` to
get usage instructions.

To run the technical lag calculator you can either use the included `Dockerfile` or build it using
`./gradlew installDist` and then run the resulting artifact with `./build/install/libyear-ort/bin/technical-lag-calculator`.
### Display results

The tool results are created in the provided output folder. The results contain a visualization of the output in the
form of an HTML report. To display the report locally you need to start a local webserver (e.g. by running `python3 -m http.server`) in the folder containing the
files and access the URL (e.g., `localhost:8000/htmlReport.html`).

### Architecture overview

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import commands.calculateTechnicalLag.visualization.Visualizer
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.apache.logging.log4j.kotlin.logger
import org.jetbrains.kotlinx.dataframe.api.toDataFrame
import shared.analyzerResultDtos.AnalyzerResultDto
import shared.analyzerResultDtos.ProjectDto
import shared.project.Project
import shared.project.ProjectPaths
import util.StoreResultHelper
import reporter.Reporter
import java.nio.file.Files
import kotlin.io.path.createDirectories
import kotlin.io.path.extension
Expand Down Expand Up @@ -41,11 +41,10 @@ class CalculateTechnicalLag : CliktCommand() {

override fun run(): Unit = runBlocking {
outputPath.createDirectories()

val reporter = Reporter(outputPath)
val technicalLagStatisticsService = TechnicalLagStatisticsService()
logger.info { "Running libyears for projects in $dependencyGraphDirs and output path $outputPath" }

outputPath.createDirectories()

val techLagExport = mutableListOf<Visualizer.TechnicalLagExport>()

Expand Down Expand Up @@ -87,10 +86,13 @@ class CalculateTechnicalLag : CliktCommand() {
environmentInfo = analyzerResult.environmentInfo,
)
// TODO: store direct and transitive stats for the graph. need to extend the serialization and DTO
StoreResultHelper.storeAnalyzerResultInFile(outputPath.toFile(), result)
reporter.storeAnalyzerResultInFile(result)
}

Visualizer.createAndStoreBoxplotFromTechLag(data = techLagExport, outputPath = outputPath)
val df = techLagExport.toDataFrame()
Visualizer.createAndStoreBoxplotFromTechLag(df, outputPath = outputPath)
reporter.storeCsvExport(df)
reporter.generateHtmlReport()
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package commands.calculateTechnicalLag.visualization

import org.jetbrains.kotlinx.dataframe.DataFrame
import org.jetbrains.kotlinx.dataframe.api.toDataFrame
import org.jetbrains.kotlinx.dataframe.io.writeCSV
import org.jetbrains.kotlinx.kandy.dsl.plot
Expand Down Expand Up @@ -39,11 +40,9 @@ object Visualizer {
val version: String
)

fun createAndStoreBoxplotFromTechLag(data: List<TechnicalLagExport>, outputPath: Path) {
fun createAndStoreBoxplotFromTechLag(df: DataFrame<TechnicalLagExport>, outputPath: Path) {

val df = data.toDataFrame()

df.writeCSV(outputPath.toAbsolutePath().resolve("boxplot-data.csv").toString())
val outputFilePath = outputPath.toAbsolutePath().resolve("${Date().time}-boxplot.png").toString()
df.plot {
boxplot("scope", "libdays") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.kotlin.logger
import shared.analyzerResultDtos.AnalyzerResultDto
import shared.analyzerResultDtos.ProjectDto
import util.StoreResultHelper
import reporter.Reporter
import java.io.File
import java.nio.file.Files
import kotlin.io.path.createDirectories
Expand Down Expand Up @@ -81,8 +81,8 @@ class CreateDependencyGraph : CliktCommand() {
repositoryInfo = rawResult.repositoryInfo,
environmentInfo = rawResult.environmentInfo,
)

StoreResultHelper.storeAnalyzerResultInFile(outputPath.toFile(), result)
val reporter = Reporter(outputPath)
reporter.storeAnalyzerResultInFile(result)
} catch (error: Exception) {
logger.error("Dependency Analyzer failed with error $error")
}
Expand Down
76 changes: 76 additions & 0 deletions src/main/kotlin/reporter/Reporter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package reporter

import commands.calculateTechnicalLag.visualization.Visualizer
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.jetbrains.kotlinx.dataframe.DataFrame
import org.jetbrains.kotlinx.dataframe.io.writeCSV
import shared.analyzerResultDtos.AnalyzerResultDto
import shared.project.ProjectPaths
import java.io.File
import java.nio.file.Path
import java.util.*


class Reporter(outputPath: Path) {

private val json = Json { prettyPrint = true }
private val outputFile = outputPath.toAbsolutePath().toFile()

private val analyzerResultFile by lazy {
createFileInOutputPath("${Date().time}-analyzerResult.json")
}
private val htmlReportFile by lazy {
createFileInOutputPath("htmlReport.html")
}
private val csvFileName = "csvReport.csv"
private val csvReportFile by lazy {
outputPath.resolve(csvFileName)
}

private fun createFileInOutputPath(fileName: String): File {
val file = outputFile.resolve(fileName)
file.createNewFile()
return file
}

fun storeAnalyzerResultInFile(
result: AnalyzerResultDto,
) {
val jsonString =
json.encodeToString(AnalyzerResultDto.serializer(), result)
analyzerResultFile.writeText(jsonString)
}

fun storeCsvExport(df: DataFrame<Visualizer.TechnicalLagExport>) {
df.writeCSV(csvReportFile.toString())
}

fun generateHtmlReport(
) {
val resource =
this.javaClass.classLoader.getResource("html-report-template.html") ?: return

val htmlReportTemplate = resource.readText(Charsets.UTF_8)
val htmlReport = htmlReportTemplate.replace("{{ PATH_TO_STATISTICS }}", "\"$csvFileName\"")

htmlReportFile.writeText(htmlReport, Charsets.UTF_8)
}

suspend fun storeResultFilePathsInFile(
outputDirectory: File,
paths: ProjectPaths,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): File {
val outputFile = outputDirectory.resolve("dependencyGraphs.json")
return withContext(dispatcher) {
outputFile.createNewFile()
val jsonString =
json.encodeToString(ProjectPaths.serializer(), paths)
outputFile.writeText(jsonString)
return@withContext outputFile
}
}
}
49 changes: 0 additions & 49 deletions src/main/kotlin/util/StoreResultsHelper.kt

This file was deleted.

115 changes: 115 additions & 0 deletions src/main/resources/html-report-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<head>
<!-- Load plotly.js into the DOM -->
<script src='https://cdn.plot.ly/plotly-2.32.0.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js'></script>
</head>

<body>
<div id='myDiv'><!-- Plotly chart will be drawn inside this DIV --></div>
</body>

<script>

d3.csv(
{{ PATH_TO_STATISTICS }},
function (rows) {
function unpack(rows, key) {
return rows.map(function (row) {
return row[key];
});
}

var directDeps = rows.filter((row) => {
return row["scope"] == "dependencies-direct";
});

var traceDirectDeps = {
meanline: {
visible: true,
},
legendgroup: "dependencies-direct",
scalegroup: "dependencies-direct",
points: "all",
pointpos: -1.2,
box: {
visible: true,
},
jitter: 0,
scalemode: "count",
marker: {
line: {
width: 2,
color: "green",
},
symbol: "line-ns",
},
showlegend: true,
side: "negative",
type: "violin",
name: "dependencies-direct",
line: {
color: "green",
},
x0: "dependencies-direct-transitive",
y: unpack(directDeps, "libdays"),
};

var transDeps = rows.filter((row) => {
return row["scope"] == "dependencies-transitive";
});

var traceTransDeps = {
meanline: {
visible: true,
},
legendgroup: "dependencies-transitive",
scalegroup: "dependencies-transitive",
points: "all",
pointpos: 1.2,
box: {
visible: true,
},
jitter: 0,
scalemode: "count",
marker: {
line: {
width: 2,
color: 'red',
},
symbol: "line-ns",
},
showlegend: true,
side: "positive",
type: "violin",
name: "dependencies-transitive",
line: {
color: "red",
},
x0: "dependencies-direct-transitive",
y: unpack(transDeps, "libdays"),
};

console.log(directDeps);
console.log(transDeps);
console.log(rows);
var data = [traceDirectDeps, traceTransDeps]

var layout = {
hovermode: "closest",
yaxis: {
showgrid: true,
},
title: "Dependency Libyears",
legend: {
tracegroupgap: 0,
},
violingap: 0,
violingroupgap: 0,
violinmode: "overlay",
};

Plotly.newPlot("myDiv", data, layout);
}
);

</script>

0 comments on commit 4c276f0

Please sign in to comment.