1 /* <lambda>null2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.benchmark 18 19 import android.annotation.SuppressLint 20 import android.os.Build 21 import android.os.Environment 22 import android.util.Log 23 import androidx.annotation.RestrictTo 24 import androidx.benchmark.FileMover.moveTo 25 import androidx.test.platform.app.InstrumentationRegistry 26 import java.io.File 27 import java.text.SimpleDateFormat 28 import java.util.Date 29 import java.util.TimeZone 30 31 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 32 object Outputs { 33 34 private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss") 35 36 /** 37 * Matches substrings to be removed from filenames. 38 * 39 * We only allow digits, ascii letters, `_` and `-` to remain. 40 * 41 * Note `-` is important for baseline profiles, see b/303034735 42 */ 43 private val sanitizerRegex = Regex("([^0-9a-zA-Z._-]+)") 44 45 /** The intended output directory that respects the `additionalTestOutputDir`. */ 46 val outputDirectory: File 47 48 /** 49 * The usable output directory, given permission issues with `adb shell` on Android R. Both the 50 * app and the shell have access to this output folder. 51 * 52 * This dir can be read/written by app This dir can be read by shell (see 53 * [forceFilesForShellAccessible] for API 21/22!) 54 */ 55 val dirUsableByAppAndShell: File 56 57 /** 58 * Any file created by this process for the shell to use must be explicitly made filesystem 59 * globally readable, as prior to API 23 the shell didn't have access by default. 60 */ 61 val forceFilesForShellAccessible: Boolean = Build.VERSION.SDK_INT in 21..22 62 63 init { 64 // Be explicit about the TimeZone for stable formatting 65 formatter.timeZone = TimeZone.getTimeZone("UTC") 66 67 val context = InstrumentationRegistry.getInstrumentation().targetContext 68 @SuppressLint("NewApi") 69 dirUsableByAppAndShell = 70 when { 71 Build.VERSION.SDK_INT >= 29 -> { 72 // On Android Q+ we are using the media directory because that is 73 // the directory that the shell has access to. Context: b/181601156 74 // Additionally, Benchmarks append user space traces to the ones produced 75 // by the Macro Benchmark run; and that is a lot simpler to do if we use the 76 // Media directory. (b/216588251) 77 @Suppress("DEPRECATION") 78 context.externalMediaDirs.firstOrNull { 79 Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED 80 } 81 } 82 Build.VERSION.SDK_INT <= 22 -> { 83 // prior to API 23, shell didn't have access to externalCacheDir 84 context.cacheDir 85 } 86 else -> context.externalCacheDir 87 } 88 ?: throw IllegalStateException( 89 "Unable to select a directory for writing files, " + 90 "additionalTestOutputDir argument required to declare output dir." 91 ) 92 93 if (forceFilesForShellAccessible) { 94 // By default, shell doesn't have access to app dirs on 21/22 so we need to modify 95 // this so that the shell can output here too 96 dirUsableByAppAndShell.setReadable(true, false) 97 dirUsableByAppAndShell.setWritable(true, false) 98 dirUsableByAppAndShell.setExecutable(true, false) 99 } 100 101 Log.d(BenchmarkState.TAG, "Usable output directory: $dirUsableByAppAndShell") 102 103 outputDirectory = 104 Arguments.additionalTestOutputDir?.let { File(it) } ?: dirUsableByAppAndShell 105 106 Log.d(BenchmarkState.TAG, "Output Directory: $outputDirectory") 107 108 // Clear all the existing files in the output directories 109 listOf(outputDirectory, dirUsableByAppAndShell).forEach { 110 it.listFiles()?.forEach { file -> if (file.isFile) file.delete() } 111 } 112 113 // Ensure output dir is created 114 outputDirectory.mkdirs() 115 } 116 117 /** 118 * Create a benchmark output [File] to write to. 119 * 120 * This method handles reporting files to `InstrumentationStatus` to request copy, writing them 121 * in the desired output directory, and handling shell access issues on Android R. 122 * 123 * @return The absolute path of the output [File]. 124 */ 125 fun writeFile( 126 fileName: String, 127 reportOnRunEndOnly: Boolean = false, 128 block: (file: File) -> Unit, 129 ): String { 130 val sanitizedName = sanitizeFilename(fileName) 131 val destination = File(outputDirectory, sanitizedName) 132 133 // We override the `additionalTestOutputDir` argument. 134 // Context: b/181601156 135 val file = File(dirUsableByAppAndShell, sanitizedName) 136 block.invoke(file) 137 check(file.exists()) { "File doesn't exist!" } 138 139 if (dirUsableByAppAndShell != outputDirectory) { 140 // We need to copy files over anytime `dirUsableByAppAndShell` is different from 141 // `outputDirectory`. 142 Log.d(BenchmarkState.TAG, "Moving $file to $destination") 143 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { 144 file.moveTo(destination, overwrite = true) 145 } else { 146 file.copyTo(destination, overwrite = true) 147 file.delete() 148 } 149 } 150 151 InstrumentationResults.reportAdditionalFileToCopy( 152 key = sanitizedName, 153 absoluteFilePath = destination.absolutePath, 154 reportOnRunEndOnly = reportOnRunEndOnly 155 ) 156 return destination.absolutePath 157 } 158 159 fun sanitizeFilename(filename: String): String { 160 require(filename.length < 200) { 161 // Check length instead of sanitizing because in practice, names this long will 162 // break AGP/Studio/Desktop side tooling as well, at least on Linux. 163 // This threshold is conservative and operates on the input as, in practice, Studio 164 // tooling expands testnames into filenames a bit more than benchmark does. 165 "Filename too long (${filename.length} > 200) $filename - trim your test name, or" + 166 " parameterization string to avoid filename too long exceptions" 167 } 168 return filename.replace(sanitizerRegex, "_") 169 } 170 171 fun testOutputFile(filename: String): File { 172 return File(outputDirectory, filename) 173 } 174 175 fun dateToFileName(date: Date = Date()): String { 176 return formatter.format(date) 177 } 178 179 fun relativePathFor(path: String): String { 180 val hasOutputDirectoryPrefix = path.startsWith(outputDirectory.absolutePath) 181 val relativePath = 182 when { 183 hasOutputDirectoryPrefix -> path.removePrefix("${outputDirectory.absolutePath}/") 184 else -> path.removePrefix("${dirUsableByAppAndShell.absolutePath}/") 185 } 186 check(relativePath != path) { "$relativePath == $path" } 187 return relativePath 188 } 189 } 190