summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerService.java51
-rw-r--r--services/core/java/com/android/server/pm/ParallelPackageParser.java158
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/ParallelPackageParserTest.java82
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;
+ }
+ }
+}