1 /*
2  * Copyright (C) 2022 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 android.os
18 
19 import android.content.pm.PackageParser
20 import android.content.pm.PackageParserCacheHelper.ReadHelper
21 import android.content.pm.PackageParserCacheHelper.WriteHelper
22 import android.content.pm.parsing.result.ParseInput
23 import android.content.pm.parsing.result.ParseTypeImpl
24 import android.content.res.TypedArray
25 import android.perftests.utils.BenchmarkState
26 import android.perftests.utils.PerfStatusReporter
27 import androidx.test.filters.LargeTest
28 import com.android.internal.util.ConcurrentUtils
29 import com.android.server.pm.parsing.pkg.PackageImpl
30 import com.android.server.pm.pkg.parsing.ParsingPackageUtils
31 import java.io.File
32 import java.io.FileOutputStream
33 import java.util.concurrent.ArrayBlockingQueue
34 import java.util.concurrent.TimeUnit
35 import libcore.io.IoUtils
36 import org.junit.Rule
37 import org.junit.Test
38 import org.junit.rules.TemporaryFolder
39 import org.junit.runner.RunWith
40 import org.junit.runners.Parameterized
41 
42 @LargeTest
43 @RunWith(Parameterized::class)
44 public class PackageParsingPerfTest {
45 
46     companion object {
47         private const val PARALLEL_QUEUE_CAPACITY = 10
48         private const val PARALLEL_MAX_THREADS = 4
49 
50         private const val QUEUE_POLL_TIMEOUT_SECONDS = 5L
51 
52         // TODO: Replace this with core version of SYSTEM_PARTITIONS
53         val FOLDERS_TO_TEST = listOf(
54             Environment.getRootDirectory(),
55             Environment.getVendorDirectory(),
56             Environment.getOdmDirectory(),
57             Environment.getOemDirectory(),
58             Environment.getOemDirectory(),
59             Environment.getSystemExtDirectory()
60         )
61 
62         @JvmStatic
63         @Parameterized.Parameters(name = "{0}")
64         fun parameters(): Array<Params> {
65             val apks = FOLDERS_TO_TEST
66                 .filter(File::exists)
67                 .map(File::walkTopDown)
68                 .flatMap(Sequence<File>::asIterable)
69                 .filter { it.name.endsWith(".apk") }
70 
71             return arrayOf(
72                 Params(1, apks) { ParallelParser1(it?.let(::PackageCacher1)) },
73                 Params(2, apks) { ParallelParser2(it?.let(::PackageCacher2)) }
74             )
75         }
76 
77         data class Params(
78             val version: Int,
79             val apks: List<File>,
80             val cacheDirToParser: (File?) -> ParallelParser<*>
81         ) {
82             // For test name formatting
83             override fun toString() = "v$version"
84         }
85     }
86 
87     @get:Rule
88     var perfStatusReporter = PerfStatusReporter()
89 
90     @get:Rule
91     var testFolder = TemporaryFolder()
92 
93     @Parameterized.Parameter(0)
94     lateinit var params: Params
95 
96     private val state: BenchmarkState get() = perfStatusReporter.benchmarkState
97     private val apks: List<File> get() = params.apks
98 
99     private fun safeParse(parser: ParallelParser<*>, file: File) {
100         try {
101             parser.parse(file)
102         } catch (e: Exception) {
103             // ignore
104         }
105     }
106 
107     @Test
108     fun sequentialNoCache() {
109         params.cacheDirToParser(null).use { parser ->
110             while (state.keepRunning()) {
111                 apks.forEach {
112                     safeParse(parser, it)
113                 }
114             }
115         }
116     }
117 
118     @Test
119     fun sequentialCached() {
120         params.cacheDirToParser(testFolder.newFolder()).use { parser ->
121             // Fill the cache
122             apks.forEach { safeParse(parser, it) }
123 
124             while (state.keepRunning()) {
125                 apks.forEach { safeParse(parser, it) }
126             }
127         }
128     }
129 
130     @Test
131     fun parallelNoCache() {
132         params.cacheDirToParser(null).use { parser ->
133             while (state.keepRunning()) {
134                 apks.forEach { parser.submit(it) }
135                 repeat(apks.size) { parser.take() }
136             }
137         }
138     }
139 
140     @Test
141     fun parallelCached() {
142         params.cacheDirToParser(testFolder.newFolder()).use { parser ->
143             // Fill the cache
144             apks.forEach { safeParse(parser, it) }
145 
146             while (state.keepRunning()) {
147                 apks.forEach { parser.submit(it) }
148                 repeat(apks.size) { parser.take() }
149             }
150         }
151     }
152 
153     abstract class ParallelParser<PackageType : Parcelable>(
154         private val cacher: PackageCacher<PackageType>? = null
155     ) : AutoCloseable {
156         private val queue = ArrayBlockingQueue<Any>(PARALLEL_QUEUE_CAPACITY)
157         private val service = ConcurrentUtils.newFixedThreadPool(
158             PARALLEL_MAX_THREADS, "package-parsing-test",
159             Process.THREAD_PRIORITY_FOREGROUND)
160 
161         fun submit(file: File) {
162                 service.submit {
163                     try {
164                         queue.put(parse(file))
165                     } catch (e: Exception) {
166                         queue.put(e)
167                     }
168                 }
169         }
170 
171         fun take() = queue.poll(QUEUE_POLL_TIMEOUT_SECONDS, TimeUnit.SECONDS)
172 
173         override fun close() {
174             service.shutdownNow()
175         }
176 
177         fun parse(file: File) = cacher?.getCachedResult(file)
178             ?: parseImpl(file).also { cacher?.cacheResult(file, it) }
179 
180         protected abstract fun parseImpl(file: File): PackageType
181     }
182 
183     class ParallelParser1(private val cacher: PackageCacher1? = null) :
184         ParallelParser<PackageParser.Package>(cacher) {
185         val parser = PackageParser().apply {
186             setCallback { true }
187         }
188 
189         override fun parseImpl(file: File) = parser.parsePackage(file, 0, cacher != null)
190     }
191 
192     class ParallelParser2(cacher: PackageCacher2? = null) :
193         ParallelParser<PackageImpl>(cacher) {
194         val input = ThreadLocal.withInitial {
195             // For testing, just disable enforcement to avoid hooking up to compat framework
196             ParseTypeImpl(ParseInput.Callback { _, _, _ -> false })
197         }
198         val parser = ParsingPackageUtils(null,
199             null,
200             emptyList(),
201             object :
202                 ParsingPackageUtils.Callback {
203                 override fun hasFeature(feature: String) = true
204 
205                 override fun startParsingPackage(
206                     packageName: String,
207                     baseApkPath: String,
208                     path: String,
209                     manifestArray: TypedArray,
210                     isCoreApp: Boolean
211                 ) = PackageImpl(
212                     packageName,
213                     baseApkPath,
214                     path,
215                     manifestArray,
216                     isCoreApp,
217                 )
218             })
219 
220         override fun parseImpl(file: File) =
221                 parser.parsePackage(input.get()!!.reset(), file, 0).result
222                         as PackageImpl
223     }
224 
225     abstract class PackageCacher<PackageType : Parcelable>(private val cacheDir: File) {
226 
227         fun getCachedResult(file: File): PackageType? {
228             val cacheFile = File(cacheDir, file.name)
229             if (!cacheFile.exists()) {
230                 return null
231             }
232 
233             val bytes = IoUtils.readFileAsByteArray(cacheFile.absolutePath)
234             val parcel = Parcel.obtain().apply {
235                 unmarshall(bytes, 0, bytes.size)
236                 setDataPosition(0)
237             }
238             ReadHelper(parcel).apply { startAndInstall() }
239             return fromParcel(parcel).also {
240                 parcel.recycle()
241             }
242         }
243 
244         fun cacheResult(file: File, parsed: Parcelable) {
245             val cacheFile = File(cacheDir, file.name)
246             if (cacheFile.exists()) {
247                 if (!cacheFile.delete()) {
248                     throw IllegalStateException("Unable to delete cache file: $cacheFile")
249                 }
250             }
251             val cacheEntry = toCacheEntry(parsed)
252             return FileOutputStream(cacheFile).use { fos -> fos.write(cacheEntry) }
253         }
254 
255         private fun toCacheEntry(pkg: Parcelable): ByteArray {
256             val parcel = Parcel.obtain()
257             val helper = WriteHelper(parcel)
258             pkg.writeToParcel(parcel, 0 /* flags */)
259             helper.finishAndUninstall()
260             return parcel.marshall().also {
261                 parcel.recycle()
262             }
263         }
264 
265         protected abstract fun fromParcel(parcel: Parcel): PackageType
266     }
267 
268     /**
269      * Re-implementation of v1's cache, since that's gone in R+.
270      */
271     class PackageCacher1(cacheDir: File) : PackageCacher<PackageParser.Package>(cacheDir) {
272         override fun fromParcel(parcel: Parcel) = PackageParser.Package(parcel)
273     }
274 
275     /**
276      * Re-implementation of the server side PackageCacher, as it's inaccessible here.
277      */
278     class PackageCacher2(cacheDir: File) : PackageCacher<PackageImpl>(cacheDir) {
279         override fun fromParcel(parcel: Parcel) =
280             PackageImpl(parcel)
281     }
282 }
283