diff options
3 files changed, 279 insertions, 12 deletions
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index bc2814270865..c1bc6182a86b 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -6670,7 +6670,7 @@ public class PackageManagerService extends IPackageManager.Stub { } } - private void scanDirLI(File dir, final int parseFlags, int scanFlags, long currentTime) { + private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) { final File[] files = dir.listFiles(); if (ArrayUtils.isEmpty(files)) { Log.d(TAG, "No files in app dir " + dir); @@ -6681,7 +6681,11 @@ public class PackageManagerService extends IPackageManager.Stub { Log.d(TAG, "Scanning app dir " + dir + " scanFlags=" + scanFlags + " flags=0x" + Integer.toHexString(parseFlags)); } + ParallelPackageParser parallelPackageParser = new ParallelPackageParser( + mSeparateProcesses, mOnlyCore, mMetrics); + // Submit files for parsing in parallel + int fileCount = 0; for (File file : files) { final boolean isPackage = (isApkFile(file) || file.isDirectory()) && !PackageInstallerService.isStageName(file.getName()); @@ -6689,20 +6693,43 @@ public class PackageManagerService extends IPackageManager.Stub { // Ignore entries which are not packages continue; } - try { - scanPackageTracedLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK, - scanFlags, currentTime, null); - } catch (PackageManagerException e) { - Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage()); + parallelPackageParser.submit(file, parseFlags); + fileCount++; + } - // Delete invalid userdata apps - if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 && - e.error == PackageManager.INSTALL_FAILED_INVALID_APK) { - logCriticalInfo(Log.WARN, "Deleting invalid package at " + file); - removeCodePathLI(file); - } + // Process results one by one + for (; fileCount > 0; fileCount--) { + ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take(); + Throwable throwable = parseResult.throwable; + int errorCode = PackageManager.INSTALL_SUCCEEDED; + + if (throwable == null) { + try { + scanPackageLI(parseResult.pkg, parseResult.scanFile, parseFlags, scanFlags, + currentTime, null); + } catch (PackageManagerException e) { + errorCode = e.error; + Slog.w(TAG, "Failed to scan " + parseResult.scanFile + ": " + e.getMessage()); + } + } else if (throwable instanceof PackageParser.PackageParserException) { + PackageParser.PackageParserException e = (PackageParser.PackageParserException) + throwable; + errorCode = e.error; + Slog.w(TAG, "Failed to parse " + parseResult.scanFile + ": " + e.getMessage()); + } else { + throw new IllegalStateException("Unexpected exception occurred while parsing " + + parseResult.scanFile, throwable); + } + + // Delete invalid userdata apps + if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 && + errorCode == PackageManager.INSTALL_FAILED_INVALID_APK) { + logCriticalInfo(Log.WARN, + "Deleting invalid package at " + parseResult.scanFile); + removeCodePathLI(parseResult.scanFile); } } + parallelPackageParser.close(); } private static File getSettingsProblemFile() { diff --git a/services/core/java/com/android/server/pm/ParallelPackageParser.java b/services/core/java/com/android/server/pm/ParallelPackageParser.java new file mode 100644 index 000000000000..158cfc946b73 --- /dev/null +++ b/services/core/java/com/android/server/pm/ParallelPackageParser.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 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 com.android.server.pm; + +import android.content.pm.PackageParser; +import android.os.Process; +import android.os.Trace; +import android.util.DisplayMetrics; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; + +/** + * Helper class for parallel parsing of packages using {@link PackageParser}. + * <p>Parsing requests are processed by a thread-pool of {@link #MAX_THREADS}. + * At any time, at most {@link #QUEUE_CAPACITY} results are kept in RAM</p> + */ +class ParallelPackageParser implements AutoCloseable { + + private static final int QUEUE_CAPACITY = 10; + private static final int MAX_THREADS = 4; + + private final String[] mSeparateProcesses; + private final boolean mOnlyCore; + private final DisplayMetrics mMetrics; + private volatile String mInterruptedInThread; + + private final BlockingQueue<ParseResult> mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + private final ExecutorService mService = Executors.newFixedThreadPool(MAX_THREADS, + new ThreadFactory() { + private final AtomicInteger threadNum = new AtomicInteger(0); + + @Override + public Thread newThread(final Runnable r) { + return new Thread("package-parsing-thread" + threadNum.incrementAndGet()) { + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); + r.run(); + } + }; + } + }); + + ParallelPackageParser(String[] separateProcesses, boolean onlyCoreApps, + DisplayMetrics metrics) { + mSeparateProcesses = separateProcesses; + mOnlyCore = onlyCoreApps; + mMetrics = metrics; + } + + static class ParseResult { + + PackageParser.Package pkg; // Parsed package + File scanFile; // File that was parsed + Throwable throwable; // Set if an error occurs during parsing + + @Override + public String toString() { + return "ParseResult{" + + "pkg=" + pkg + + ", scanFile=" + scanFile + + ", throwable=" + throwable + + '}'; + } + } + + /** + * Take the parsed package from the parsing queue, waiting if necessary until the element + * appears in the queue. + * @return parsed package + */ + public ParseResult take() { + try { + if (mInterruptedInThread != null) { + throw new InterruptedException("Interrupted in " + mInterruptedInThread); + } + return mQueue.take(); + } catch (InterruptedException e) { + // We cannot recover from interrupt here + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + /** + * Submits the file for parsing + * @param scanFile file to scan + * @param parseFlags parse falgs + */ + public void submit(File scanFile, int parseFlags) { + mService.submit(() -> { + ParseResult pr = new ParseResult(); + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]"); + try { + PackageParser pp = new PackageParser(); + pp.setSeparateProcesses(mSeparateProcesses); + pp.setOnlyCoreApps(mOnlyCore); + pp.setDisplayMetrics(mMetrics); + pr.scanFile = scanFile; + pr.pkg = parsePackage(pp, scanFile, parseFlags); + } catch (Throwable e) { + pr.throwable = e; + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + try { + mQueue.put(pr); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // Propagate result to callers of take(). + // This is helpful to prevent main thread from getting stuck waiting on + // ParallelPackageParser to finish in case of interruption + mInterruptedInThread = Thread.currentThread().getName(); + } + }); + } + + @VisibleForTesting + protected PackageParser.Package parsePackage(PackageParser packageParser, File scanFile, + int parseFlags) throws PackageParser.PackageParserException { + return packageParser.parsePackage(scanFile, parseFlags); + } + + @Override + public void close() { + List<Runnable> unfinishedTasks = mService.shutdownNow(); + if (!unfinishedTasks.isEmpty()) { + throw new IllegalStateException("Not all tasks finished before calling close: " + + unfinishedTasks); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/ParallelPackageParserTest.java b/services/tests/servicestests/src/com/android/server/pm/ParallelPackageParserTest.java new file mode 100644 index 000000000000..d165b8b2685c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ParallelPackageParserTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 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 com.android.server.pm; + +import android.content.pm.PackageParser; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +/** + * Tests for {@link ParallelPackageParser} + */ +@RunWith(AndroidJUnit4.class) +public class ParallelPackageParserTest { + private static final String TAG = ParallelPackageParserTest.class.getSimpleName(); + + private ParallelPackageParser mParser; + + @Before + public void setUp() { + mParser = new TestParallelPackageParser(); + } + + @Test(timeout = 1000) + public void test() { + Set<File> submittedFiles = new HashSet<>(); + int fileCount = 15; + for (int i = 0; i < fileCount; i++) { + File file = new File("f" + i); + mParser.submit(file, 0); + submittedFiles.add(file); + Log.d(TAG, "submitting " + file); + } + for (int i = 0; i < fileCount; i++) { + ParallelPackageParser.ParseResult result = mParser.take(); + Assert.assertNotNull(result); + File parsedFile = result.scanFile; + Log.d(TAG, "took " + parsedFile); + Assert.assertNotNull(parsedFile); + boolean removeSuccessful = submittedFiles.remove(parsedFile); + Assert.assertTrue("Unexpected file " + parsedFile + ". Expected submitted files: " + + submittedFiles, removeSuccessful); + } + } + + class TestParallelPackageParser extends ParallelPackageParser { + + TestParallelPackageParser() { + super(null, false, null); + } + + @Override + protected PackageParser.Package parsePackage(PackageParser packageParser, File scanFile, + int parseFlags) throws PackageParser.PackageParserException { + // Do not actually parse the package for testing + return null; + } + } +} |