Merge changes from topic "safeoutputs" into androidx-main
* changes:
Rework file output for safety, dont swallow throwables
Fix file / trace file moving
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ArgumentInjectingApplication.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ArgumentInjectingApplication.kt
index 5c58edc..99f775b 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ArgumentInjectingApplication.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ArgumentInjectingApplication.kt
@@ -52,6 +52,10 @@
"ACTIVITY-MISSING,CODE-COVERAGE,DEBUGGABLE,EMULATOR,LOW-BATTERY,UNLOCKED," +
"UNSUSTAINED-ACTIVITY-MISSING,ENG-BUILD"
)
+ putString(
+ "androidx.benchmark.thermalThrottle.sleepDurationSeconds",
+ "0"
+ )
// TODO: consider moving default directory to files dir.
putString(
"additionalTestOutputDir",
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
index 02322c9..1ffe3b6 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
@@ -19,14 +19,15 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
import java.io.File
import java.util.Date
import kotlin.test.assertEquals
import kotlin.test.assertFalse
+import org.junit.Assert
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -105,6 +106,7 @@
public fun dirUsableByAppAndShell_writeAppReadShell() {
val dir = Outputs.dirUsableByAppAndShell
val file = File.createTempFile("testFile", null, dir)
+ file.setReadable(true, false)
try {
file.writeText(file.name) // use name, as it's fairly unique
Assert.assertEquals(
@@ -115,4 +117,70 @@
file.delete()
}
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public fun dirUsableByAppAndShell_writeShellReadShell() {
+ val dir = Outputs.dirUsableByAppAndShell
+
+ // simple way to get a unique path, not shared across runs
+ val file = File.createTempFile("shellwrite", null, dir)
+ val path = file.absolutePath
+ file.delete()
+
+ Shell.executeScriptSilent("rm -f $path")
+ try {
+ Shell.executeScriptSilent("echo test > $path")
+ assertEquals(
+ "test\n",
+ Shell.executeScriptCaptureStdout("cat $path")
+ )
+ file.appendBytes("extra".toByteArray())
+ } finally {
+ Shell.executeScriptSilent("rm -f $path")
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public fun dirUsableByAppAndShell_writeShellReadApp() {
+ val dir = Outputs.dirUsableByAppAndShell
+
+ // simple way to get a unique path, not shared across runs
+ val file = File.createTempFile("shellwrite", null, dir)
+ val path = file.absolutePath
+ file.delete()
+
+ Shell.executeScriptSilent("rm -f $path")
+ try {
+ Shell.executeScriptSilent("echo test > $path")
+ assertEquals(
+ "test\n",
+ File(path).readText()
+ )
+ file.appendBytes("extra".toByteArray())
+ } finally {
+ Shell.executeScriptSilent("rm -f $path")
+ }
+ }
+
+ /**
+ * NOTE: this test checks that the instrumentation argument additionalTestOutputDir isn't set to
+ * an invalid / unusable location.
+ *
+ * Running through Studio/Gradle, this isn't defined by the library, it's defined by AGP.
+ *
+ * If this test fails, we need to handle the directory differently.
+ */
+ @Test
+ fun additionalTestOutputDir_appWrite() {
+ val additionalTestOutputDir = Arguments.additionalTestOutputDir
+ assumeTrue(additionalTestOutputDir != null)
+ val file = File.createTempFile("testFile", null, File(additionalTestOutputDir!!))
+ try {
+ file.writeText("testString")
+ } finally {
+ file.delete()
+ }
+ }
}
\ No newline at end of file
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index aa98691..fbbbc32 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -76,6 +76,7 @@
get() = if (profilerOverride != null) profilerOverride else _profiler
internal val profilerSampleFrequency: Int
internal val profilerSampleDurationSeconds: Long
+ internal val thermalThrottleSleepDurationSeconds: Long
internal var error: String? = null
internal val additionalTestOutputDir: String?
@@ -164,6 +165,12 @@
"$profilerSampleFrequency, duration $profilerSampleDurationSeconds"
)
}
+
+ thermalThrottleSleepDurationSeconds =
+ arguments.getBenchmarkArgument("thermalThrottle.sleepDurationSeconds")?.ifBlank { null }
+ ?.toLong()
+ ?: 90
+
additionalTestOutputDir = arguments.getString("additionalTestOutputDir")
Log.d(BenchmarkState.TAG, "additionalTestOutputDir=$additionalTestOutputDir")
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt
index b0dc302..fa768fc 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/BenchmarkState.kt
@@ -621,7 +621,7 @@
internal const val MIN_TEST_ITERATIONS = 1
private const val THROTTLE_MAX_RETRIES = 2
- private const val THROTTLE_BACKOFF_S = 90L
+ private val THROTTLE_BACKOFF_S = Arguments.thermalThrottleSleepDurationSeconds
private var firstBenchmark = true
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
index 3865c5c..246ca42 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
@@ -42,14 +42,22 @@
/**
* The usable output directory, given permission issues with `adb shell` on Android R.
* Both the app and the shell have access to this output folder.
+ *
+ * This dir can be read/written by app
+ * This dir can be read by shell (see [forceFilesForShellAccessible] for API 21/22!)
*/
public val dirUsableByAppAndShell: File
+ /**
+ * Any file created by this process for the shell to use must be explicitly made filesystem
+ * globally readable, as prior to API 23 the shell didn't have access by default.
+ */
+ val forceFilesForShellAccessible: Boolean = Build.VERSION.SDK_INT in 21..22
+
init {
// Be explicit about the TimeZone for stable formatting
formatter.timeZone = TimeZone.getTimeZone("UTC")
- @Suppress("DEPRECATION")
@SuppressLint("NewApi")
dirUsableByAppAndShell = when {
Build.VERSION.SDK_INT >= 29 -> {
@@ -70,12 +78,21 @@
"additionalTestOutputDir argument required to declare output dir."
)
+ if (forceFilesForShellAccessible) {
+ // By default, shell doesn't have access to app dirs on 21/22 so we need to modify
+ // this so that the shell can output here too
+ dirUsableByAppAndShell.setReadable(true, false)
+ dirUsableByAppAndShell.setWritable(true, false)
+ dirUsableByAppAndShell.setExecutable(true, false)
+ }
+
Log.d(BenchmarkState.TAG, "Usable output directory: $dirUsableByAppAndShell")
outputDirectory = Arguments.additionalTestOutputDir?.let { File(it) }
?: dirUsableByAppAndShell
Log.d(BenchmarkState.TAG, "Output Directory: $outputDirectory")
+ outputDirectory.mkdirs()
}
/**
@@ -93,47 +110,27 @@
block: (file: File) -> Unit,
): String {
val sanitizedName = sanitizeFilename(fileName)
+ val destination = File(outputDirectory, sanitizedName)
- // We need to copy files over anytime `dirUsableByAppAndShell` is different from
- // `outputDirectory`.
- val override = dirUsableByAppAndShell != outputDirectory
// We override the `additionalTestOutputDir` argument.
// Context: b/181601156
val file = File(dirUsableByAppAndShell, sanitizedName)
- try {
- block.invoke(file)
- } finally {
- var destination = file
- if (override) {
- // This respects the `additionalTestOutputDir` argument.
- val actualOutputDirectory = outputDirectory
- destination = File(actualOutputDirectory, sanitizedName)
- Log.d(BenchmarkState.TAG, "Copying $file to $destination")
- try {
- destination.mkdirs()
- file.copyTo(destination, overwrite = true)
- } catch (exception: Throwable) {
- // This can happen when `additionalTestOutputDir` being passed in cannot
- // be written to. The shell does not have permissions to do the necessary
- // setup, and this can cause `adb pull` to fail.
- val message = """
- Unable to copy files to ${destination.absolutePath}.
- Please pull the Macrobenchmark results manually by using:
- adb pull ${file.absolutePath}
- """.trimIndent()
- Log.e(BenchmarkState.TAG, message, exception)
+ block.invoke(file)
+ check(file.exists()) { "File doesn't exist!" }
- // TODO(b/227510293): return failure/null to signal file isn't readable
- destination = file
- }
- }
- InstrumentationResults.reportAdditionalFileToCopy(
- key = reportKey,
- absoluteFilePath = destination.absolutePath,
- reportOnRunEndOnly = reportOnRunEndOnly
- )
- return destination.absolutePath
+ if (dirUsableByAppAndShell != outputDirectory) {
+ // We need to copy files over anytime `dirUsableByAppAndShell` is different from
+ // `outputDirectory`.
+ Log.d(BenchmarkState.TAG, "Copying $file to $destination")
+ file.copyTo(destination, overwrite = true)
}
+
+ InstrumentationResults.reportAdditionalFileToCopy(
+ key = reportKey,
+ absoluteFilePath = destination.absolutePath,
+ reportOnRunEndOnly = reportOnRunEndOnly
+ )
+ return destination.absolutePath
}
public fun sanitizeFilename(filename: String): String {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index 0f0b95f..b0b4ad3 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -119,9 +119,8 @@
ShellImpl.executeCommandUnsafe("md5sum $path").substringBefore(" ")
} else {
// this isn't good, but it's good enough for API 22
- val out = ShellImpl.executeCommandUnsafe("ls -l $path").split(Regex("\\s+"))[3]
- println("value is $out")
- out
+ val result = ShellImpl.executeCommandUnsafe("ls -l $path")
+ if (result.isBlank()) "" else result.split(Regex("\\s+"))[3]
}
check(sum.isNotBlank()) {
"Checksum for $path was blank"
@@ -169,11 +168,10 @@
fun createRunnableExecutable(name: String, inputStream: InputStream): String {
// dirUsableByAppAndShell is writable, but we can't execute there (as of Q),
// so we copy to /data/local/tmp
- val externalDir = Outputs.dirUsableByAppAndShell
val writableExecutableFile = File.createTempFile(
/* prefix */ "temporary_$name",
/* suffix */ null,
- /* directory */ externalDir
+ /* directory */ Outputs.dirUsableByAppAndShell
)
val runnableExecutablePath = "/data/local/tmp/$name"
@@ -181,6 +179,11 @@
writableExecutableFile.outputStream().use {
inputStream.copyTo(it)
}
+ if (Outputs.forceFilesForShellAccessible) {
+ // executable must be readable by shell to be moved, and for some reason
+ // doesn't inherit shell readability from dirUsableByAppAndShell
+ writableExecutableFile.setReadable(true, false)
+ }
moveToTmpAndMakeExecutable(
src = writableExecutableFile.absolutePath,
dst = runnableExecutablePath
@@ -552,6 +555,12 @@
val externalDir = Outputs.dirUsableByAppAndShell
val scriptContentFile = File.createTempFile("temporaryScript", null, externalDir)
+ if (Outputs.forceFilesForShellAccessible) {
+ // script content must be readable by shell, and for some reason doesn't
+ // inherit shell readability from dirUsableByAppAndShell
+ scriptContentFile.setReadable(true, false)
+ }
+
// only create/read/delete stdin/stderr files if they are needed
val stdinFile = stdin?.run {
File.createTempFile("temporaryStdin", null, externalDir)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index 163a308..8cc6dde 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -72,6 +72,9 @@
listOf("*")
}
configProtoFile.writeBytes(perfettoConfig(atraceApps).validateAndEncode())
+ if (Outputs.forceFilesForShellAccessible) {
+ configProtoFile.setReadable(true, /* ownerOnly = */ false)
+ }
}
userspaceTrace("start perfetto process") {
helper.startCollecting(configProtoFile.absolutePath, false)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 198ab7a..1ffbb6a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -23,6 +23,7 @@
import androidx.benchmark.Outputs
import androidx.benchmark.Outputs.dateToFileName
import androidx.benchmark.PropOverride
+import androidx.benchmark.Shell
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
/**
@@ -80,6 +81,12 @@
reportKey = "perfetto_trace_$traceLabel"
) {
capture!!.stop(it.absolutePath)
+ if (Outputs.forceFilesForShellAccessible) {
+ // This shell written file must be made readable to be later accessed by this
+ // process (e.g. for appending UiState). Unlike in other places, shell
+ // must increase access, since it's giving the app access
+ Shell.executeScriptSilent("chmod 777 ${it.absolutePath}")
+ }
}
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
index 01381a9..e3a3d72 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
@@ -97,18 +97,20 @@
val path = "$UNBUNDLED_PERFETTO_ROOT_DIR/config.pb"
// Move the config to a directory that unbundled perfetto has permissions for.
Shell.executeScriptSilent("rm -f $path")
- if (Build.VERSION.SDK_INT >= 24) {
- Shell.executeScriptSilent("mv $configFilePath $path")
- } else {
+ if (Build.VERSION.SDK_INT == 23) {
// Observed stderr output (though command still completes successfully) on:
// google/shamu/shamu:6.0.1/MOB31T/3671974:userdebug/dev-keys
// Doesn't repro on all API 23 devices :|
- Shell.executeScriptCaptureStdoutStderr("mv $configFilePath $path").also {
+ Shell.executeScriptCaptureStdoutStderr("cp $configFilePath $path").also {
check(
it.stdout.isBlank() &&
(it.stderr.isBlank() || it.stderr.startsWith("mv: chown"))
- )
+ ) {
+ "Observed unexpected output: it"
+ }
}
+ } else {
+ Shell.executeScriptSilent("cp $configFilePath $path")
}
path
} else {
@@ -116,8 +118,15 @@
}
val outputPath = getPerfettoTmpOutputFilePath()
+
+ if (!unbundled && Build.VERSION.SDK_INT == 29) {
+ // observed this on unrooted emulator
+ val output = Shell.executeScriptCaptureStdoutStderr("rm -f $outputPath")
+ Log.d(LOG_TAG, "Attempted to remove $outputPath, result = $output")
+ } else {
+ Shell.executeScriptSilent("rm -f $outputPath")
+ }
// Remove already existing temporary output trace file if any.
- Shell.executeScriptSilent("rm -f $outputPath")
// Perfetto
val perfettoCmd = perfettoCommand(actualConfigPath, isTextProtoConfig)
@@ -309,6 +318,7 @@
private fun copyFileOutput(destinationFile: String): Boolean {
val sourceFile = getPerfettoTmpOutputFilePath()
val filePath = File(destinationFile)
+ filePath.setWritable(true, false)
val destDirectory = filePath.parent
if (destDirectory != null) {
// Check if the directory already exists
@@ -328,14 +338,14 @@
// Copy the collected trace from /data/misc/perfetto-traces/trace_output.pb to
// destinationFile
try {
- val moveResult =
- Shell.executeScriptCaptureStdoutStderr("mv $sourceFile $destinationFile")
- if (!moveResult.isBlank()) {
+ val copyResult =
+ Shell.executeScriptCaptureStdoutStderr("cp $sourceFile $destinationFile")
+ if (!copyResult.isBlank()) {
Log.e(
LOG_TAG,
"""
- Unable to move perfetto output file from $sourceFile
- to $destinationFile due to $moveResult.
+ Unable to copy perfetto output file from $sourceFile
+ to $destinationFile due to $copyResult.
""".trimIndent()
)
return false
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
index c9949cc..207731a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
@@ -16,12 +16,8 @@
package androidx.benchmark.perfetto
-import android.os.Build
-import android.util.Log
import androidx.annotation.RestrictTo
-import androidx.benchmark.BenchmarkState.Companion.TAG
import java.io.File
-import java.io.FileNotFoundException
import perfetto.protos.Trace
import perfetto.protos.TracePacket
import perfetto.protos.UiState
@@ -45,19 +41,5 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun File.appendUiState(state: UiState) {
val traceToAppend = Trace(packet = listOf(TracePacket(ui_state = state)))
- appendBytesSafely(traceToAppend.encode())
-}
-
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun File.appendBytesSafely(bytes: ByteArray) {
- try {
- appendBytes(bytes)
- } catch (e: FileNotFoundException) {
- if (Build.VERSION.SDK_INT in 21..22) {
- // Failure is common on API 21/22 due to b/227510293
- Log.d(TAG, "Unable to append additional bytes to ${this.absolutePath}")
- } else {
- throw e
- }
- }
+ appendBytes(traceToAppend.encode())
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java b/benchmark/benchmark-common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
index 3305363..e50a95a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
@@ -154,7 +154,7 @@
mSimpleperfPath = findSimpleperf();
checkIfPerfEnabled();
createSimpleperfDataDir();
- createSimpleperfProcess(mSimpleperfPath, args);
+ startRecordingProcess(args);
mState = State.STARTED;
}
@@ -327,7 +327,7 @@
/**
* Convert a .data simpleperf file to the .trace proto format.
- *
+ * <p>
* Paths may be relative to the simpleperf data dir.
*
* @param inputPath Path of the .data file within the simpleperf output directory
@@ -338,6 +338,7 @@
@NonNull String outputPath
) {
ArrayList<String> args = new ArrayList<>();
+ args.add(mSimpleperfPath);
args.add("report-sample");
args.add("--protobuf");
args.add("--show-callchain");
@@ -346,14 +347,23 @@
args.add("-o");
args.add(outputPath);
- createSimpleperfProcess(mSimpleperfPath, args);
+ createProcess(args);
waitForSimpleperfProcess();
}
- private void createSimpleperfProcess(String simpleperfPath, List<String> recordArgs) {
+ private void createProcess(List<String> args) {
+ ProcessBuilder pb = new ProcessBuilder(args).directory(new File(mSimpleperfDataDir));
+ try {
+ mSimpleperfProcess = pb.start();
+ } catch (IOException e) {
+ throw new Error("failed to create simpleperf process: " + e.getMessage());
+ }
+ }
+
+ private void startRecordingProcess(List<String> recordArgs) {
// 1. Prepare simpleperf arguments.
ArrayList<String> args = new ArrayList<>();
- args.add(simpleperfPath);
+ args.add(mSimpleperfPath);
args.add("record");
args.add("--log-to-android-buffer");
args.add("--log");
@@ -365,12 +375,7 @@
args.addAll(recordArgs);
// 2. Create the simpleperf process.
- ProcessBuilder pb = new ProcessBuilder(args).directory(new File(mSimpleperfDataDir));
- try {
- mSimpleperfProcess = pb.start();
- } catch (IOException e) {
- throw new Error("failed to create simpleperf process: " + e.getMessage());
- }
+ createProcess(args);
// 3. Wait until simpleperf starts recording.
String startFlag = readReply();
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 1671a9b..1be0375 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -24,7 +24,6 @@
import androidx.benchmark.UserspaceTracing
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.perfetto.UiState
-import androidx.benchmark.perfetto.appendBytesSafely
import androidx.benchmark.perfetto.appendUiState
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
@@ -224,9 +223,9 @@
// that events won't lie outside the bounds of the trace content.
userspaceTrace = UserspaceTracing.commitToTrace()
}?.apply {
- // trace completed, and copied into app writeable dir
+ // trace completed, and copied into shell writeable dir
val file = File(this)
- file.appendBytesSafely(userspaceTrace!!.encode())
+ file.appendBytes(userspaceTrace!!.encode())
file.appendUiState(
UiState(
timelineStart = null,