Skip to content

Commit

Permalink
feat(android): support tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
Malinskiy committed Sep 20, 2024
1 parent cd16c0a commit 36467f0
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.malinskiy.marathon.config.vendor.android.AdbEndpoint
import com.malinskiy.marathon.config.vendor.android.AllureConfiguration
import com.malinskiy.marathon.config.vendor.android.AndroidTestBundleConfiguration
import com.malinskiy.marathon.config.vendor.android.FileSyncConfiguration
import com.malinskiy.marathon.config.vendor.android.TracingConfiguration
import com.malinskiy.marathon.config.vendor.android.ScreenRecordConfiguration
import com.malinskiy.marathon.config.vendor.android.SerialStrategy
import com.malinskiy.marathon.config.vendor.android.TestAccessConfiguration
Expand Down Expand Up @@ -74,6 +75,7 @@ sealed class VendorConfiguration {
@JsonProperty("testAccessConfiguration") val testAccessConfiguration: TestAccessConfiguration = TestAccessConfiguration(),
@JsonProperty("adbServers") val adbServers: List<AdbEndpoint> = listOf(AdbEndpoint()),
@JsonProperty("disableWindowAnimation") val disableWindowAnimation: Boolean = DEFAULT_DISABLE_WINDOW_ANIMATION,
@JsonProperty("tracingConfiguration") val tracingConfiguration: TracingConfiguration = TracingConfiguration(),
) : VendorConfiguration() {
fun safeAndroidSdk(): File = androidSdk ?: throw ConfigurationException("No android SDK path specified")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.malinskiy.marathon.config.vendor.android

import com.fasterxml.jackson.annotation.JsonProperty
import java.io.File

data class TracingConfiguration(
@JsonProperty("enabled") val enabled: Boolean = false,
@JsonProperty("pbtxt") val pbtxt: File? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data class Attachment(val file: File, val type: AttachmentType, val name: String
const val LOG = "log"
const val LOGCAT = "logcat"
const val XCODEBUILDLOG = "xcodebuild-log"
const val TRACING = "perfetto-trace"
}
}

Expand All @@ -20,5 +21,6 @@ enum class AttachmentType(val mimeType: String) {
SCREENSHOT_PNG("image/png"),
SCREENSHOT_WEBP("image/webp"),
VIDEO("video/mp4"),
LOG("text/plain");
LOG("text/plain"),
TRACING("text/plain");
}
1 change: 1 addition & 0 deletions core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ enum class FileType(val dir: String, val suffix: String) {
SCREENSHOT_GIF("screenshot", "jpg"),
XCTESTRUN("xctestrun", "xctestrun"),
BILL("bill", "json"),
TRACING("tracing", "perfetto-trace"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.malinskiy.marathon.android.executor.listeners.TestResultsListener
import com.malinskiy.marathon.android.executor.listeners.filesync.FileSyncTestRunListener
import com.malinskiy.marathon.android.executor.listeners.screenshot.AdamScreenCaptureTestRunListener
import com.malinskiy.marathon.android.executor.listeners.screenshot.ScreenCapturerTestRunListener
import com.malinskiy.marathon.android.executor.listeners.tracing.PerfettoRunListener
import com.malinskiy.marathon.android.executor.listeners.video.ScreenRecorderTestBatchListener
import com.malinskiy.marathon.android.model.ShellCommandResult
import com.malinskiy.marathon.device.screenshot.Rotation
Expand Down Expand Up @@ -47,6 +48,7 @@ abstract class BaseAndroidDevice(
protected val serialStrategy: SerialStrategy,
protected val configuration: Configuration,
protected val androidConfiguration: VendorConfiguration.AndroidConfiguration,
protected val testBundleIdentifier: AndroidTestBundleIdentifier,
protected val track: Track,
protected val timer: Timer
) : AndroidDevice, CoroutineScope {
Expand Down Expand Up @@ -252,25 +254,48 @@ abstract class BaseAndroidDevice(
prepareRecorderListener(feature, fileManager, devicePoolId, testBatch.id, screenRecordingPolicy, attachmentProviders)
} ?: NoOpTestRunListener()

val tracingConfiguration = this@BaseAndroidDevice.androidConfiguration.tracingConfiguration
val tracingListener = if (tracingConfiguration.enabled && tracingConfiguration.pbtxt != null) {
PerfettoRunListener(
fileManager,
devicePoolId,
testBatch,
this,
tracingConfiguration,
testBundleIdentifier,
this
).also { attachmentProviders.add(it) }
} else {
NoOpTestRunListener()
}

val logListener = TestRunListenerAdapter(
LogListener(this.toDeviceInfo(), this, devicePoolId, testBatch.id, LogWriter(fileManager), attachmentName = Attachment.Name.LOGCAT)
.also { attachmentProviders.add(it) }
LogListener(
this.toDeviceInfo(),
this,
devicePoolId,
testBatch.id,
LogWriter(fileManager),
attachmentName = Attachment.Name.LOGCAT
)
.also { attachmentProviders.add(it) }
)

val fileSyncTestRunListener =
FileSyncTestRunListener(devicePoolId, this, this@BaseAndroidDevice.androidConfiguration.fileSyncConfiguration, fileManager)

val adamScreenCaptureTestRunListener = AdamScreenCaptureTestRunListener(devicePoolId, this, fileManager, testBatch.id)
attachmentProviders.add(adamScreenCaptureTestRunListener)

return CompositeTestRunListener(
listOf(
recorderListener,
logListener,
TestResultsListener(testBatch, this, deferred, timer, devicePoolId, attachmentProviders),
DebugTestRunListener(this),
adamScreenCaptureTestRunListener,
fileSyncTestRunListener
fileSyncTestRunListener,
tracingListener,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,26 @@ class RemoteFileManager(private val device: AndroidDevice) {
return remoteFileForTest(videoFileName(test, testBatchId, chunk))
}

fun remoteTracingForTest(test: Test, testBatchId: String): String {
return "$PERFETTO_TRACE_ROOT/${traceFileName(test, testBatchId)}"
}

private fun remoteFileForTest(filename: String): String {
return "$outputDir/$filename"
}

private fun traceFileName(test: Test, testBatchId: String): String {
return remoteFileName(test, testBatchId, extension = "perfetto-trace", chunk = null)
}

private fun videoFileName(test: Test, testBatchId: String, chunk: Long? = null): String {
return remoteFileName(test, testBatchId, extension = "mp4", chunk = chunk)

}

private fun remoteFileName(test: Test, testBatchId: String, extension: String, chunk: Long? = null): String {
val chunkId = chunk?.let { "-$it" } ?: ""
val testSuffix = "-$testBatchId$chunkId.mp4"
val testSuffix = "-$testBatchId$chunkId.$extension"
val rawTestName = "${test.toClassName()}-${test.method}".escape()
val testName = rawTestName.take(MAX_FILENAME - testSuffix.length)
val fileName = "$testName$testSuffix"
Expand All @@ -49,5 +62,7 @@ class RemoteFileManager(private val device: AndroidDevice) {
companion object {
const val MAX_FILENAME = 255
const val TMP_PATH = "/data/local/tmp"
const val PERFETTO_TRACE_ROOT = "/data/misc/perfetto-traces"
const val PERFETTO_CONFIG_FILE = "$TMP_PATH/tracing.pbtx"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ class AdamAndroidDevice(
internal val client: AndroidDebugBridgeClient,
private val deviceStateTracker: DeviceStateTracker,
private val logcatManager: LogcatManager,
private val testBundleIdentifier: AndroidTestBundleIdentifier,
private val installContext: CoroutineContext,
adbSerial: String,
configuration: Configuration,
androidConfiguration: VendorConfiguration.AndroidConfiguration,
testBundleIdentifier: AndroidTestBundleIdentifier,
track: Track,
timer: Timer,
serialStrategy: SerialStrategy
) : BaseAndroidDevice(adbSerial, serialStrategy, configuration, androidConfiguration, track, timer), LineListener {
) : BaseAndroidDevice(adbSerial, serialStrategy, configuration, androidConfiguration, testBundleIdentifier, track, timer), LineListener {

/**
* This adapter is thread-safe but the internal reusable buffer should be considered if we ever need to make screenshots in parallel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ class AdamDeviceProvider(
client,
multiServerDeviceStateTracker.getTracker(client),
logcatManager,
testBundleIdentifier,
installDispatcher,
serial,
configuration,
vendorConfiguration,
testBundleIdentifier,
track,
timer,
vendorConfiguration.serialStrategy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.malinskiy.marathon.android.executor.listeners.tracing

import com.malinskiy.marathon.android.AndroidDevice
import com.malinskiy.marathon.android.AndroidTestBundleIdentifier
import com.malinskiy.marathon.android.InstrumentationInfo
import com.malinskiy.marathon.android.executor.listeners.NoOpTestRunListener
import com.malinskiy.marathon.android.model.TestIdentifier
import com.malinskiy.marathon.config.vendor.android.TracingConfiguration
import com.malinskiy.marathon.device.DevicePoolId
import com.malinskiy.marathon.device.toDeviceInfo
import com.malinskiy.marathon.exceptions.TransferException
import com.malinskiy.marathon.execution.Attachment
import com.malinskiy.marathon.execution.AttachmentType
import com.malinskiy.marathon.io.FileManager
import com.malinskiy.marathon.io.FileType
import com.malinskiy.marathon.log.MarathonLogging
import com.malinskiy.marathon.report.attachment.AttachmentListener
import com.malinskiy.marathon.report.attachment.AttachmentProvider
import com.malinskiy.marathon.test.TestBatch
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope
import org.apache.commons.text.StringSubstitutor
import org.apache.commons.text.lookup.StringLookupFactory
import kotlin.coroutines.cancellation.CancellationException
import kotlin.system.measureTimeMillis


class PerfettoRunListener(
private val fileManager: FileManager,
private val pool: DevicePoolId,
private val testBatch: TestBatch,
private val device: AndroidDevice,
private val tracingConfiguration: TracingConfiguration,
private val testBundleIdentifier: AndroidTestBundleIdentifier,
coroutineScope: CoroutineScope
) : NoOpTestRunListener(), AttachmentProvider, CoroutineScope by coroutineScope {
private val logger = MarathonLogging.logger("PerfettoRunListener")

private var job: Job? = null
private val attachmentListeners = mutableListOf<AttachmentListener>()
private var targetPid: Int? = null
private var tracingConfig: String? = null
private var renderedConfig: String? = null

override fun registerListener(listener: AttachmentListener) {
attachmentListeners.add(listener)
}

override suspend fun beforeTestRun(info: InstrumentationInfo?) {
super.beforeTestRun(info)
tracingConfig = tracingConfiguration.pbtxt?.readText()
}

override suspend fun testRunStarted(runName: String, testCount: Int) {
super.testRunStarted(runName, testCount)

// Assumption is that we can never execute test batches with multiple packages
val testBundle = testBundleIdentifier.identify(testBatch.tests.first())
val result = device.criticalExecuteShellCommand("pidof ${testBundle.instrumentationInfo.applicationPackage}")
//TODO: check app is profileable and debuggable for java heap profiling

targetPid = result.output.trim().toIntOrNull()
logger.debug { "Target pid: $targetPid" }

if (tracingConfig != null) {
val lookup = StringSubstitutor(
StringLookupFactory.INSTANCE.mapStringLookup(
mapOf(
"TRACING_TARGET_PID" to targetPid,
"TRACING_TARGET_PACKAGE" to testBundle.instrumentationInfo.applicationPackage
)
)
)
renderedConfig = lookup.replace(tracingConfig)

logger.debug { "Rendered config:\n$renderedConfig" }
}
}

override suspend fun testStarted(test: TestIdentifier) {
super.testStarted(test)

val remoteFile = device.fileManager.remoteTracingForTest(test.toTest(), testBatch.id)

job = async(coroutineContext + CoroutineName("perfetto ${device.serialNumber}")) {
supervisorScope {
try {
val result =
device.executeShellCommand("echo '${renderedConfig}' | perfetto --txt -c - -o $remoteFile")
logger.debug { "perfetto process finished: $result" }
} catch (e: CancellationException) {
logger.warn(e) { "perfetto start was interrupted" }
throw e
} catch (e: Exception) {
logger.error("Something went wrong while recording perfetto trace", e)
throw e
}
}
}
}

override suspend fun testEnded(test: TestIdentifier, testMetrics: Map<String, String>) {
super.testEnded(test, testMetrics)
pullTrace(test)
logger.debug { "Finished processing" }
}

private suspend fun pullTrace(test: TestIdentifier) {
try {
stop()

val test = test.toTest()
val remoteFile = device.fileManager.remoteTracingForTest(test, testBatch.id)
val localFile = fileManager.createFile(FileType.TRACING, pool, device.toDeviceInfo(), test, testBatch.id)
val millis = measureTimeMillis {
device.safePullFile(remoteFile, localFile.toString())
}
logger.trace { "Pulling finished in ${millis}ms $remoteFile " }

attachmentListeners.forEach {
it.onAttachment(
test,
Attachment(localFile, AttachmentType.TRACING, name = Attachment.Name.TRACING)
)
}

/**
* Read-only partition hence -f is required
*/
device.safeExecuteShellCommand("rm -f $remoteFile")
} catch (e: TransferException) {
logger.warn { "Can't pull tracing file" }
}
}

private suspend fun stop() {
logger.debug { "Stopping perfetto" }
val stop = measureTimeMillis {
device.safeExecuteShellCommand("killall perfetto")
}
logger.debug { "Stopped perfetto: ${stop}ms" }
val join = measureTimeMillis {
job?.join()
}
logger.debug { "Joining perfetto: ${join}ms" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ScreenRecorderTestBatchListener(
attachmentListeners.add(listener)
}

private val logger = MarathonLogging.logger("ScreenRecorder")
private val logger = MarathonLogging.logger("ScreenRecorderTestBatchListener")

private val screenRecorder = ScreenRecorder(device, videoConfiguration)

Expand Down

0 comments on commit 36467f0

Please sign in to comment.