Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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" }
Expand Down
55 changes: 54 additions & 1 deletion project-minimization-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -71,4 +76,52 @@ tasks.test {
jvmArgumentProviders += CommandLineArgumentProvider {
listOf("-Didea.kotlin.plugin.use.k2=true")
}
}
}

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,
)
}
}
}
1 change: 0 additions & 1 deletion project-minimization-plugin/gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ class BenchmarkAction : AnAction() {
runCatching {
val project = e.project ?: return
val benchmarkingService = project.service<BenchmarkService>()
benchmarkingService.benchmark()
benchmarkingService.benchmark { BenchmarkSettings.isBenchmarkingEnabled = false }
}.onFailure {
logger.error("Benchmarking failed", it)
}.also {
BenchmarkSettings.isBenchmarkingEnabled = false
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String>): List<ProjectStatistics> {
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<String>,
stageNames: List<String>,
): 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 = "<tmp>", 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<String>,
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<String>.extractRelevantLogsFor(name: String): List<String>? {
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<String>, 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<String>, 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"
}
}
Original file line number Diff line number Diff line change
@@ -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<String>) = cliktRunner.main(args.drop(1))
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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<ProjectOpeningService>().openProject(projectPath) ?: run {
reportOpeningProblem()
return
}
val minimizationResult = project
.service<MinimizationService>()
.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)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Loading