Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class DumpHangedTestPlugin : Plugin<Project> {
/** Plugin properties */
abstract class DumpHangedTestProperties @Inject constructor(objects: ObjectFactory) {
// Time offset (in seconds) before a test reaches its timeout at which dumps should be started.
// Defaults to 60 seconds.
// Defaults to 180 seconds.
val dumpOffset: Property<Long> = objects.property(Long::class.java)
}

Expand Down Expand Up @@ -82,7 +82,7 @@ class DumpHangedTestPlugin : Plugin<Project> {
return
}

val dumpOffset = props.dumpOffset.getOrElse(60)
val dumpOffset = props.dumpOffset.getOrElse(180)
val delay = t.timeout.map { it.minusSeconds(dumpOffset) }.orNull

if (delay == null || delay.seconds < 0) {
Expand Down Expand Up @@ -123,25 +123,28 @@ class DumpHangedTestPlugin : Plugin<Project> {
throw IOException("Could not create dump directory $dumpsDir")
}

var executorCount = 0
val processes = mutableListOf<ProcessHandle>()
ProcessHandle.current().children().use { children ->
children.filter { it.info().commandLine().getOrElse { "" }.contains("Gradle Test Executor") }
.forEach { process ->
executorCount++
collectDump(t, dumpsDir, process)
processes.add(process)

process.children().use { descendants ->
descendants.forEach { child -> collectDump(t, dumpsDir, child) }
descendants.forEach { child -> processes.add(child) }
}
}
}
if (executorCount == 0) {
if (processes.isEmpty()) {
t.logger.warn("No Gradle test executors found for ${t.path}; attempting all-JVM thread dumps")
}

// Preserve all thread stacks before a slow heap dump can consume the remaining time.
processes.forEach { process -> collectThreadDump(t, dumpsDir, process) }

// Just in case collect all thread dumps by using special PID `0`.
val allThreadsFile = file(dumpsDir, "all-thread-dumps")
runCmd(t.logger, t.path, Redirect.to(allThreadsFile), "jcmd", "0", "Thread.print", "-l")
processes.forEach { process -> collectHeapDump(t, dumpsDir, process) }
t.logger.quiet("Finished dump collection for ${t.path}; output directory: $dumpsDir")
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
Expand Down Expand Up @@ -196,7 +199,7 @@ class DumpHangedTestPlugin : Plugin<Project> {
}
}

private fun collectDump(
private fun collectThreadDump(
t: Task,
baseDir: File,
process: ProcessHandle
Expand All @@ -208,13 +211,18 @@ class DumpHangedTestPlugin : Plugin<Project> {
// It will be writen into `/tmp/javacore.YYYYMMDD.HHMMSS.PID.SEQ.txt
runCmd(t.logger, t.path, Redirect.INHERIT, "kill", "-3", pid)
} else {
// Collect heap dump by pid.
val heapDumpPath = file(baseDir, "$pid-heap-dump", "hprof").absolutePath
runCmd(t.logger, t.path, Redirect.INHERIT, "jcmd", pid, "GC.heap_dump", heapDumpPath)

// Collect thread dump by pid.
val threadDumpFile = file(baseDir, "$pid-thread-dump", "log")
runCmd(t.logger, t.path, Redirect.to(threadDumpFile), "jcmd", pid, "Thread.print", "-l")
}
}

private fun collectHeapDump(t: Task, baseDir: File, process: ProcessHandle) {
if (process.info().command().getOrElse { "" }.contains("/ibm8")) {
return
}
val pid = process.pid().toString()
val heapDumpPath = file(baseDir, "$pid-heap-dump", "hprof").absolutePath
runCmd(t.logger, t.path, Redirect.INHERIT, "jcmd", pid, "GC.heap_dump", heapDumpPath)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ class DumpHangedTestIntegrationTest : GradleFixture() {
assertNotNull(dumpFiles.find { it.startsWith("all-thread-dumps") })
}

@Test
fun `should start dumps three minutes before timeout by default`() {
val output = runGradleTest(testSleepMillis = 10_000, timeoutSeconds = 185, dumpOffset = null)

assertTrue(output.contains("Taking dumps after 5 seconds delay for :test"))
assertFalse(output.any { it.contains("has exceeded its configured timeout") })
}

@Test
fun `should report directory failures with stack trace`() {
writeFile("build/dumps", "This file prevents creating the dump directory")
Expand All @@ -55,7 +63,7 @@ class DumpHangedTestIntegrationTest : GradleFixture() {

@Test
@EnabledOnOs(OS.LINUX, OS.MAC)
fun `should attempt thread dumps after heap dump failure`() {
fun `should collect all thread dumps before attempting heap dumps`() {
val jcmd = writeFile(
"bin/jcmd",
"""
Expand Down Expand Up @@ -83,9 +91,17 @@ class DumpHangedTestIntegrationTest : GradleFixture() {
assertTrue(dumps.any { it.name.contains("-thread-dump-") && it.readText().startsWith("Synthetic thread dump") })
assertTrue(dumps.any { it.name.startsWith("all-thread-dumps-") && it.readText().contains("PID 0") })
assertTrue(output.any { it.startsWith("Finished dump collection for :test;") })
val firstHeapDump = output.indexOfFirst { it.startsWith("Starting dump command") && it.contains("GC.heap_dump") }
val lastThreadDump = output.indexOfLast { it.startsWith("Completed dump command") && it.contains("Thread.print") }
assertTrue(lastThreadDump >= 0 && firstHeapDump > lastThreadDump)
}

private fun runGradleTest(testSleepMillis: Long, env: Map<String, String> = emptyMap()): List<String> {
private fun runGradleTest(
testSleepMillis: Long,
env: Map<String, String> = emptyMap(),
timeoutSeconds: Long = 20,
dumpOffset: Long? = 5
): List<String> {
writeSettings("""rootProject.name = "test-project"""")

writeRootProject(
Expand All @@ -111,13 +127,11 @@ class DumpHangedTestIntegrationTest : GradleFixture() {
}

dumpHangedTest {
// Set the dump offset for 5 seconds to trigger taking dumps after 15 seconds.
dumpOffset.set(5)
${dumpOffset?.let { "dumpOffset.set($it)" } ?: ""}
}

tasks.withType<Test>().configureEach {
// Set test timeout after 20 seconds.
timeout.set(Duration.ofSeconds(20))
timeout.set(Duration.ofSeconds($timeoutSeconds))

useJUnitPlatform()
}
Expand Down
Loading