Skip to content

Commit

Permalink
Generalize AsyncProfiler
Browse files Browse the repository at this point in the history
  • Loading branch information
dagguh committed Oct 9, 2023
1 parent 3954f34 commit ffe467a
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 44 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ 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.<init>` 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
private val outputFile: String
) : Profiler {

@Deprecated("Use AsyncProfiler.Builder instead")
constructor() : this(emptyList(), "flamegraph.html")

private val release = "async-profiler-2.9-linux-x64"

Expand All @@ -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.html"

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<String>()

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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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())
Expand Down

0 comments on commit ffe467a

Please sign in to comment.