Skip to content

Commit

Permalink
Snapshots performance profiling (#315)
Browse files Browse the repository at this point in the history
* Rudimentary snapshots performance profiling

* Update to be testRule, add profile property to gradle to enable

* Fix tests/nullability

* Detekt

* Inline

* noinline

* End span

* Remove trace

* Fix and more tracing
  • Loading branch information
rbro112 authored Dec 19, 2024
1 parent f8f7dd3 commit 8fcac98
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ class EmergePlugin : Plugin<Project> {
)
addItem("tag (optional): ${extension.snapshotOptions.tag.orEmpty()}", snapshotsHeading)
addItem("enabled: ${extension.snapshotOptions.enabled.getOrElse(true)}", snapshotsHeading)
addItem("profile: ${extension.snapshotOptions.profile.getOrElse(false)}", snapshotsHeading)

val reaperHeading = addHeading("reaper")
addItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ abstract class SnapshotOptions : ProductOptions() {
* defaults to true
*/
abstract val includePreviewParamPreviews: Property<Boolean>

/**
* Record profiling information for snapshot tests, defaults to false.
*/
abstract val profile: Property<Boolean>
}

abstract class ReaperOptions : ProductOptions() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ abstract class LocalSnapshots : DefaultTask() {
@get:Optional
abstract val includePreviewParamPreviews: Property<Boolean>

@get:Input
@get:Optional
abstract val profile: Property<Boolean>

@TaskAction
fun execute() {
val artifactMetadataFiles = packageDir.asFileTree.matching {
Expand Down Expand Up @@ -149,6 +153,11 @@ abstract class LocalSnapshots : DefaultTask() {
it.add("invoke_data_path")
it.add("/data/local/tmp/$COMPOSE_SNAPSHOTS_FILENAME")
}
if (profile.getOrElse(false)) {
it.add("-e")
it.add("save_profile")
it.add("true")
}
it.add("${testAppId}/${testInstrumentationRunner.get()}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ private fun registerSnapshotLocalTask(
it.testInstrumentationRunner.set(testInstrumentationRunner)
it.includePrivatePreviews.set(extension.snapshotOptions.includePrivatePreviews)
it.includePreviewParamPreviews.set(extension.snapshotOptions.includePreviewParamPreviews)
it.profile.set(extension.snapshotOptions.profile)
it.dependsOn(packageTask)
}
}
Expand Down
2 changes: 2 additions & 0 deletions snapshots/sample/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ emerge {

snapshots {
tag.setFromVariant()

profile.set(true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Bundle
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import com.emergetools.snapshots.util.Profiler
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
Expand Down Expand Up @@ -38,6 +39,7 @@ internal object SnapshotSaver {
fqn: String,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig,
) {
Profiler.startSpan("save")
val snapshotsDir = File(filesDir, SNAPSHOTS_DIR_NAME)
if (!snapshotsDir.exists() && !snapshotsDir.mkdirs()) {
error("Unable to create snapshots storage directory.")
Expand All @@ -52,6 +54,7 @@ internal object SnapshotSaver {
keyName = keyName,
bitmap = bitmap
)

if (saveMetadata) {
saveMetadata(
snapshotsDir = snapshotsDir,
Expand All @@ -61,6 +64,7 @@ internal object SnapshotSaver {
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)
}
Profiler.endSpan()
}

fun saveError(
Expand All @@ -69,6 +73,7 @@ internal object SnapshotSaver {
errorType: SnapshotErrorType,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig,
) {
Profiler.startSpan("saveError")
val snapshotsDir = File(filesDir, SNAPSHOTS_DIR_NAME)
if (!snapshotsDir.exists() && !snapshotsDir.mkdirs()) {
error("Unable to create snapshots storage directory.")
Expand All @@ -83,16 +88,19 @@ internal object SnapshotSaver {
composePreviewSnapshotConfig = composePreviewSnapshotConfig,
)
}
Profiler.endSpan()
}

private fun saveImage(
snapshotsDir: File,
keyName: String,
bitmap: Bitmap,
) {
Profiler.startSpan("saveImage")
saveFile(snapshotsDir, "$keyName$PNG_EXTENSION") {
bitmap.compress(Bitmap.CompressFormat.PNG, DEFAULT_PNG_QUALITY, this)
}
Profiler.endSpan()
}

private fun saveMetadata(
Expand All @@ -102,6 +110,7 @@ internal object SnapshotSaver {
fqn: String,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig,
) {
Profiler.startSpan("saveMetadata")
val metadata: SnapshotMetadata = SnapshotMetadata.SuccessMetadata(
name = keyName,
displayName = displayName,
Expand All @@ -116,6 +125,7 @@ internal object SnapshotSaver {
saveFile(snapshotsDir, "$keyName$JSON_EXTENSION") {
write(jsonString.toByteArray(Charset.defaultCharset()))
}
Profiler.endSpan()
}

private fun saveErrorMetadata(
Expand All @@ -125,6 +135,7 @@ internal object SnapshotSaver {
errorType: SnapshotErrorType,
composePreviewSnapshotConfig: ComposePreviewSnapshotConfig,
) {
Profiler.startSpan("saveErrorMetadata")
val keyName = composePreviewSnapshotConfig.keyName()
val metadata: SnapshotMetadata = SnapshotMetadata.ErrorMetadata(
name = composePreviewSnapshotConfig.keyName(),
Expand All @@ -140,13 +151,15 @@ internal object SnapshotSaver {
saveFile(snapshotsDir, "$keyName$JSON_EXTENSION") {
write(jsonString.toByteArray(Charset.defaultCharset()))
}
Profiler.endSpan()
}

private fun saveFile(
dir: File,
filenameWithExtension: String,
writer: FileOutputStream.() -> Unit,
) {
Profiler.startSpan("saveFile")
val outputFile = File(dir, filenameWithExtension)

if (outputFile.exists()) {
Expand All @@ -156,6 +169,7 @@ internal object SnapshotSaver {

Log.d(TAG, "Saving file to ${outputFile.path}")
outputFile.outputStream().use { writer(it) }
Profiler.endSpan()
}

private const val PNG_EXTENSION = ".png"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.emergetools.snapshots.compose

import android.util.Log
import androidx.compose.runtime.Composer
import com.emergetools.snapshots.util.Profiler
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import kotlin.math.ceil
Expand All @@ -17,6 +18,7 @@ object ComposableInvoker {
composer: Composer,
vararg args: Any?
) {
Profiler.startSpan("invokeComposable")
try {
val composableClass = Class.forName(className)
val method = composableClass.findComposableMethod(methodName, *args)
Expand All @@ -35,6 +37,8 @@ object ComposableInvoker {
} catch (e: Exception) {
Log.w(TAG, "Failed to invoke Composable Method '$className.$methodName'")
throw e
} finally {
Profiler.endSpan()
}
}

Expand Down Expand Up @@ -98,6 +102,7 @@ object ComposableInvoker {
methodName: String,
vararg previewParamArgs: Any?
): Method? {
Profiler.startSpan("findComposableMethod")
val argsArray: Array<Class<out Any>> =
previewParamArgs.mapNotNull { it?.javaClass }.toTypedArray()
return try {
Expand All @@ -121,6 +126,8 @@ object ComposableInvoker {
Log.w(TAG, "Method $methodName not found in class ${this.simpleName}")
null
}
} finally {
Profiler.endSpan()
}
}

Expand All @@ -134,6 +141,7 @@ object ComposableInvoker {
composer: Composer,
vararg args: Any?
): Any? {
Profiler.startSpan("Method.invokeComposableMethod")
val composerIndex = parameterTypes.indexOfLast { it == Composer::class.java }
val realParams = composerIndex
val thisParams = if (instance != null) 1 else 0
Expand Down Expand Up @@ -175,7 +183,10 @@ object ComposableInvoker {
else -> error("Unexpected index")
}
}
return invoke(instance, *arguments)

val result = invoke(instance, *arguments)
Profiler.endSpan()
return result
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import com.emergetools.snapshots.EmergeSnapshots
import com.emergetools.snapshots.SnapshotErrorType
import com.emergetools.snapshots.compose.previewparams.PreviewParamUtils
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import com.emergetools.snapshots.util.Profiler

@Suppress("TooGenericExceptionCaught")
fun snapshotComposable(
snapshotRule: EmergeSnapshots,
activity: PreviewActivity,
previewConfig: ComposePreviewSnapshotConfig,
) {
Profiler.startSpan("snapshotComposable")
try {
snapshot(
activity = activity,
Expand All @@ -40,6 +42,8 @@ fun snapshotComposable(
)
// Re-throw to fail the test
throw e
} finally {
Profiler.endSpan()
}
}

Expand All @@ -53,14 +57,20 @@ private fun snapshot(
val previewParameters =
PreviewParamUtils.getPreviewProviderParameters(previewConfig) ?: arrayOf<Any?>(null)

Profiler.startSpan("configToDeviceSpec")
val deviceSpec = configToDeviceSpec(previewConfig)
Profiler.endSpan()

for (index in previewParameters.indices) {
Profiler.startSpan("previewParam_$index")
val prevParam = previewParameters[index]
Log.d(
EmergeComposeSnapshotReflectiveParameterizedInvoker.TAG,
"Invoking composable method with preview parameter: $prevParam"
)

Profiler.startSpan("setupComposeView")

val composeView = ComposeView(activity)
composeView.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
val args = if (prevParam != null) arrayOf(prevParam) else emptyArray()
Expand All @@ -69,6 +79,8 @@ private fun snapshot(
previewParameter = previewConfig.previewParameter?.copy(index = index)
)

Profiler.endSpan()

// Update activity window size if device is specified
if (deviceSpec != null) {
updateActivityBounds(activity, deviceSpec)
Expand All @@ -86,12 +98,15 @@ private fun snapshot(
}

// Add the ComposeView to the activity
Profiler.startSpan("addContentView")
activity.addContentView(
composeView,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
)
Profiler.endSpan()

composeView.post {
Profiler.startSpan("post")
val size = measureViewSize(composeView, previewConfig)
val bitmap = captureBitmap(composeView, size.width, size.height)

Expand All @@ -106,14 +121,17 @@ private fun snapshot(

// Reset activity content view
(composeView.parent as? ViewGroup)?.removeView(composeView)
Profiler.endSpan()
}
Profiler.endSpan()
}
}

private fun measureViewSize(
view: View,
previewConfig: ComposePreviewSnapshotConfig
): IntSize {
Profiler.startSpan("measureViewSize")
val deviceSpec = configToDeviceSpec(previewConfig)

// Use exact measurements when we have them
Expand Down Expand Up @@ -149,18 +167,25 @@ private fun measureViewSize(
View.MeasureSpec.makeMeasureSpec(view.height, View.MeasureSpec.AT_MOST)
}

Profiler.startSpan("view.measure")
view.measure(widthMeasureSpec, heightMeasureSpec)
return IntSize(view.measuredWidth, view.measuredHeight)
Profiler.endSpan()

val size = IntSize(view.measuredWidth, view.measuredHeight)
Profiler.endSpan()
return size
}

private fun updateActivityBounds(activity: Activity, deviceSpec: DeviceSpec) {
Profiler.startSpan("updateActivityBounds")
// Apply the device spec dimensions to the activity window
val width = deviceSpec.widthPixels
val height = deviceSpec.heightPixels

if (width > 0 && height > 0) {
activity.window.setLayout(width, height)
}
Profiler.endSpan()
}

private fun dpToPx(dp: Int, scalingFactor: Float): Int {
Expand All @@ -172,6 +197,7 @@ fun captureBitmap(
width: Int,
height: Int,
): Bitmap? {
Profiler.startSpan("captureBitmap")
try {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
Expand All @@ -185,5 +211,7 @@ fun captureBitmap(
e,
)
return null
} finally {
Profiler.endSpan()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.emergetools.snapshots.EmergeSnapshots
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import com.emergetools.snapshots.shared.ComposeSnapshots
import com.emergetools.snapshots.util.Profiler
import kotlinx.serialization.json.Json
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -79,8 +80,18 @@ class EmergeComposeSnapshotReflectiveParameterizedInvoker(
@get:Rule
val snapshotRule: EmergeSnapshots = EmergeSnapshots()

@get:Rule
val profiler = Profiler.getInstance(parameter.previewConfig)

fun someMethod() {
Profiler.startSpan("someMethod")
// some code
Profiler.endSpan()
}

@Test
fun reflectiveComposableInvoker() {
Profiler.startSpan("reflectiveComposableInvoker")
Log.i(TAG, "Running snapshot test ${parameter.previewConfig.keyName()}")
// Force application to be debuggable to ensure PreviewActivity doesn't early exit
val applicationInfo = InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo
Expand All @@ -90,5 +101,6 @@ class EmergeComposeSnapshotReflectiveParameterizedInvoker(
scenarioRule.scenario.onActivity { activity ->
snapshotComposable(snapshotRule, activity, previewConfig)
}
Profiler.endSpan()
}
}
Loading

0 comments on commit 8fcac98

Please sign in to comment.