diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96ca2d0a..1e262c48 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,9 @@ jcloc = "0.3.0" jqwik = "1.9.2" graphviz-java = "0.18.1" jgrapht = "1.5.2" +clikt = "5.0.2" +mordant = "3.0.1" +kotlin-csv = "1.10.0" [libraries] diktat-gradle-plugin = { module = "com.saveourtool.diktat:diktat-gradle-plugin", version.ref = "diktat" } @@ -32,9 +35,13 @@ jcloc = { module = "ch.usi.si.seart:jcloc", version.ref = "jcloc" } kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } jqwik = { module = "net.jqwik:jqwik", version.ref = "jqwik" } jqwik-kotlin = { module = "net.jqwik:jqwik-kotlin", version.ref = "jqwik" } -graphviz-java = { group = "guru.nidi", name = "graphviz-java", version.ref = "graphviz-java"} -graphviz-kotlin = { group = "guru.nidi", name = "graphviz-kotlin", version.ref = "graphviz-java"} +graphviz-java = { group = "guru.nidi", name = "graphviz-java", version.ref = "graphviz-java" } +graphviz-kotlin = { group = "guru.nidi", name = "graphviz-kotlin", version.ref = "graphviz-java" } jgrapht-core = { module = "org.jgrapht:jgrapht-core", version.ref = "jgrapht" } +clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } +clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref = "clikt" } +mordant = { module = "com.github.ajalt.mordant:mordant", version.ref = "mordant" } +csv = { module = "com.jsoizo:kotlin-csv-jvm", version.ref = "kotlin-csv" } [plugins] intellij = { id = "org.jetbrains.intellij.platform", version.ref = "intellij" } diff --git a/project-minimization-plugin/build.gradle.kts b/project-minimization-plugin/build.gradle.kts index f2985d36..be7563d6 100644 --- a/project-minimization-plugin/build.gradle.kts +++ b/project-minimization-plugin/build.gradle.kts @@ -1,6 +1,8 @@ +import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.models.Coordinates import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask +import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware.SplitModeTarget plugins { alias(libs.plugins.intellij) @@ -46,6 +48,9 @@ dependencies { implementation(libs.jcloc) implementation(libs.graphviz.java) implementation(libs.graphviz.kotlin) + implementation(libs.clikt) + implementation(libs.clikt.markdown) + implementation(libs.mordant) testImplementation("org.junit.jupiter:junit-jupiter-api:5.1.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.1.0") @@ -71,4 +76,52 @@ tasks.test { jvmArgumentProviders += CommandLineArgumentProvider { listOf("-Didea.kotlin.plugin.use.k2=true") } -} \ No newline at end of file +} + +val runCliMinimization by intellijPlatformTesting.runIde.registering { + task { + dependsOn("buildPlugin") + val inputFolder: String? by project + jvmArgs = listOf( + "-Djava.awt.headless=true", + "--add-exports", + "java.base/jdk.internal.vm=ALL-UNNAMED", + ) + maxHeapSize = "20g" + standardInput = System.`in` + standardOutput = System.`out` + splitMode = false + splitModeTarget = SplitModeTarget.BACKEND + inputFolder?.let { + args = listOf( + "minimize", + "-p", + it, + ) + } + } +} + +val runCliBenchmark by intellijPlatformTesting.runIde.registering { + task { + dependsOn("buildPlugin") + val inputFolder: String? by project + jvmArgs = listOf( + "-Djava.awt.headless=true", + "--add-exports", + "java.base/jdk.internal.vm=ALL-UNNAMED", + ) + maxHeapSize = "20g" + standardInput = System.`in` + standardOutput = System.`out` + splitMode = false + splitModeTarget = SplitModeTarget.BACKEND + inputFolder?.let { + args = listOf( + "benchmark-minimization", + "-p", + it, + ) + } + } +} diff --git a/project-minimization-plugin/gradle.properties b/project-minimization-plugin/gradle.properties index c89948bb..83003573 100644 --- a/project-minimization-plugin/gradle.properties +++ b/project-minimization-plugin/gradle.properties @@ -1,7 +1,6 @@ pluginName = Project Minimization platformType = IC platformVersion = 2024.3.2.1 -# https://youtrack.jetbrains.com/issue/LLM-13649 platformBundledPlugins = com.intellij.java, org.jetbrains.kotlin, com.intellij.gradle, org.intellij.groovy platformPlugins = kotlin.daemon.jvmargs=-Xmx8G \ No newline at end of file diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/BenchmarkAction.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/BenchmarkAction.kt index 4e79e8a6..7eb48ca7 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/BenchmarkAction.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/BenchmarkAction.kt @@ -30,11 +30,9 @@ class BenchmarkAction : AnAction() { runCatching { val project = e.project ?: return val benchmarkingService = project.service() - benchmarkingService.benchmark() + benchmarkingService.benchmark { BenchmarkSettings.isBenchmarkingEnabled = false } }.onFailure { logger.error("Benchmarking failed", it) - }.also { - BenchmarkSettings.isBenchmarkingEnabled = false } } } diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/benchmark/LogParser.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/benchmark/LogParser.kt new file mode 100644 index 00000000..58ca47b6 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/benchmark/LogParser.kt @@ -0,0 +1,151 @@ +package org.plan.research.minimization.plugin.benchmark + +import org.plan.research.minimization.plugin.model.benchmark.logs.LinesMetric +import org.plan.research.minimization.plugin.model.benchmark.logs.ProjectStatistics +import org.plan.research.minimization.plugin.model.benchmark.logs.StageStatistics +import org.plan.research.minimization.plugin.model.benchmark.logs.ThroughMinimizationStatistics + +import com.charleskorn.kaml.Yaml + +import java.nio.file.Path +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.io.path.readLines +import kotlin.io.path.readText +import kotlin.time.Duration +import kotlin.time.toKotlinDuration +import kotlinx.serialization.decodeFromString + +class LogParser { + fun parseLogs(baseDir: Path, stageNames: List): List { + val configPath = baseDir.resolve("config.yaml") + val config = loadConfig(configPath) + + return config.projects.mapNotNull { project -> + val logs = findLatestStatisticsLog(baseDir, project.path) + ?.readLines() + ?: return@mapNotNull null + val relevantLogs = logs + .extractRelevantLogsFor("Project minimization for project: ${project.name}") + ?: return@mapNotNull null + project.gatherStatistics( + relevantLogs, + stageNames, + ) + } + } + + private fun BenchmarkProject.gatherStatistics( + relevantLogs: List, + stageNames: List, + ): ProjectStatistics { + val projectLinesMetric = LinesMetric( + totalLines = extractStat(relevantLogs, TOTAL_NAME), + blankLines = extractStat(relevantLogs, BLANK_NAME), + commentLines = extractStat(relevantLogs, COMMENT_NAME), + codeLines = extractStat(relevantLogs, CODE_NAME), + ) + + return ProjectStatistics( + projectName = name, + elapsed = calculateDuration(relevantLogs.first(), relevantLogs.last()), + ktFiles = extractStat(relevantLogs, "Kotlin files"), + lines = projectLinesMetric, + numberOfCompilations = relevantLogs.count { it.contains("Snapshot manager start's transaction") }, + stageMetrics = buildList { + add(StageStatistics(name = "", linesMetric = projectLinesMetric.stale())) + stageNames.forEach { stageName -> + add( + extractStageStatistics( + stageName, + relevantLogs.extractRelevantLogsFor("$stageName stage") ?: return@forEach, + this.last(), + ), + ) + } + }.drop(1), + ) + } + + private fun loadConfig(configPath: Path): BenchmarkConfig { + val configFile = configPath + .takeIf { it.exists() } ?: throw IllegalArgumentException("Configuration file not found at: $configPath") + return Yaml.Companion.default.decodeFromString(configFile.readText()) + } + + private fun findLatestStatisticsLog(baseDir: Path, projectPath: String): Path? { + val logsDir = baseDir + .resolve(projectPath) + .resolve("minimization-logs") + .takeIf { it.exists() && it.isDirectory() } + ?.toFile() + ?: return null + + val executionDirs = logsDir.listFiles { file -> file.isDirectory } ?: return null + val latestExecutionDir = executionDirs.maxByOrNull { it.name } ?: return null + val statisticsLog = latestExecutionDir.resolve("statistics.log") + return statisticsLog.toPath().takeIf { it.exists() } + } + + private fun calculateDuration(startLine: String, endLine: String): Duration { + val startTime = extractTime(startLine) + val endTime = extractTime(endLine) + return java.time.Duration.between(startTime, endTime) + .toKotlinDuration() + } + + private fun extractStageStatistics( + stageName: String, + relevantLogs: List, + previousValue: StageStatistics, + ): StageStatistics { + val previousLines = previousValue.linesMetric + return StageStatistics( + name = stageName, + linesMetric = LinesMetric( + totalLines = previousLines.totalLines.continueWith(extractSingleMetric(relevantLogs, TOTAL_NAME)), + blankLines = previousLines.blankLines.continueWith(extractSingleMetric(relevantLogs, BLANK_NAME)), + commentLines = previousLines.commentLines.continueWith(extractSingleMetric(relevantLogs, COMMENT_NAME)), + codeLines = previousLines.codeLines.continueWith(extractSingleMetric(relevantLogs, CODE_NAME)), + ), + ) + } + + private fun List.extractRelevantLogsFor(name: String): List? { + val startLine = find { it.contains("Start $name") } ?: return null + val endLine = find { it.contains("End $name") } ?: return null + return subList(indexOf(startLine), indexOf(endLine) + 1) + } + + private fun extractTime(logLine: String): LocalTime { + val timePart = logLine.substringBefore(" - ") + return LocalTime.parse(timePart, DateTimeFormatter.ofPattern("HH:mm:ss.SSS")) + } + + private fun extractStat(logs: List, statName: String): ThroughMinimizationStatistics { + val statLines = logs.filter { it.contains(statName) } + val before = statLines.firstOrNull().extractMetric(statName) + val after = statLines.lastOrNull().extractMetric(statName) + return ThroughMinimizationStatistics(before, after) + } + + private fun extractSingleMetric(logs: List, statName: String) = logs + .lastOrNull { it.contains(statName) } + .extractMetric(statName) + + private fun String?.extractMetric(metricName: String) = this + ?.substringAfter("$metricName: ") + ?.trim() + ?.toIntOrNull() + ?: 0 + + companion object { + private const val BLANK_NAME = "Blank LOC" + private const val CODE_NAME = "Code LOC" + private const val COMMENT_NAME = "Comment LOC" + private const val TOTAL_NAME = "Total LOC" + } +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/AbstractApplicationStarterCliktAdapter.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/AbstractApplicationStarterCliktAdapter.kt new file mode 100644 index 00000000..12edaa3f --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/AbstractApplicationStarterCliktAdapter.kt @@ -0,0 +1,10 @@ +package org.plan.research.minimization.plugin.headless + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.command.main +import com.intellij.openapi.application.ModernApplicationStarter + +abstract class AbstractApplicationStarterCliktAdapter : ModernApplicationStarter() { + abstract val cliktRunner: SuspendingCliktCommand + final override suspend fun start(args: List) = cliktRunner.main(args.drop(1)) +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/MinimizeProjectApplicationStarter.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/MinimizeProjectApplicationStarter.kt new file mode 100644 index 00000000..e8312139 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/MinimizeProjectApplicationStarter.kt @@ -0,0 +1,7 @@ +package org.plan.research.minimization.plugin.headless + +import com.github.ajalt.clikt.command.SuspendingCliktCommand + +class MinimizeProjectApplicationStarter : AbstractApplicationStarterCliktAdapter() { + override val cliktRunner: SuspendingCliktCommand = MinimizeProjectCliCommand() +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/MinimizeProjectCliCommand.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/MinimizeProjectCliCommand.kt new file mode 100644 index 00000000..3da1e4c7 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/MinimizeProjectCliCommand.kt @@ -0,0 +1,66 @@ +package org.plan.research.minimization.plugin.headless + +import org.plan.research.minimization.plugin.services.MinimizationResult +import org.plan.research.minimization.plugin.services.MinimizationService +import org.plan.research.minimization.plugin.services.ProjectOpeningService + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.path +import com.github.ajalt.mordant.rendering.TextStyles +import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.terminal.danger +import com.github.ajalt.mordant.terminal.success +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.project.ex.ProjectManagerEx + +import java.nio.file.Path + +class MinimizeProjectCliCommand : SuspendingCliktCommand() { + val projectPath: Path by option( + "-p", + "--project-path", + help = "Path to the project to minimize", + ).path(mustExist = true, canBeFile = false, canBeDir = true).required() + lateinit var terminal: Terminal + + override suspend fun run() { + terminal = Terminal() + val project = service().openProject(projectPath) ?: run { + reportOpeningProblem() + return + } + val minimizationResult = project + .service() + .minimizeProjectAsync() + reportMinimizationResult(minimizationResult) + ApplicationManager.getApplication().exit(false, true, false) + } + private fun reportOpeningProblem() { + terminal.danger(""" + Error with opening project with path $projectPath. + There is some possible reasons: + - Project folder does not contain any project + - Project folder does not contain *gradle* project + - Project is corrupted. + You can try to open the project using IDEA and run the minimization again manually. + """.trimIndent()) + } + private suspend fun reportMinimizationResult(minimizationResult: MinimizationResult) = minimizationResult + .onLeft { + // TODO: explain errors + terminal.danger(""" + Minimization has failed. The reason is $it. Please submit the logs to the developers or retry using IDEA. + """.trimIndent()) + } + .onRight { + terminal.success(""" + The project has been successfully minimized. + The result of the minimization is in folder ${TextStyles.hyperlink(it.projectDir.url)}. + """.trimIndent()) + // TODO: Pretty-print statistics + ProjectManagerEx.getInstanceEx().forceCloseProjectAsync(it.project) + } +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/RunBenchmarkApplicationStarter.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/RunBenchmarkApplicationStarter.kt new file mode 100644 index 00000000..bf48b4bf --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/RunBenchmarkApplicationStarter.kt @@ -0,0 +1,7 @@ +package org.plan.research.minimization.plugin.headless + +import com.github.ajalt.clikt.command.SuspendingCliktCommand + +class RunBenchmarkApplicationStarter : AbstractApplicationStarterCliktAdapter() { + override val cliktRunner: SuspendingCliktCommand = RunBenchmarkCliCommand() +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/RunBenchmarkCliCommand.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/RunBenchmarkCliCommand.kt new file mode 100644 index 00000000..cbbd8e41 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/headless/RunBenchmarkCliCommand.kt @@ -0,0 +1,183 @@ +package org.plan.research.minimization.plugin.headless + +import org.plan.research.minimization.plugin.benchmark.BenchmarkSettings +import org.plan.research.minimization.plugin.model.benchmark.logs.ProjectStatistics +import org.plan.research.minimization.plugin.model.benchmark.logs.StageStatistics +import org.plan.research.minimization.plugin.services.BenchmarkService +import org.plan.research.minimization.plugin.services.ProjectOpeningService + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.path +import com.github.ajalt.mordant.rendering.OverflowWrap +import com.github.ajalt.mordant.rendering.TextAlign +import com.github.ajalt.mordant.rendering.TextColors +import com.github.ajalt.mordant.rendering.TextColors.brightGreen +import com.github.ajalt.mordant.rendering.TextColors.brightRed +import com.github.ajalt.mordant.rendering.TextStyle +import com.github.ajalt.mordant.rendering.TextStyles.bold +import com.github.ajalt.mordant.rendering.TextStyles.dim +import com.github.ajalt.mordant.table.Borders +import com.github.ajalt.mordant.table.SectionBuilder +import com.github.ajalt.mordant.table.TableBuilder +import com.github.ajalt.mordant.table.table +import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.terminal.danger +import com.github.ajalt.mordant.terminal.success +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project + +import java.nio.file.Path + +import kotlin.time.Duration + +class RunBenchmarkCliCommand : SuspendingCliktCommand() { + val projectPath: Path by option( + "-p", + "--project-path", + help = "Path to the project to minimize", + ).path(mustExist = true, canBeFile = false, canBeDir = true).required() + lateinit var terminal: Terminal + override suspend fun run() { + terminal = Terminal() + terminal.tabWidth + val project = service().openProject(projectPath) ?: run { + reportOpeningProblem() + return + } + BenchmarkSettings.isBenchmarkingEnabled = true + // TODO: rewrite benchmark to support result values + project + .service() + .asyncBenchmark() + BenchmarkSettings.isBenchmarkingEnabled = false + procesResults() + printLogs(project) + ApplicationManager.getApplication().exit(false, true, false) + } + + private fun reportOpeningProblem() { + terminal.danger( + """ + Error with opening benchmark with path $projectPath. + There is some possible reasons: + - Benchmark folder does not contain any project + - Benchmark is corrupted. + You can try to open the benchmark using IDEA and run the benchmarking again manually. + """.trimIndent(), + ) + } + + private fun procesResults() { + terminal.success("Benchmarking is done successfully") + } + + private fun printLogs(project: Project) { + val logs = project.service().parseBenchmarkLogs() + for (statistics in logs) { + terminal.println(statistics.toTable()) + } + } + + private fun ProjectStatistics.toTable() = table { + captionTop(projectName) + buildHeader() + body { + style = brightGreen + align = TextAlign.CENTER + column(0) { + align = TextAlign.LEFT + cellBorders = Borders.ALL + style = TextColors.brightWhite + bold + } + rowStyles(TextStyle(), dim.style) + cellBorders = Borders.TOP_BOTTOM + row( + "Before Minimization", + ktFiles.before, + lines.totalLines.before, + lines.blankLines.before, + lines.commentLines.before, + lines.codeLines.before, + "", + "", + ) + stageMetrics.dropLast(1).forEach { rowForStage(it, ktFiles = ktFiles.before) } + stageMetrics.lastOrNull()?.let { + rowForStage( + it, + ktFiles = ktFiles.after, + elapsedTime = elapsed.dump(), + compilations = numberOfCompilations, + ) + } + } + } + + private fun SectionBuilder.rowForStage( + stage: StageStatistics, + // FIXME + ktFiles: Any = "", + // FIXME + elapsedTime: Any = "", + // FIXME + compilations: Any = "", + ) = stage.let { (name, lines) -> + row( + name, + ktFiles, + lines.totalLines.after, + lines.blankLines.after, + lines.commentLines.after, + lines.codeLines.after, + elapsedTime, + compilations, + ) + } + + private fun TableBuilder.buildHeader() = header { + style = brightRed + bold + @Suppress("MAGIC_NUMBER") + column(0) { + cellBorders = Borders.TOP_RIGHT_BOTTOM + } + @Suppress("MAGIC_NUMBER") + column(7) { + cellBorders = Borders.LEFT_TOP_BOTTOM + } + row { + cellBorders = Borders.TOP_BOTTOM + cell("") + cell("") + cell("LoC") { + columnSpan = 4 + align = TextAlign.CENTER + } + cells("", "") + } + row( + "Stage", + ".kt Files", + "Total", + "Blank", + "Comment", + "Code", + "Elapsed time", + "Compilations", + ) { + cellBorders = Borders.ALL + overflowWrap = OverflowWrap.NORMAL + } + } + + private fun Duration.dump(): String { + val (minutes, seconds) = this.toComponents { minutes, seconds, _ -> minutes to seconds } + return if (minutes > 0) { + "$minutes m $seconds s" + } else { + "$seconds s" + } + } +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/LinesMetric.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/LinesMetric.kt new file mode 100644 index 00000000..f9711440 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/LinesMetric.kt @@ -0,0 +1,15 @@ +package org.plan.research.minimization.plugin.model.benchmark.logs + +data class LinesMetric( + val totalLines: ThroughMinimizationStatistics, + val blankLines: ThroughMinimizationStatistics, + val commentLines: ThroughMinimizationStatistics, + val codeLines: ThroughMinimizationStatistics, +) { + internal fun stale() = copy( + totalLines = totalLines.stale(), + blankLines = blankLines.stale(), + commentLines = commentLines.stale(), + codeLines = codeLines.stale(), + ) +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/ProjectStatistics.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/ProjectStatistics.kt new file mode 100644 index 00000000..b7434868 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/ProjectStatistics.kt @@ -0,0 +1,13 @@ +package org.plan.research.minimization.plugin.model.benchmark.logs + +import kotlin.time.Duration + +// Data class to hold parsed project statistics +data class ProjectStatistics( + val projectName: String, + val elapsed: Duration, + val ktFiles: ThroughMinimizationStatistics, + val lines: LinesMetric, + val numberOfCompilations: Int, + val stageMetrics: List, +) diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/StageStatistics.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/StageStatistics.kt new file mode 100644 index 00000000..00978dcb --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/StageStatistics.kt @@ -0,0 +1,3 @@ +package org.plan.research.minimization.plugin.model.benchmark.logs + +data class StageStatistics(val name: String, val linesMetric: LinesMetric) diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/ThroughMinimizationStatistics.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/ThroughMinimizationStatistics.kt new file mode 100644 index 00000000..1a8ec55f --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/model/benchmark/logs/ThroughMinimizationStatistics.kt @@ -0,0 +1,9 @@ +package org.plan.research.minimization.plugin.model.benchmark.logs + +data class ThroughMinimizationStatistics( + val before: Int, + val after: Int, +) { + internal fun stale() = copy(after = before) + internal fun continueWith(newValue: Int) = copy(before = after, after = newValue) +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/BenchmarkService.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/BenchmarkService.kt index 885f16e4..89597c2f 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/BenchmarkService.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/BenchmarkService.kt @@ -3,7 +3,9 @@ package org.plan.research.minimization.plugin.services import org.plan.research.minimization.plugin.benchmark.BenchmarkConfig import org.plan.research.minimization.plugin.benchmark.BenchmarkProject import org.plan.research.minimization.plugin.benchmark.BuildSystemType +import org.plan.research.minimization.plugin.benchmark.LogParser import org.plan.research.minimization.plugin.benchmark.ProjectModulesType +import org.plan.research.minimization.plugin.model.benchmark.logs.ProjectStatistics import org.plan.research.minimization.plugin.settings.loadStateFromFile import arrow.core.Either @@ -32,12 +34,25 @@ import kotlinx.coroutines.* @Service(Service.Level.PROJECT) class BenchmarkService(private val rootProject: Project, private val cs: CoroutineScope) { private val logger = KotlinLogging.logger {} + fun parseBenchmarkLogs(): List { + val stagesName = rootProject + .service() + .stateObservable + .stages + .get() + .map { it.name } + return LogParser().parseLogs(rootProject.guessProjectDir()!!.toNioPath(), stagesName) + } - fun benchmark() = cs.launch { + fun benchmark(onComplete: () -> Unit) = cs.launch { + asyncBenchmark() + onComplete() + } + suspend fun asyncBenchmark() { logger.info { "Start benchmark Action" } logger.info { "Read Config" } - val config = readConfig().getOrNull() ?: return@launch + val config = readConfig().getOrNull() ?: return val filteredProjects = config .projects diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/MinimizationStageExecutorService.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/MinimizationStageExecutorService.kt index c1e7bee7..779bdb12 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/MinimizationStageExecutorService.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/MinimizationStageExecutorService.kt @@ -45,10 +45,9 @@ class MinimizationStageExecutorService(private val project: Project) : Minimizat private val snapshotManager = project.service() override suspend fun executeFileLevelStage(context: HeavyIJDDContext<*>, fileLevelStage: FileLevelStage) = either { - logger.info { "Start File level stage" } - statLogger.info { "Start File level stage" } + logStageStart(fileLevelStage) statLogger.info { - "File level stage settings, " + + "${fileLevelStage.name} stage settings, " + "DDAlgorithm: ${fileLevelStage.ddAlgorithm}" } @@ -66,16 +65,16 @@ class MinimizationStageExecutorService(private val project: Project) : Minimizat lightContext.runMonadWithProgress { hierarchicalDD.minimize(hierarchy) } - }.logResult("File") + }.logResult(fileLevelStage.name) override suspend fun executeFunctionLevelStage( context: HeavyIJDDContext<*>, functionLevelStage: FunctionLevelStage, ) = either { - logger.info { "Start Function Body Replacement level stage" } - statLogger.info { "Start Function Body Replacement level stage" } + logStageStart(functionLevelStage) statLogger.info { - "Function level stage settings. DDAlgorithm: ${functionLevelStage.ddAlgorithm}" + "${functionLevelStage.name} stage settings, " + + "DDAlgorithm: ${functionLevelStage.ddAlgorithm}" } val lightContext = FunctionLevelStageContext(context.projectDir, context.project, context.originalProject) @@ -104,7 +103,12 @@ class MinimizationStageExecutorService(private val project: Project) : Minimizat propertyChecker, ) } - }.logResult("Function Body Replacement") + }.logResult(functionLevelStage.name) + + private fun logStageStart(stage: MinimizationStage) { + logger.info { "Start ${stage.name} stage" } + statLogger.info { "Start ${stage.name} stage" } + } private suspend fun List>.logPsiElements(context: IJDDContext) { if (!logger.isTraceEnabled) { @@ -123,8 +127,7 @@ class MinimizationStageExecutorService(private val project: Project) : Minimizat context: HeavyIJDDContext<*>, declarationLevelStage: DeclarationLevelStage, ) = either { - logger.info { "Start Function Deleting level stage" } - statLogger.info { "Start Function Deleting level stage" } + logStageStart(declarationLevelStage) statLogger.info { "Function deleting stage settings, " + "DDAlgorithm: ${declarationLevelStage.ddAlgorithm}" @@ -149,15 +152,15 @@ class MinimizationStageExecutorService(private val project: Project) : Minimizat lightContext.runMonadWithProgress { hierarchicalDD.minimize(hierarchy) } - }.logResult("Function Deleting") + }.logResult(declarationLevelStage.name) override suspend fun executeDeclarationGraphStage( context: HeavyIJDDContext<*>, declarationGraphStage: DeclarationGraphStage, ) = either { - logger.info { "Start Function Deleting Graph stage" } + logStageStart(declarationGraphStage) statLogger.info { - "Function deleting Graph stage settings, " + + "${declarationGraphStage.name} stage settings, " + "DDAlgorithm: ${declarationGraphStage.ddAlgorithm}" } @@ -192,17 +195,17 @@ class MinimizationStageExecutorService(private val project: Project) : Minimizat lightContext.runMonad { graphDD.minimize(graph, propertyTester) } - }.logResult("Function Deleting Graph") + }.logResult(declarationGraphStage.name) private fun Either.logResult(stageName: String) = onRight { - logger.info { "End $stageName level stage" } - statLogger.info { "End $stageName level stage" } - statLogger.info { "$stageName level stage result: success" } + logger.info { "End $stageName stage" } + statLogger.info { "End $stageName stage" } + statLogger.info { "$stageName stage result: success" } }.onLeft { error -> - logger.info { "End $stageName level stage" } - statLogger.info { "End $stageName level stage" } - statLogger.info { "$stageName level stage result: $error" } - logger.error { "$stageName level stage failed with error: $error" } + logger.info { "End $stageName stage" } + statLogger.info { "End $stageName stage" } + statLogger.info { "$stageName stage result: $error" } + logger.error { "$stageName stage failed with error: $error" } } private suspend fun > C.runMonadWithProgress( diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/MinimizationPluginState.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/MinimizationPluginState.kt index 274c76ce..fb30121a 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/MinimizationPluginState.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/MinimizationPluginState.kt @@ -49,7 +49,6 @@ class MinimizationPluginState : BaseState() { val defaultStages: List = listOf( FunctionLevelStage(), DeclarationGraphStage(), - FileLevelStage(), ) val defaultTransformations: List = listOf( TransformationDescriptor.PATH_RELATIVIZATION, diff --git a/project-minimization-plugin/src/main/resources/META-INF/plugin.xml b/project-minimization-plugin/src/main/resources/META-INF/plugin.xml index 96c1ee84..ad372dbd 100644 --- a/project-minimization-plugin/src/main/resources/META-INF/plugin.xml +++ b/project-minimization-plugin/src/main/resources/META-INF/plugin.xml @@ -35,6 +35,10 @@ instance="org.plan.research.minimization.plugin.settings.MinimizationPluginSettingsConfigurable" id="org.plan.research.minimization.plugin.settings.MinimizationPluginSettingsConfigurable" displayName="Project Minimization"/> + + diff --git a/project-minimization-scripts/build.gradle.kts b/project-minimization-scripts/build.gradle.kts index 46ffbdef..8128d31c 100644 --- a/project-minimization-scripts/build.gradle.kts +++ b/project-minimization-scripts/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":project-minimization-plugin")) implementation(libs.kaml) + implementation(libs.csv) } tasks.test { diff --git a/project-minimization-scripts/src/main/kotlin/RunLogParser.kt b/project-minimization-scripts/src/main/kotlin/RunLogParser.kt new file mode 100644 index 00000000..951dc298 --- /dev/null +++ b/project-minimization-scripts/src/main/kotlin/RunLogParser.kt @@ -0,0 +1,18 @@ +package org.plan.research.minimization + +import org.plan.research.minimization.plugin.benchmark.LogParser +import org.plan.research.minimization.scripts.logs.LogsCSVWriter +import java.nio.file.Path + +// Usage: [ ...] +fun main(args: Array) { + if (args.size < 2) { + return + } + + val baseDir = Path.of(args[0]) + val outputCsvPath = Path.of(args[1]) + val stageNames = args.slice(2 until args.size) + val parsedLogs = LogParser().parseLogs(baseDir, stageNames) + LogsCSVWriter(parsedLogs).writeToCsv(outputCsvPath) +} diff --git a/project-minimization-scripts/src/main/kotlin/org/plan/research/minimization/scripts/LogParser.kt b/project-minimization-scripts/src/main/kotlin/org/plan/research/minimization/scripts/LogParser.kt deleted file mode 100644 index 86702f6f..00000000 --- a/project-minimization-scripts/src/main/kotlin/org/plan/research/minimization/scripts/LogParser.kt +++ /dev/null @@ -1,255 +0,0 @@ -package org.plan.research.minimization.scripts - -import org.plan.research.minimization.plugin.benchmark.BenchmarkConfig - -import com.charleskorn.kaml.Yaml - -import java.io.File -import java.nio.file.Path -import java.time.LocalTime -import java.time.format.DateTimeFormatter - -import kotlinx.serialization.decodeFromString - -// Data class to hold parsed project statistics -data class ProjectStatistics( - val projectName: String, - val durationMinutes: Long, - val ktFilesBefore: Int, - val ktFilesAfter: Int, - - val totalLocBefore: Int, - val totalLocStages: List, - val totalLocAfter: Int, - - val blankLocBefore: Int, - val blankLocStages: List, - val blankLocAfter: Int, - - val commentLocBefore: Int, - val commentLocStages: List, - val commentLocAfter: Int, - - val codeLocBefore: Int, - val codeLocStages: List, - val codeLocAfter: Int, - - val numberOfCompilations: Int, -) - -data class StageLoc( - val totalLocAfter: Int, - val blankLocAfter: Int, - val commentLocAfter: Int, - val codeLocAfter: Int, -) - -class LogParser { - @Suppress("TOO_LONG_FUNCTION") - fun parseLogs(baseDir: Path, stageNames: List, outputCsvPath: Path) { - val configPath = baseDir.resolve("config.yaml") - val config = loadConfig(configPath) - - val projectsStatistics = mutableListOf() - - config.projects.forEach { project -> - val logFilePath = findLatestStatisticsLog(baseDir, project.path) - - logFilePath ?: run { - return@forEach - } - - val logs = logFilePath.toFile().readLines() - - val startLine = logs.find { it.contains("Start Project minimization for project: ${project.name}") } - val endLine = logs.find { it.contains("End Project minimization for project: ${project.name}") } - - if (startLine != null && endLine != null) { - val relevantLogs = logs.subList(logs.indexOf(startLine), logs.indexOf(endLine) + 1) - - val durationMinutes = calculateDuration(startLine, endLine) - - val ktFilesBefore = extractStat(relevantLogs, "Kotlin files", first = true) - val ktFilesAfter = extractStat(relevantLogs, "Kotlin files", first = false) - - val totalLocBefore = extractStat(relevantLogs, "Total LOC", first = true) - val totalLocAfter = extractStat(relevantLogs, "Total LOC", first = false) - - val blankLocBefore = extractStat(relevantLogs, "Blank LOC", first = true) - val blankLocAfter = extractStat(relevantLogs, "Blank LOC", first = false) - - val commentLocBefore = extractStat(relevantLogs, "Comment LOC", first = true) - val commentLocAfter = extractStat(relevantLogs, "Comment LOC", first = false) - - val codeLocBefore = extractStat(relevantLogs, "Code LOC", first = true) - val codeLocAfter = extractStat(relevantLogs, "Code LOC", first = false) - - val totalLocStages: MutableList = mutableListOf() - val blankLocStages: MutableList = mutableListOf() - val commentLocStages: MutableList = mutableListOf() - val codeLocStages: MutableList = mutableListOf() - - stageNames.forEach { stageName -> - val stageResults = extractStageStatistics(relevantLogs, stageName) - totalLocStages.add(stageResults.totalLocAfter) - blankLocStages.add(stageResults.blankLocAfter) - commentLocStages.add(stageResults.commentLocAfter) - codeLocStages.add(stageResults.codeLocAfter) - } - - val numberOfCompilations = relevantLogs.count { it.contains("Project dir:") } - - projectsStatistics.add( - ProjectStatistics( - project.name, - durationMinutes, - ktFilesBefore, - ktFilesAfter, - totalLocBefore, - totalLocStages, - totalLocAfter, - blankLocBefore, - blankLocStages, - blankLocAfter, - commentLocBefore, - commentLocStages, - commentLocAfter, - codeLocBefore, - codeLocStages, - codeLocAfter, - numberOfCompilations, - ), - ) - } - } - - writeCsv(projectsStatistics, stageNames, outputCsvPath) - } - - private fun loadConfig(configPath: Path): BenchmarkConfig { - val configFile = configPath.toFile() - - if (!configFile.exists()) { - throw IllegalArgumentException("Configuration file not found at: $configPath") - } - - val content = configFile.readText() - - return Yaml.default.decodeFromString(content) - } - - private fun findLatestStatisticsLog(baseDir: Path, projectPath: String): Path? { - val logsDir = baseDir.resolve(projectPath).resolve("minimization-logs").toFile() - - if (!logsDir.exists() || !logsDir.isDirectory) { - return null - } - - val executionDirs = logsDir.listFiles { file -> file.isDirectory } ?: return null - - val latestExecutionDir = executionDirs.maxByOrNull { it.name } ?: return null - - val statisticsLog = latestExecutionDir.resolve("statistics.log") - return if (statisticsLog.exists()) statisticsLog.toPath() else null - } - - private fun calculateDuration(startLine: String, endLine: String): Long { - val startTime = extractTime(startLine) - val endTime = extractTime(endLine) - return java.time.Duration.between(startTime, endTime) - .toMinutes() - } - - private fun extractStageStatistics(logs: List, stageName: String): StageLoc { - val startStageLine = logs.find { it.contains("Start $stageName level stage") } - val endStageLine = logs.find { it.contains("End $stageName level stage") } - - if (startStageLine != null && endStageLine != null) { - val stageLogs = logs.subList(logs.indexOf(startStageLine), logs.indexOf(endStageLine) + 1) - - val totalLocAfter = extractStat(stageLogs, "Total LOC", first = false) - - val blankLocAfter = extractStat(stageLogs, "Blank LOC", first = false) - - val commentLocAfter = extractStat(stageLogs, "Comment LOC", first = false) - - val codeLocAfter = extractStat(stageLogs, "Code LOC", first = false) - - return StageLoc( - totalLocAfter, - blankLocAfter, - commentLocAfter, - codeLocAfter, - ) - } - return StageLoc(0, 0, 0, 0) - } - - private fun extractTime(logLine: String): LocalTime { - val timePart = logLine.substringBefore(" - ") - return LocalTime.parse(timePart, DateTimeFormatter.ofPattern("HH:mm:ss.SSS")) - } - - private fun extractStat(logs: List, statName: String, first: Boolean): Int { - val statLines = logs.filter { it.contains(statName) } - val targetLine = if (first) statLines.firstOrNull() else statLines.lastOrNull() - return targetLine?.substringAfter("$statName: ")?.trim()?.toIntOrNull() ?: 0 - } - - @Suppress("TOO_LONG_FUNCTION") - private fun writeCsv(projectsStatistics: List, stageNames: List, outputCsvPath: Path) { - val file = File(outputCsvPath.toUri()) - file.bufferedWriter().use { writer -> - val header = buildString { - append("Project Name,Duration (minutes),Kotlin Files Before,Kotlin Files After,Total LOC Before,") - stageNames.forEach { stageName -> - append("$stageName Total LOC,") - } - append("Total LOC After,Blank LOC Before,") - stageNames.forEach { stageName -> - append("$stageName Blank LOC,") - } - append("Blank LOC After,Comment LOC Before,") - stageNames.forEach { stageName -> - append("$stageName Comment LOC,") - } - append("Comment LOC After,Code LOC Before,") - stageNames.forEach { stageName -> - append("$stageName Code LOC,") - } - append("Code LOC After,Number of Compilations\n") - } - - writer.write(header) - - projectsStatistics.forEach { stats -> - val row = buildString { - append("${stats.projectName},${stats.durationMinutes},${stats.ktFilesBefore},${stats.ktFilesAfter},") - append("${stats.totalLocBefore},") - stats.totalLocStages.forEach { append("$it,") } - append("${stats.totalLocAfter},${stats.blankLocBefore},") - stats.blankLocStages.forEach { append("$it,") } - append("${stats.blankLocAfter},${stats.commentLocBefore},") - stats.commentLocStages.forEach { append("$it,") } - append("${stats.commentLocAfter},${stats.codeLocBefore},") - stats.codeLocStages.forEach { append("$it,") } - append("${stats.codeLocAfter},${stats.numberOfCompilations}\n") - } - writer.write(row) - } - } - } -} - -// Usage: [ ...] -fun main(args: Array) { - if (args.size < 2) { - return - } - - val baseDir = Path.of(args[0]) - val outputCsvPath = Path.of(args[1]) - val stageNames = args.slice(2 until args.size) - - LogParser().parseLogs(baseDir, stageNames, outputCsvPath) -} diff --git a/project-minimization-scripts/src/main/kotlin/org/plan/research/minimization/scripts/logs/LogsCSVWriter.kt b/project-minimization-scripts/src/main/kotlin/org/plan/research/minimization/scripts/logs/LogsCSVWriter.kt new file mode 100644 index 00000000..0e50ba67 --- /dev/null +++ b/project-minimization-scripts/src/main/kotlin/org/plan/research/minimization/scripts/logs/LogsCSVWriter.kt @@ -0,0 +1,71 @@ +package org.plan.research.minimization.scripts.logs + +import org.plan.research.minimization.plugin.model.benchmark.logs.LinesMetric +import org.plan.research.minimization.plugin.model.benchmark.logs.ProjectStatistics + +import com.github.doyaaaaaken.kotlincsv.client.ICsvFileWriter +import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter + +import java.nio.file.Path + +import kotlin.io.path.outputStream +import kotlin.time.Duration + +class LogsCSVWriter(val logs: List) { + init { + require(logs.map { it.stageMetrics.map { it.name } }.distinct().size == 1) { + "Different project has different stages. Dumping to CSV is impossible." + } + } + + fun writeToCsv(path: Path) = csvWriter().open(path.outputStream()) { + writeHeader() + logs.forEach { writeProject(it) } + } + + private fun ICsvFileWriter.writeHeader() { + val stagesName = logs.first().stageMetrics.map { "After ${it.name}" } + val locColumns = (listOf("Initial") + stagesName).flatMap { name -> + listOf( + "Total", + "Blank", + "Comment", + "Code", + ).map { "${name.capitalize()} LOC ($it)" } + }.toTypedArray() + writeRow( + "Project Name", + "Elapsed Time", + "Initial Files Number", + "Minimized Files Number", + *locColumns, + "Number of Compilations", + ) + } + + private fun ICsvFileWriter.writeProject(project: ProjectStatistics) { + writeRow( + project.projectName, + project.elapsed.dump(), + project.ktFiles.before, + project.ktFiles.after, + *project.lines.flattenBefore(), + *(project.stageMetrics.flatMap { it.linesMetric.flatten() }.toTypedArray()), + project.numberOfCompilations, + ) + } + + private fun LinesMetric.flatten() = listOf(totalLines.after, blankLines.after, commentLines.after, codeLines.after) + private fun String.capitalize() = replaceFirstChar { it.uppercase() } + private fun Duration.dump(): String { + val (minutes, seconds) = this.toComponents { minutes, seconds, _ -> minutes to seconds } + return if (minutes > 0) { + "$minutes m $seconds s" + } else { + "$seconds s" + } + } + + private fun LinesMetric.flattenBefore() = + arrayOf(totalLines.before, blankLines.before, commentLines.before, codeLines.before) +}