perf(agent): run diagnostic cleanup off startup thread (#2147)

* perf(agent): run stage cleanup off startup thread

* fix(agent): preserve active diagnostic stages

* test(agent): verify active stage locks cross-process
This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-11-05 20:04:16 -05:00
1 parent 6a681bcbc2
commit c8a76e7cce
4 files changed
+61 -7

No files matched your search

@@ -17,7 +17,7 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
runCatching { EnhancedSessionLogShare.cleanupAbandonedStages(this) }
Thread({ runCatching { EnhancedSessionLogShare.cleanupAbandonedStages(this) } }, "diagnostic-stage-cleanup").start()
com.droiddeck.launcher.ui.Themes.loadGradient(this)
com.droiddeck.launcher.ui.BuildWatermark.install(this)
HomeApp.trackRecents(this)
@@ -51,17 +51,21 @@ internal object EnhancedSessionLogShare {
internal class DiagnosticStageLease internal constructor(
val directory: File,
private val canonicalPath: String,
private val channel: FileChannel,
private val lock: FileLock,
) : Closeable {
override fun close() {
try {
lock.release()
} finally {
synchronized(stageLifecycleMonitor) {
activeStagePaths.remove(canonicalPath)
try {
channel.close()
lock.release()
} finally {
directory.deleteRecursively()
try {
channel.close()
} finally {
directory.deleteRecursively()
}
}
}
}
@@ -117,6 +121,7 @@ internal object EnhancedSessionLogShare {
override fun initialValue(): SecureSourceOpener? = null
}
private val stageLifecycleMonitor = Any()
private val activeStagePaths = mutableSetOf<String>()
internal fun acquireDiagnosticStage(context: Context, outputName: String): DiagnosticStageLease {
val cache = context.cacheDir
@@ -133,7 +138,9 @@ internal object EnhancedSessionLogShare {
try {
val lock = channel.tryLock()
?: throw IOException("Could not lock the diagnostic staging directory")
DiagnosticStageLease(stage, channel, lock)
val canonicalPath = stage.canonicalPath
activeStagePaths += canonicalPath
DiagnosticStageLease(stage, canonicalPath, channel, lock)
} catch (error: Exception) {
channel.close()
stage.deleteRecursively()
@@ -149,6 +156,7 @@ internal object EnhancedSessionLogShare {
cache.listFiles()?.forEach { stage ->
if (!stage.isDirectory || Files.isSymbolicLink(stage.toPath()) ||
stage.canonicalFile.parentFile != canonicalCache || !isDiagnosticStage(stage.name)) return@forEach
if (stage.canonicalPath in activeStagePaths) return@forEach
val active = File(stage, ".active")
if (Files.isSymbolicLink(active.toPath())) return@forEach
val channel = runCatching {
@@ -0,0 +1,19 @@
package com.droiddeck.launcher.session;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public final class DiagnosticStageLockProbe {
public static void main(String[] args) throws Exception {
try (FileChannel channel = FileChannel.open(Path.of(args[0]), StandardOpenOption.WRITE)) {
FileLock lock = channel.tryLock();
if (lock == null) {
System.exit(0);
}
lock.release();
System.exit(1);
}
}
}
@@ -6,6 +6,7 @@ import java.text.SimpleDateFormat
import java.util.UUID
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -34,6 +35,32 @@ class SessionLogShareZipTest {
EnhancedSessionLogShare.cleanupAbandonedStages(app)
assertFalse(abandoned.exists())
assertTrue(active.directory.isDirectory)
EnhancedSessionLogShare.cleanupAbandonedStages(app)
assertTrue(active.directory.isDirectory)
val probeClassResource = checkNotNull(
DiagnosticStageLockProbe::class.java.getResource("DiagnosticStageLockProbe.class"),
)
val probeClass = File(probeClassResource.toURI())
var classPathRoot = checkNotNull(probeClass.parentFile)
repeat(4) { classPathRoot = checkNotNull(classPathRoot.parentFile) }
val classPath = classPathRoot.absolutePath
val javaName = if (checkNotNull(System.getProperty("os.name")).startsWith("Windows")) {
"java.exe"
} else {
"java"
}
val java = File(File(System.getProperty("java.home"), "bin"), javaName)
val probe = ProcessBuilder(
java.absolutePath,
"-cp",
classPath,
DiagnosticStageLockProbe::class.java.name,
File(active.directory, ".active").absolutePath,
).redirectErrorStream(true).start()
val completed = probe.waitFor(5, TimeUnit.SECONDS)
if (!completed) probe.destroyForcibly()
assertTrue("lock probe timed out", completed)
assertEquals("cleanup released the active stage lock", 0, probe.exitValue())
} finally {
active.close()
}