blob: 29721c59364681d27a9c6fec21eb1207ee8c8009 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.os
import android.content.pm.PackageParser
import android.content.pm.PackageParserCacheHelper.ReadHelper
import android.content.pm.PackageParserCacheHelper.WriteHelper
import android.content.pm.parsing.ParsingPackageImpl
import android.content.pm.parsing.ParsingPackageRead
import android.content.pm.parsing.ParsingPackageUtils
import android.content.pm.parsing.result.ParseInput
import android.content.pm.parsing.result.ParseTypeImpl
import android.content.res.TypedArray
import android.perftests.utils.BenchmarkState
import android.perftests.utils.PerfStatusReporter
import androidx.test.filters.LargeTest
import com.android.internal.util.ConcurrentUtils
import libcore.io.IoUtils
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(Parameterized::class)
class PackageParsingPerfTest {
companion object {
private const val PARALLEL_QUEUE_CAPACITY = 10
private const val PARALLEL_MAX_THREADS = 4
private const val QUEUE_POLL_TIMEOUT_SECONDS = 5L
// TODO: Replace this with core version of SYSTEM_PARTITIONS
val FOLDERS_TO_TEST = listOf(
Environment.getRootDirectory(),
Environment.getVendorDirectory(),
Environment.getOdmDirectory(),
Environment.getOemDirectory(),
Environment.getOemDirectory(),
Environment.getSystemExtDirectory()
)
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun parameters(): Array<Params> {
val apks = FOLDERS_TO_TEST
.filter(File::exists)
.map(File::walkTopDown)
.flatMap(Sequence<File>::asIterable)
.filter { it.name.endsWith(".apk") }
return arrayOf(
Params(1, apks) { ParallelParser1(it?.let(::PackageCacher1)) },
Params(2, apks) { ParallelParser2(it?.let(::PackageCacher2)) }
)
}
data class Params(
val version: Int,
val apks: List<File>,
val cacheDirToParser: (File?) -> ParallelParser<*>
) {
// For test name formatting
override fun toString() = "v$version"
}
}
@get:Rule
var perfStatusReporter = PerfStatusReporter()
@get:Rule
var testFolder = TemporaryFolder()
@Parameterized.Parameter(0)
lateinit var params: Params
private val state: BenchmarkState get() = perfStatusReporter.benchmarkState
private val apks: List<File> get() = params.apks
@Test
fun sequentialNoCache() {
params.cacheDirToParser(null).use { parser ->
while (state.keepRunning()) {
apks.forEach { parser.parse(it) }
}
}
}
@Test
fun sequentialCached() {
params.cacheDirToParser(testFolder.newFolder()).use { parser ->
// Fill the cache
apks.forEach { parser.parse(it) }
while (state.keepRunning()) {
apks.forEach { parser.parse(it) }
}
}
}
@Test
fun parallelNoCache() {
params.cacheDirToParser(null).use { parser ->
while (state.keepRunning()) {
apks.forEach { parser.submit(it) }
repeat(apks.size) { parser.take() }
}
}
}
@Test
fun parallelCached() {
params.cacheDirToParser(testFolder.newFolder()).use { parser ->
// Fill the cache
apks.forEach { parser.parse(it) }
while (state.keepRunning()) {
apks.forEach { parser.submit(it) }
repeat(apks.size) { parser.take() }
}
}
}
abstract class ParallelParser<PackageType : Parcelable>(
private val cacher: PackageCacher<PackageType>? = null
) : AutoCloseable {
private val queue = ArrayBlockingQueue<Any>(PARALLEL_QUEUE_CAPACITY)
private val service = ConcurrentUtils.newFixedThreadPool(
PARALLEL_MAX_THREADS, "package-parsing-test",
Process.THREAD_PRIORITY_FOREGROUND)
fun submit(file: File) = service.submit { queue.put(parse(file)) }
fun take() = queue.poll(QUEUE_POLL_TIMEOUT_SECONDS, TimeUnit.SECONDS)
override fun close() {
service.shutdownNow()
}
fun parse(file: File) = cacher?.getCachedResult(file)
?: parseImpl(file).also { cacher?.cacheResult(file, it) }
protected abstract fun parseImpl(file: File): PackageType
}
class ParallelParser1(private val cacher: PackageCacher1? = null)
: ParallelParser<PackageParser.Package>(cacher) {
val parser = PackageParser().apply {
setCallback { true }
}
override fun parseImpl(file: File) = parser.parsePackage(file, 0, cacher != null)
}
class ParallelParser2(cacher: PackageCacher2? = null)
: ParallelParser<ParsingPackageRead>(cacher) {
val input = ThreadLocal.withInitial {
// For testing, just disable enforcement to avoid hooking up to compat framework
ParseTypeImpl(ParseInput.Callback { _, _, _ -> false })
}
val parser = ParsingPackageUtils(false, null, null,
object : ParsingPackageUtils.Callback {
override fun hasFeature(feature: String) = true
override fun startParsingPackage(
packageName: String,
baseCodePath: String,
codePath: String,
manifestArray: TypedArray,
isCoreApp: Boolean
) = ParsingPackageImpl(packageName, baseCodePath, codePath, manifestArray)
})
override fun parseImpl(file: File) =
parser.parsePackage(input.get()!!.reset(), file, 0).result
}
abstract class PackageCacher<PackageType : Parcelable>(private val cacheDir: File) {
fun getCachedResult(file: File): PackageType? {
val cacheFile = File(cacheDir, file.name)
if (!cacheFile.exists()) {
return null
}
val bytes = IoUtils.readFileAsByteArray(cacheFile.absolutePath)
val parcel = Parcel.obtain().apply {
unmarshall(bytes, 0, bytes.size)
setDataPosition(0)
}
ReadHelper(parcel).apply { startAndInstall() }
return fromParcel(parcel).also {
parcel.recycle()
}
}
fun cacheResult(file: File, parsed: Parcelable) {
val cacheFile = File(cacheDir, file.name)
if (cacheFile.exists()) {
if (!cacheFile.delete()) {
throw IllegalStateException("Unable to delete cache file: $cacheFile")
}
}
val cacheEntry = toCacheEntry(parsed)
return FileOutputStream(cacheFile).use { fos -> fos.write(cacheEntry) }
}
private fun toCacheEntry(pkg: Parcelable): ByteArray {
val parcel = Parcel.obtain()
val helper = WriteHelper(parcel)
pkg.writeToParcel(parcel, 0 /* flags */)
helper.finishAndUninstall()
return parcel.marshall().also {
parcel.recycle()
}
}
protected abstract fun fromParcel(parcel: Parcel): PackageType
}
/**
* Re-implementation of v1's cache, since that's gone in R+.
*/
class PackageCacher1(cacheDir: File) : PackageCacher<PackageParser.Package>(cacheDir) {
override fun fromParcel(parcel: Parcel) = PackageParser.Package(parcel)
}
/**
* Re-implementation of the server side PackageCacher, as it's inaccessible here.
*/
class PackageCacher2(cacheDir: File) : PackageCacher<ParsingPackageRead>(cacheDir) {
override fun fromParcel(parcel: Parcel) = ParsingPackageImpl(parcel)
}
}