diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fd093d..7c9b55a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,16 @@ Dropping a requirement of a major version of a dependency is a new contract. ## [Unreleased] [Unreleased]: https://github.com/atlassian/infrastructure/compare/release-4.26.1...master +### Added +- Add `AsyncProfiler.Builder` to expose more options. + +### Deprecated +- Deprecate `AsyncProfiler.` and `WallClockProfiler` in favor of `AsyncProfiler.Builder`. + +### Fixed +- Fix `AsyncProfiler` flamegraph file extension from `.svg` to `.html`. +- Bump `WallClockProfiler` to async-profiler v2.9. + ## [4.26.1] - 2023-07-20 [4.26.1]: https://github.com/atlassian/infrastructure/compare/release-4.26.0...release-4.26.1 diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt index 82f2764d..57558aa5 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfiler.kt @@ -10,7 +10,13 @@ import java.time.Duration.ofSeconds /** * Asynchronous profiler. See https://github.com/jvm-profiling-tools/async-profiler#basic-usage */ -class AsyncProfiler : Profiler { +class AsyncProfiler private constructor( + private val startParams: List, + private val outputFile: String +) : Profiler { + + @Deprecated("Use AsyncProfiler.Builder instead") + constructor() : this(emptyList(), "flamegraph.html") private val release = "async-profiler-2.9-linux-x64" @@ -27,24 +33,55 @@ class AsyncProfiler : Profiler { pid: Int ): RemoteMonitoringProcess { val script = "./$release/profiler.sh" + val params = startParams.joinToString(separator = " ") IdempotentAction("start async-profiler") { - ssh.execute("$script start $pid") + ssh.execute("$script start $params $pid") }.retry(2, StaticBackoff(ofSeconds(5))) - return ProfilerProcess(script, pid) + return ProfilerProcess(script, pid, outputFile) } private class ProfilerProcess( private val script: String, - private val pid: Int + private val pid: Int, + private val outputFile: String ) : RemoteMonitoringProcess { - private val flameGraphFile = "flamegraph.svg" override fun stop(ssh: SshConnection) { - ssh.execute("$script stop $pid -o flamegraph > $flameGraphFile", timeout = ofSeconds(50)) + ssh.execute("$script stop $pid -o flamegraph > $outputFile", timeout = ofSeconds(50)) } override fun getResultPath(): String { - return flameGraphFile + return outputFile + } + } + + class Builder { + + private var outputFile: String = "flamegraph.html" + private val startParams = mutableListOf() + + fun outputFile(outputFile: String) = apply { this.outputFile = outputFile } + + fun wallClockMode() = apply { + startParams.add("-e") + startParams.add("wall") + } + + fun interval(interval: Duration) = apply { + startParams.add("-i") + if (interval < ofSeconds(1)) { + startParams.add(interval.nano.toString()) + } else { + throw Exception("The interval $interval seems to big. Usually it's counted in milliseconds or nanoseconds. Try an interval under a second.") + } + } + + fun extraParams(vararg extraParams: String) = apply { + startParams.addAll(extraParams) + } + + fun build(): Profiler { + return AsyncProfiler(ArrayList(startParams), outputFile) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/WallClockProfiler.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/WallClockProfiler.kt index 5918f6a3..5aff0da5 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/WallClockProfiler.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/WallClockProfiler.kt @@ -1,37 +1,13 @@ package com.atlassian.performance.tools.infrastructure.api.profiler -import com.atlassian.performance.tools.infrastructure.api.process.RemoteMonitoringProcess -import com.atlassian.performance.tools.ssh.api.SshConnection +import java.time.Duration /** * A profiler to sample all threads equally every given period of time regardless of thread status. */ -class WallClockProfiler : Profiler { - override fun install(ssh: SshConnection) { - ssh.execute("wget -q https://github.com/jvm-profiling-tools/async-profiler/releases/download/v1.5/async-profiler-1.5-linux-x64.tar.gz") - ssh.execute("mkdir async-profiler") - ssh.execute("tar -xzf async-profiler-1.5-linux-x64.tar.gz -C async-profiler") - ssh.execute("sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'") - ssh.execute("sudo sh -c 'echo 0 > /proc/sys/kernel/kptr_restrict'") - } - - override fun start( - ssh: SshConnection, - pid: Int - ): RemoteMonitoringProcess { - ssh.execute("./async-profiler/profiler.sh -e wall -i 9ms -b 160000000 start $pid") - return ProfilerProcess(pid) - } - - private class ProfilerProcess(private val pid: Int) : RemoteMonitoringProcess { - private val flameGraphFile = "wall-clock-flamegraph.svg" - - override fun stop(ssh: SshConnection) { - ssh.execute("./async-profiler/profiler.sh stop $pid -o svg > $flameGraphFile") - } - - override fun getResultPath(): String { - return flameGraphFile - } - } -} +@Deprecated("Use AsyncProfiler.Builder instead") +class WallClockProfiler : Profiler by AsyncProfiler.Builder() + .wallClockMode() + .interval(Duration.ofMillis(9)) + .outputFile("wall-clock-flamegraph.html") + .build() diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt index f3042d1f..f0f1d999 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt @@ -13,24 +13,34 @@ import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Files.createTempFile +import java.time.Duration import java.util.function.Consumer class AsyncProfilerIT { @Test fun shouldWorkOnXenial() { - testOn("16.04") + val profiler = AsyncProfiler.Builder().build() + testOn(profiler, "16.04") } @Test fun shouldWorkOnFocal() { - testOn("20.04") + val profiler = AsyncProfiler.Builder().build() + testOn(profiler, "20.04") } - private fun testOn(ubuntuVersion: String) { - // given - val profiler = AsyncProfiler() + @Test + fun shouldRunInWallClockMode() { + val profiler = AsyncProfiler.Builder() + .wallClockMode() + .interval(Duration.ofMillis(9)) + .build() + + testOn(profiler, "20.04") + } + private fun testOn(profiler: Profiler, ubuntuVersion: String) { testOnInstalledJira(ubuntuVersion) { installedJira -> val sshClient = installedJira.server.ssh sshClient.newConnection().use { ssh -> @@ -39,7 +49,7 @@ class AsyncProfilerIT { val startedJira = JiraLaunchScript().start(installedJira) val process = profiler.start(ssh, startedJira.pid) Thread.sleep(5000) - process.stop(ssh) + process!!.stop(ssh) // then val profilerResult = RemotePath(sshClient.host, process.getResultPath())