diff options
| author | 2023-07-24 18:42:17 +0000 | |
|---|---|---|
| committer | 2023-11-15 19:56:38 +0000 | |
| commit | 99042d052a1a6a08a6cbc5263e2ae16d05c6d347 (patch) | |
| tree | 755708ce51fc7623bd82430ed6392e25a6066179 | |
| parent | 586efa0cd6411fba931f7329be20520875b16b71 (diff) | |
Pin webview to memory
Bug: 307594624
Test: dumpsys pinner verify pinned ranges updating webview and enabling
disabling flags.
Change-Id: I5fe3f03f0a8c0eb5b101d75d1bf1449b54050519
| -rw-r--r-- | core/res/OWNERS | 4 | ||||
| -rw-r--r-- | core/res/res/values/config.xml | 3 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 1 | ||||
| -rw-r--r-- | services/core/java/com/android/server/PinnerService.java | 315 | ||||
| -rw-r--r-- | services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java | 36 |
5 files changed, 286 insertions, 73 deletions
diff --git a/core/res/OWNERS b/core/res/OWNERS index 0df7c2047bc1..46bd3175b4be 100644 --- a/core/res/OWNERS +++ b/core/res/OWNERS @@ -45,6 +45,10 @@ per-file res/values/config_device_idle.xml = file:/apex/jobscheduler/OWNERS # Wear per-file res/*-watch/* = file:/WEAR_OWNERS +# Peformance +per-file res/values/config.xml = file:/PERFORMANCE_OWNERS +per-file res/values/symbols.xml = file:/PERFORMANCE_OWNERS + # PowerProfile per-file res/xml/power_profile.xml = file:/BATTERY_STATS_OWNERS per-file res/xml/power_profile_test.xml = file:/BATTERY_STATS_OWNERS diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index f827d28b26fd..a9d9bbf4f27e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4336,6 +4336,9 @@ <!-- True if assistant app should be pinned via Pinner Service --> <bool name="config_pinnerAssistantApp">false</bool> + <!-- Bytes that the PinnerService will pin for WebView --> + <integer name="config_pinnerWebviewPinBytes">0</integer> + <!-- Number of days preloaded file cache should be preserved on a device before it can be deleted --> <integer name="config_keepPreloadsMinDays">7</integer> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e19d5486768a..afe293b57406 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3376,6 +3376,7 @@ <java-symbol type="bool" name="config_pinnerCameraApp" /> <java-symbol type="bool" name="config_pinnerHomeApp" /> <java-symbol type="bool" name="config_pinnerAssistantApp" /> + <java-symbol type="integer" name="config_pinnerWebviewPinBytes" /> <java-symbol type="string" name="config_doubleTouchGestureEnableFile" /> diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/PinnerService.java index 8cd5ce1f4ff8..e28933b44527 100644 --- a/services/core/java/com/android/server/PinnerService.java +++ b/services/core/java/com/android/server/PinnerService.java @@ -20,6 +20,8 @@ import static android.app.ActivityManager.UID_OBSERVER_ACTIVE; import static android.app.ActivityManager.UID_OBSERVER_GONE; import static android.os.Process.SYSTEM_UID; +import static com.android.server.flags.Flags.pinWebview; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,6 +50,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemProperties; +import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; @@ -83,6 +86,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -110,11 +114,8 @@ public final class PinnerService extends SystemService { private static final int KEY_ASSISTANT = 2; // Pin using pinlist.meta when pinning apps. - private static boolean PROP_PIN_PINLIST = SystemProperties.getBoolean( - "pinner.use_pinlist", true); - // Pin the whole odex/vdex/etc file when pinning apps. - private static boolean PROP_PIN_ODEX = SystemProperties.getBoolean( - "pinner.whole_odex", true); + private static boolean PROP_PIN_PINLIST = + SystemProperties.getBoolean("pinner.use_pinlist", true); private static final int MAX_CAMERA_PIN_SIZE = 80 * (1 << 20); // 80MB max for camera app. private static final int MAX_HOME_PIN_SIZE = 6 * (1 << 20); // 6MB max for home app. @@ -134,8 +135,7 @@ public final class PinnerService extends SystemService { private SearchManager mSearchManager; /** The list of the statically pinned files. */ - @GuardedBy("this") - private final ArrayList<PinnedFile> mPinnedFiles = new ArrayList<>(); + @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>(); /** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */ @GuardedBy("this") @@ -170,6 +170,7 @@ public final class PinnerService extends SystemService { private final boolean mConfiguredToPinCamera; private final boolean mConfiguredToPinHome; private final boolean mConfiguredToPinAssistant; + private final int mConfiguredWebviewPinBytes; private BinderService mBinderService; private PinnerHandler mPinnerHandler = null; @@ -228,6 +229,8 @@ public final class PinnerService extends SystemService { com.android.internal.R.bool.config_pinnerHomeApp); mConfiguredToPinAssistant = context.getResources().getBoolean( com.android.internal.R.bool.config_pinnerAssistantApp); + mConfiguredWebviewPinBytes = context.getResources().getInteger( + com.android.internal.R.integer.config_pinnerWebviewPinBytes); mPinKeys = createPinKeys(); mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper()); @@ -317,7 +320,7 @@ public final class PinnerService extends SystemService { public List<PinnedFileStats> dumpDataForStatsd() { List<PinnedFileStats> pinnedFileStats = new ArrayList<>(); synchronized (PinnerService.this) { - for (PinnedFile pinnedFile : mPinnedFiles) { + for (PinnedFile pinnedFile : mPinnedFiles.values()) { pinnedFileStats.add(new PinnedFileStats(SYSTEM_UID, pinnedFile)); } @@ -353,39 +356,17 @@ public final class PinnerService extends SystemService { com.android.internal.R.array.config_defaultPinnerServiceFiles); // Continue trying to pin each file even if we fail to pin some of them for (String fileToPin : filesToPin) { - PinnedFile pf = pinFile(fileToPin, - Integer.MAX_VALUE, - /*attemptPinIntrospection=*/false); + PinnedFile pf = pinFileInternal(fileToPin, Integer.MAX_VALUE, + /*attemptPinIntrospection=*/false); if (pf == null) { Slog.e(TAG, "Failed to pin file = " + fileToPin); continue; } synchronized (this) { - mPinnedFiles.add(pf); - } - if (fileToPin.endsWith(".jar") | fileToPin.endsWith(".apk")) { - // Check whether the runtime has compilation artifacts to pin. - String arch = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]); - String[] files = null; - try { - files = DexFile.getDexFileOutputPaths(fileToPin, arch); - } catch (IOException ioe) { } - if (files == null) { - continue; - } - for (String file : files) { - PinnedFile df = pinFile(file, - Integer.MAX_VALUE, - /*attemptPinIntrospection=*/false); - if (df == null) { - Slog.i(TAG, "Failed to pin ART file = " + file); - continue; - } - synchronized (this) { - mPinnedFiles.add(df); - } - } + mPinnedFiles.put(pf.fileName, pf); } + pf.groupName = "xml-config"; + pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null); } refreshPinAnonConfig(); @@ -482,7 +463,7 @@ public final class PinnerService extends SystemService { pinnedAppFiles = new ArrayList<>(app.mFiles); } for (PinnedFile pinnedFile : pinnedAppFiles) { - pinnedFile.close(); + unpinFile(pinnedFile.fileName); } } @@ -490,6 +471,19 @@ public final class PinnerService extends SystemService { return ResolverActivity.class.getName().equals(info.name); } + public int getWebviewPinQuota() { + if (!pinWebview()) { + return 0; + } + int quota = mConfiguredWebviewPinBytes; + int overrideQuota = SystemProperties.getInt("pinner.pin_webview_size", -1); + if (overrideQuota != -1) { + // Quota was overridden + quota = overrideQuota; + } + return quota; + } + private ApplicationInfo getCameraInfo(int userHandle) { Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle, @@ -863,7 +857,7 @@ public final class PinnerService extends SystemService { continue; } - PinnedFile pf = pinFile(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true); + PinnedFile pf = pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true); if (pf == null) { Slog.e(TAG, "Failed to pin " + apk); continue; @@ -877,40 +871,121 @@ public final class PinnerService extends SystemService { } apkPinSizeLimit -= pf.bytesPinned; + if (apk.equals(appInfo.sourceDir)) { + pinOptimizedDexDependencies(pf, apkPinSizeLimit, appInfo); + for (PinnedFile dep : pf.pinnedDeps) { + pinnedApp.mFiles.add(dep); + } + } } + } - // determine the ABI from either ApplicationInfo or Build - String abi = appInfo.primaryCpuAbi != null ? appInfo.primaryCpuAbi : - Build.SUPPORTED_ABIS[0]; - String arch = VMRuntime.getInstructionSet(abi); - // get the path to the odex or oat file - String baseCodePath = appInfo.getBaseCodePath(); - String[] files = null; - try { - files = DexFile.getDexFileOutputPaths(baseCodePath, arch); - } catch (IOException ioe) {} - if (files == null) { - return; + /** + * Pin file or apk to memory. + * + * Prefer to use this method instead of {@link #pinFileInternal(String, int, boolean)} as it + * takes care of accounting and if pinning an apk, it also pins any extra optimized art files + * that related to the file but not within itself. + * + * @param fileToPin File to pin + * @param maxBytesToPin maximum quota allowed for pinning + * @return total bytes that were pinned. + */ + public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo, + @Nullable String groupName) { + PinnedFile existingPin; + synchronized(this) { + existingPin = mPinnedFiles.get(fileToPin); + } + if (existingPin != null) { + if (existingPin.bytesPinned == maxBytesToPin) { + // Duplicate pin requesting same amount of bytes, lets just bail out. + return 0; + } else { + // User decided to pin a different amount of bytes than currently pinned + // so this is a valid pin request. Unpin the previous version before repining. + if (DEBUG) { + Slog.d(TAG, "Unpinning file prior to repin: " + fileToPin); + } + unpinFile(fileToPin); + } } - //not pinning the oat/odex is not a fatal error - for (String file : files) { - PinnedFile pf = pinFile(file, pinSizeLimit, /*attemptPinIntrospection=*/false); - if (pf != null) { - synchronized (this) { - if (PROP_PIN_ODEX) { - pinnedApp.mFiles.add(pf); - } + boolean isApk = fileToPin.endsWith(".apk"); + int bytesPinned = 0; + PinnedFile pf = pinFileInternal(fileToPin, maxBytesToPin, + /*attemptPinIntrospection=*/isApk); + if (pf == null) { + Slog.e(TAG, "Failed to pin file = " + fileToPin); + return 0; + } + pf.groupName = groupName != null ? groupName : ""; + + maxBytesToPin -= bytesPinned; + bytesPinned += pf.bytesPinned; + + synchronized (this) { + mPinnedFiles.put(pf.fileName, pf); + } + if (maxBytesToPin > 0) { + pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo); + } + return bytesPinned; + } + + /** + * Pin any dependency optimized files generated by ART. + * @param pinnedFile An already pinned file whose dependencies we want pinned. + * @param maxBytesToPin Maximum amount of bytes to pin. + * @param appInfo Used to determine the ABI in case the application has one custom set, when set + * to null it will use the default supported ABI by the device. + * @return total bytes pinned. + */ + private int pinOptimizedDexDependencies( + PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) { + if (pinnedFile == null) { + return 0; + } + + int bytesPinned = 0; + if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) { + String abi = null; + if (appInfo != null) { + abi = appInfo.primaryCpuAbi; + } + if (abi == null) { + abi = Build.SUPPORTED_ABIS[0]; + } + // Check whether the runtime has compilation artifacts to pin. + String arch = VMRuntime.getInstructionSet(abi); + String[] files = null; + try { + files = DexFile.getDexFileOutputPaths(pinnedFile.fileName, arch); + } catch (IOException ioe) { + } + if (files == null) { + return bytesPinned; + } + for (String file : files) { + // Unpin if it was already pinned prior to re-pinning. + unpinFile(file); + + PinnedFile df = pinFileInternal(file, Integer.MAX_VALUE, + /*attemptPinIntrospection=*/false); + if (df == null) { + Slog.i(TAG, "Failed to pin ART file = " + file); + return bytesPinned; } - if (DEBUG) { - if (PROP_PIN_ODEX) { - Slog.i(TAG, "Pinned " + pf.fileName); - } else { - Slog.i(TAG, "Pinned [skip] " + pf.fileName); - } + df.groupName = pinnedFile.groupName; + pinnedFile.pinnedDeps.add(df); + maxBytesToPin -= df.bytesPinned; + bytesPinned += df.bytesPinned; + synchronized (this) { + mPinnedFiles.put(df.fileName, df); } } } + return bytesPinned; } /** mlock length bytes of fileToPin in memory @@ -950,9 +1025,12 @@ public final class PinnerService extends SystemService { * zip in order to extract the * @return Pinned memory resource owner thing or null on error */ - private static PinnedFile pinFile(String fileToPin, - int maxBytesToPin, - boolean attemptPinIntrospection) { + private static PinnedFile pinFileInternal( + String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) { + if (DEBUG) { + Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection); + } + Trace.beginSection("pinFile:" + fileToPin); ZipFile fileAsZip = null; InputStream pinRangeStream = null; try { @@ -964,8 +1042,6 @@ public final class PinnerService extends SystemService { pinRangeStream = maybeOpenPinMetaInZip(fileAsZip, fileToPin); } - Slog.d(TAG, "pinRangeStream: " + pinRangeStream); - PinRangeSource pinRangeSource = (pinRangeStream != null) ? new PinRangeSourceStream(pinRangeStream) : new PinRangeSourceStatic(0, Integer.MAX_VALUE /* will be clipped */); @@ -973,6 +1049,7 @@ public final class PinnerService extends SystemService { } finally { safeClose(pinRangeStream); safeClose(fileAsZip); // Also closes any streams we've opened + Trace.endSection(); } } @@ -1008,9 +1085,23 @@ public final class PinnerService extends SystemService { return null; } + // Looking at root directory is the old behavior but still some apps rely on it so keeping + // for backward compatibility. As doing a single item lookup is cheap in the root. ZipEntry pinMetaEntry = zipFile.getEntry(PIN_META_FILENAME); + + if (pinMetaEntry == null) { + // It is usually within an apk's control to include files in assets/ directory + // so this would be the expected point to have the pinlist.meta coming from. + // we explicitly avoid doing an exhaustive search because it may be expensive so + // prefer to have a good known location to retrieve the file. + pinMetaEntry = zipFile.getEntry("assets/" + PIN_META_FILENAME); + } + InputStream pinMetaStream = null; if (pinMetaEntry != null) { + if (DEBUG) { + Slog.d(TAG, "Found pinlist.meta for " + fileName); + } try { pinMetaStream = zipFile.getInputStream(pinMetaEntry); } catch (IOException ex) { @@ -1019,6 +1110,10 @@ public final class PinnerService extends SystemService { fileName), ex); } + } else { + Slog.w(TAG, + String.format( + "Could not find pinlist.meta for \"%s\": pinning as blob", fileName)); } return pinMetaStream; } @@ -1155,6 +1250,49 @@ public final class PinnerService extends SystemService { } } } + private List<PinnedFile> getAllPinsForGroup(String group) { + List<PinnedFile> filesInGroup; + synchronized (this) { + filesInGroup = mPinnedFiles.values() + .stream() + .filter(pf -> pf.groupName.equals(group)) + .toList(); + } + return filesInGroup; + } + public void unpinGroup(String group) { + List<PinnedFile> pinnedFiles = getAllPinsForGroup(group); + for (PinnedFile pf : pinnedFiles) { + unpinFile(pf.fileName); + } + } + + public void unpinFile(String filename) { + PinnedFile pinnedFile; + synchronized (this) { + pinnedFile = mPinnedFiles.get(filename); + } + if (pinnedFile == null) { + // File not pinned, nothing to do. + return; + } + pinnedFile.close(); + synchronized (this) { + if (DEBUG) { + Slog.d(TAG, "Unpinned file: " + filename); + } + mPinnedFiles.remove(pinnedFile.fileName); + for (PinnedFile dep : pinnedFile.pinnedDeps) { + if (dep == null) { + continue; + } + mPinnedFiles.remove(dep.fileName); + if (DEBUG) { + Slog.d(TAG, "Unpinned dependency: " + dep.fileName); + } + } + } + } private static int clamp(int min, int value, int max) { return Math.max(min, Math.min(value, max)); @@ -1204,13 +1342,11 @@ public final class PinnerService extends SystemService { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + HashSet<PinnedFile> shownPins = new HashSet<>(); + HashSet<String> groups = new HashSet<>(); + synchronized (PinnerService.this) { long totalSize = 0; - for (PinnedFile pinnedFile : mPinnedFiles) { - pw.format("%s %s\n", pinnedFile.fileName, pinnedFile.bytesPinned); - totalSize += pinnedFile.bytesPinned; - } - pw.println(); for (int key : mPinnedApps.keySet()) { PinnedApp app = mPinnedApps.get(key); pw.print(getNameForKey(key)); @@ -1220,8 +1356,32 @@ public final class PinnerService extends SystemService { for (PinnedFile pf : mPinnedApps.get(key).mFiles) { pw.print(" "); pw.format("%s %s\n", pf.fileName, pf.bytesPinned); totalSize += pf.bytesPinned; + shownPins.add(pf); } } + pw.println(); + for (PinnedFile pinnedFile : mPinnedFiles.values()) { + if (!groups.contains(pinnedFile.groupName)) { + groups.add(pinnedFile.groupName); + } + } + for (String group : groups) { + pw.print("Group:" + group); + pw.println(); + List<PinnedFile> groupPins = mPinnedFiles.values() + .stream() + .filter(f -> f.groupName.equals(group)) + .toList(); + for (PinnedFile pinnedFile : groupPins) { + if (shownPins.contains(pinnedFile)) { + // Already showed in the dump and accounted for, skip. + continue; + } + pw.format(" %s %s\n", pinnedFile.fileName, pinnedFile.bytesPinned); + totalSize += pinnedFile.bytesPinned; + } + } + pw.println(); if (mPinAnonAddress != 0) { pw.format("Pinned anon region: %s\n", mCurrentlyPinnedAnonSize); } @@ -1280,6 +1440,10 @@ public final class PinnerService extends SystemService { final String fileName; final int bytesPinned; + // User defined group name for pinner accounting + String groupName = ""; + ArrayList<PinnedFile> pinnedDeps = new ArrayList<>(); + PinnedFile(long address, int mapSize, String fileName, int bytesPinned) { mAddress = address; this.mapSize = mapSize; @@ -1293,6 +1457,11 @@ public final class PinnerService extends SystemService { safeMunmap(mAddress, mapSize); mAddress = -1; } + for (PinnedFile dep : pinnedDeps) { + if (dep != null) { + dep.close(); + } + } } @Override diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index 43d62aaa120a..cc2b9998ae33 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -17,6 +17,7 @@ package com.android.server.webkit; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -29,8 +30,12 @@ import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; +import com.android.server.LocalServices; +import com.android.server.PinnerService; + import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -88,6 +93,8 @@ class WebViewUpdateServiceImpl { private static final int MULTIPROCESS_SETTING_ON_VALUE = Integer.MAX_VALUE; private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE; + private static final String PIN_GROUP = "webview"; + private final SystemInterface mSystemInterface; private final Context mContext; @@ -332,6 +339,34 @@ class WebViewUpdateServiceImpl { return newPackage; } + private void pinWebviewIfRequired(ApplicationInfo appInfo) { + PinnerService pinnerService = LocalServices.getService(PinnerService.class); + int webviewPinQuota = pinnerService.getWebviewPinQuota(); + if (webviewPinQuota <= 0) { + return; + } + + pinnerService.unpinGroup(PIN_GROUP); + + ArrayList<String> apksToPin = new ArrayList<>(); + boolean pinSharedFirst = appInfo.metaData.getBoolean("PIN_SHARED_LIBS_FIRST", true); + for (String sharedLib : appInfo.sharedLibraryFiles) { + apksToPin.add(sharedLib); + } + apksToPin.add(appInfo.sourceDir); + if (!pinSharedFirst) { + // We want to prioritize pinning of the native library that is most likely used by apps + // which in some build flavors live in the main apk and as a shared library for others. + Collections.reverse(apksToPin); + } + for (String apk : apksToPin) { + if (webviewPinQuota <= 0) { + break; + } + int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP); + webviewPinQuota -= bytesPinned; + } + } /** * This is called when we change WebView provider, either when the current provider is * updated or a new provider is chosen / takes precedence. @@ -340,6 +375,7 @@ class WebViewUpdateServiceImpl { synchronized (mLock) { mAnyWebViewInstalled = true; if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) { + pinWebviewIfRequired(newPackage.applicationInfo); mCurrentWebViewPackage = newPackage; // The relro creations might 'finish' (not start at all) before |