Commit 24f7b4ba authored by Andrew Grieve's avatar Andrew Grieve Committed by Chromium LUCI CQ

Android: Add work-around for renderers not being able to access odex

This causes trichrome's dex to be compile with "speed" rather than
"speed-profile", which is a substantial binary size regression.

Also adds a histogram to track when the work-around happens.

Bug: 1152970, 1159608
Change-Id: I25e2c350b54724613234b23505078f5df88d9cf4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2602657
Commit-Queue: Andrew Grieve <agrieve@chromium.org>
Reviewed-by: default avatarClark DuVall <cduvall@chromium.org>
Reviewed-by: default avatarSteven Holte <holte@chromium.org>
Cr-Commit-Position: refs/heads/master@{#840328}
parent 16059813
......@@ -3989,6 +3989,13 @@ if (is_android) {
"test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java",
"test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java",
]
# Make sure robolectric tests have classes filtered out of base_java by
# jar_excluded_patterns.
srcjar_deps = [
":base_build_config_gen",
":base_native_libraries_gen",
]
deps = [
":base_java",
"//testing/android/junit:junit_test_support",
......
......@@ -2152,6 +2152,8 @@ android_library("base_module_java") {
"java/src/org/chromium/chrome/browser/ChromeBackgroundService.java",
"java/src/org/chromium/chrome/browser/ChromeBackupAgent.java",
"java/src/org/chromium/chrome/browser/DeferredStartupHandler.java",
"java/src/org/chromium/chrome/browser/base/DexFixer.java",
"java/src/org/chromium/chrome/browser/base/DexFixerReason.java",
"java/src/org/chromium/chrome/browser/base/SplitChromeApplication.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatAppComponentFactory.java",
"java/src/org/chromium/chrome/browser/base/SplitCompatApplication.java",
......
......@@ -16,6 +16,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/background_sync/BackgroundSyncGooglePlayServicesCheckerTest.java",
"junit/src/org/chromium/chrome/browser/background_sync/PeriodicBackgroundSyncChromeWakeUpTaskTest.java",
"junit/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTaskTest.java",
"junit/src/org/chromium/chrome/browser/base/DexFixerTest.java",
"junit/src/org/chromium/chrome/browser/base/SplitPreloaderTest.java",
"junit/src/org/chromium/chrome/browser/bookmarks/ReadingListSectionHeaderTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClearDataDialogResultRecorderTest.java",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.base;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import dalvik.system.DexFile;
import org.chromium.base.BuildConfig;
import org.chromium.base.BuildInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.compat.ApiHelperForM;
import org.chromium.base.compat.ApiHelperForO;
import org.chromium.base.library_loader.NativeLibraries;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.DeferredStartupHandler;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.version.ChromeVersionInfo;
import java.io.File;
import java.io.IOException;
@RequiresApi(Build.VERSION_CODES.O)
class DexFixer {
private static final String TAG = "DexFixer";
private static boolean sHasIsolatedSplits;
static void setHasIsolatedSplits(boolean value) {
sHasIsolatedSplits = value;
}
static void scheduleDexFix() {
ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
// All bugs are fixed after Q.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
return;
}
// Fixes are never required for system image installs, since we can trust those to be valid
// and world-readable.
if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
return;
}
// Skip the workaround on local builds to avoid affecting perf bots.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1160070
if (ChromeVersionInfo.isLocalBuild() && ChromeVersionInfo.isOfficialBuild()) {
return;
}
// Wait until startup completes so this doesn't slow down early startup or mess with
// compiled dex files before they get loaded initially.
DeferredStartupHandler.getInstance().addDeferredTask(() -> {
// BEST_EFFORT will only affect when the task runs, the dexopt will run with
// normal priority (but in a separate process, due to using Runtime.exec()).
PostTask.postTask(TaskTraits.BEST_EFFORT_MAY_BLOCK,
() -> { fixDexIfNecessary(Runtime.getRuntime()); });
});
}
@WorkerThread
@VisibleForTesting
static void fixDexIfNecessary(Runtime runtime) {
ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
@DexFixerReason
int reason = needsDexCompile(appInfo);
if (reason > DexFixerReason.NOT_NEEDED) {
Log.w(TAG, "Triggering dex compile. Reason=%d", reason);
try {
String cmd = "cmd package compile -r shared ";
if (reason == DexFixerReason.NOT_READABLE && BuildConfig.ISOLATED_SPLITS_ENABLED) {
// Isolated processes need only access the base split.
String apkBaseName = new File(appInfo.sourceDir).getName();
cmd += String.format("--split %s ", apkBaseName);
}
cmd += ContextUtils.getApplicationContext().getPackageName();
runtime.exec(cmd);
} catch (IOException e) {
reason = DexFixerReason.FAILED_TO_RUN;
}
}
RecordHistogram.recordEnumeratedHistogram("Android.DexFixer", reason, DexFixerReason.COUNT);
}
private static String odexPathFromApkPath(String apkPath) {
// Based on https://cs.android.com/search?q=OatFileAssistant::DexLocationToOdexNames
boolean is64Bit = ApiHelperForM.isProcess64Bit();
String isaName;
if (NativeLibraries.sCpuFamily == NativeLibraries.CPU_FAMILY_ARM) {
isaName = is64Bit ? "arm64" : "arm";
} else {
isaName = is64Bit ? "x86_64" : "x86";
}
// E.g. /data/app/org.chromium.chrome-qtmmjyN79ucfPKm0ZVZMHg==/base.apk
File apkFile = new File(apkPath);
String baseName = apkFile.getName();
baseName = baseName.substring(0, baseName.lastIndexOf('.'));
return String.format("%s/oat/%s/%s.odex", apkFile.getParent(), isaName, baseName);
}
private static @DexFixerReason int needsDexCompile(ApplicationInfo appInfo) {
// Android O MR1 has a bug where bg-dexopt-job will break optimized dex files for isolated
// splits. This leads to *very* slow startup on those devices. To mitigate this, we attempt
// to force a dex compile if necessary.
if (sHasIsolatedSplits && Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
// If the app has just been updated, it will be compiled with quicken. The next time
// bg-dexopt-job runs it will break the optimized dex for splits. If we force compile
// now, then bg-dexopt-job won't mess up the splits, and we save the user a slow
// startup.
SharedPreferencesManager prefManager = SharedPreferencesManager.getInstance();
long versionCode = BuildInfo.getInstance().versionCode;
if (prefManager.readLong(ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION)
!= versionCode) {
// Compiling the dex is an asynchronous operation anyways, so update the pref here
// rather than attempting to wait.
prefManager.writeLong(
ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION, versionCode);
return DexFixerReason.O_MR1_AFTER_UPDATE;
}
// Check for corrupt dex.
String[] splitNames = ApiHelperForO.getSplitNames(appInfo);
if (splitNames != null) {
for (int i = 0; i < splitNames.length; i++) {
// Ignore config splits like "config.en".
if (splitNames[i].contains(".")) {
continue;
}
try {
if (DexFile.isDexOptNeeded(appInfo.splitSourceDirs[i])) {
return DexFixerReason.O_MR1_CORRUPTED;
}
} catch (IOException e) {
return DexFixerReason.O_MR1_IO_EXCEPTION;
}
}
}
}
String oatPath = odexPathFromApkPath(appInfo.sourceDir);
try {
StructStat st = Os.stat(oatPath);
if ((st.st_mode & 0007) == 0) {
return DexFixerReason.NOT_READABLE;
}
} catch (ErrnoException e) {
return DexFixerReason.STAT_FAILED;
}
return DexFixerReason.NOT_NEEDED;
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.base;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Histogram enum to monitor DexFixer. */
@IntDef({DexFixerReason.STAT_FAILED, DexFixerReason.FAILED_TO_RUN, DexFixerReason.NOT_NEEDED,
DexFixerReason.O_MR1_AFTER_UPDATE, DexFixerReason.O_MR1_CORRUPTED,
DexFixerReason.O_MR1_IO_EXCEPTION, DexFixerReason.NOT_READABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface DexFixerReason {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
int STAT_FAILED = 0;
int FAILED_TO_RUN = 1;
int NOT_NEEDED = 2;
// Values greater than NOT_NEEDED trigger Dexopt.
int O_MR1_AFTER_UPDATE = 5;
int O_MR1_CORRUPTED = 6;
int O_MR1_IO_EXCEPTION = 7;
int NOT_READABLE = 8;
int COUNT = 9;
}
......@@ -14,22 +14,11 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.os.SystemClock;
import dalvik.system.DexFile;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.JNIUtils;
import org.chromium.base.Log;
import org.chromium.base.PackageUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.compat.ApiHelperForO;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.DeferredStartupHandler;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.version.ChromeVersionInfo;
import java.lang.reflect.Field;
......@@ -68,13 +57,15 @@ public class SplitChromeApplication extends SplitCompatApplication {
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
if (isBrowserProcess()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
DexFixer.setHasIsolatedSplits(true);
}
setImplSupplier(() -> {
Context chromeContext = SplitCompatUtils.createChromeContext(this);
return (Impl) SplitCompatUtils.newInstance(
chromeContext, mChromeApplicationClassName);
});
applyActivityClassLoaderWorkaround();
applyDexCompileWorkaround();
} else {
setImplSupplier(() -> createNonBrowserApplication());
}
......@@ -158,71 +149,6 @@ public class SplitChromeApplication extends SplitCompatApplication {
});
}
/**
* Android OMR1 has a bug where bg-dexopt-job will break optimized dex files for splits. This
* leads to *very* slow startup on those devices. To mitigate this, we attempt to force a dex
* compile if necessary.
*/
private void applyDexCompileWorkaround() {
// This bug only happens in OMR1. Skip the workaround on local builds to avoid affecting
// perf bots.
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1
|| ChromeVersionInfo.isLocalBuild()) {
return;
}
// Wait until startup completes so this doesn't slow down early startup or mess with
// compiled dex files before they get loaded initially.
DeferredStartupHandler.getInstance().addDeferredTask(() -> {
// BEST_EFFORT will only affect when the task runs, the dexopt will run with
// normal priority (but in a separate process, due to using Runtime.exec()).
PostTask.postTask(TaskTraits.BEST_EFFORT_MAY_BLOCK, () -> {
try {
// If the app has just been updated, it will be compiled with
// quicken. The next time bg-dexopt-job runs it will break the
// optimized dex for splits. If we force compile now, then
// bg-dexopt-job won't mess up the splits, and we save the user a
// slow startup.
if (needsDexCompileAfterUpdate()) {
performDexCompile();
return;
}
// Make sure all splits are compiled correclty, and if not force a
// compile.
String[] splitNames = ApiHelperForO.getSplitNames(getApplicationInfo());
for (int i = 0; i < splitNames.length; i++) {
// Ignore config splits like "config.en".
if (splitNames[i].contains(".")) {
continue;
}
if (DexFile.isDexOptNeeded(getApplicationInfo().splitSourceDirs[i])) {
performDexCompile();
return;
}
}
} catch (Exception e) {
Log.e(TAG, "Error compiling dex.", e);
}
});
});
}
/** Returns whether the dex has been compiled since the last app update. */
private boolean needsDexCompileAfterUpdate() {
return SharedPreferencesManager.getInstance().readInt(
ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION)
!= PackageUtils.getPackageVersion(this, getPackageName());
}
/** Compiles dex for the app, and sets the pref key tracking the latest compiled version. */
private void performDexCompile() throws Exception {
Runtime.getRuntime().exec(
new String[] {"cmd", "package", "compile", "-r", "shared", getPackageName()});
SharedPreferencesManager.getInstance().writeInt(
ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION,
PackageUtils.getPackageVersion(this, getPackageName()));
}
private static void replaceClassLoader(Context baseContext, ClassLoader classLoader) {
while (baseContext instanceof ContextWrapper) {
baseContext = ((ContextWrapper) baseContext).getBaseContext();
......
......@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.CallSuper;
......@@ -145,6 +146,10 @@ public class SplitCompatApplication extends Application {
if (isBrowserProcess) {
checkAppBeingReplaced();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Fixes are never required before O (where "cmd package compile" does not exist).
DexFixer.scheduleDexFix();
}
PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX);
// Renderer and GPU processes have command line passed to them via IPC
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.base;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.system.Os;
import android.system.StructStat;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowDexFile;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.chromium.base.BuildInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import java.io.IOException;
/**
* Unit tests for {@link DexFixer}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.O_MR1,
shadows = {ShadowRecordHistogram.class, DexFixerTest.ShadowOs.class})
public class DexFixerTest {
@Implements(Os.class)
public static class ShadowOs {
static boolean sWorldReadable = true;
@Implementation
public static StructStat stat(String path) {
if (path.endsWith(".odex")) {
return new StructStat(
0, 0, sWorldReadable ? 0777 : 0700, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
return Shadow.directlyOn(Os.class, "stat", ClassParameter.from(String.class, path));
}
}
@Mock
private Runtime mMockRuntime;
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Before
public void setUp() {
ShadowOs.sWorldReadable = true;
}
@After
public void tearDown() {
ShadowRecordHistogram.reset();
DexFixer.setHasIsolatedSplits(false);
}
private static int getReason() {
int ret = -1;
for (int i = 0; i < DexFixerReason.COUNT; ++i) {
int count =
ShadowRecordHistogram.getHistogramValueCountForTesting("Android.DexFixer", i);
if (count > 0) {
assertThat(count).isEqualTo(1);
assertThat(ret).isEqualTo(-1);
ret = i;
}
}
assertThat(ret).isNotEqualTo(-1);
return ret;
}
private void verifyDexOpt() {
try {
verify(mMockRuntime).exec(Mockito.matches("cmd package compile -r shared \\S+"));
} catch (IOException e) {
// Mocks don't actually throw...
}
}
@Test
public void testFixDexIfNecessary_notNeeded() {
DexFixer.fixDexIfNecessary(mMockRuntime);
assertThat(getReason()).isEqualTo(DexFixerReason.NOT_NEEDED);
verifyNoMoreInteractions(mMockRuntime);
}
@Test
public void testFixDexIfNecessary_notReadable() {
ShadowOs.sWorldReadable = false;
DexFixer.fixDexIfNecessary(mMockRuntime);
assertThat(getReason()).isEqualTo(DexFixerReason.NOT_READABLE);
verifyDexOpt();
}
@Test
public void testFixDexIfNecessary_update() {
DexFixer.setHasIsolatedSplits(true);
DexFixer.fixDexIfNecessary(mMockRuntime);
assertThat(getReason()).isEqualTo(DexFixerReason.O_MR1_AFTER_UPDATE);
verifyDexOpt();
// Second time should be okay.
ShadowRecordHistogram.reset();
DexFixer.fixDexIfNecessary(mMockRuntime);
assertThat(getReason()).isEqualTo(DexFixerReason.NOT_NEEDED);
verifyNoMoreInteractions(mMockRuntime);
}
@Test
public void testFixDexIfNecessary_corruptDex() {
ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
appInfo.splitNames = new String[] {"a"};
appInfo.splitSourceDirs = new String[] {"/a.apk"};
DexFixer.setHasIsolatedSplits(true);
SharedPreferencesManager.getInstance().writeLong(
ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION,
BuildInfo.getInstance().versionCode);
ShadowDexFile.setIsDexOptNeeded(true);
DexFixer.fixDexIfNecessary(mMockRuntime);
assertThat(getReason()).isEqualTo(DexFixerReason.O_MR1_CORRUPTED);
verifyDexOpt();
}
@Test
public void testFixDexIfNecessary_notReadableWithSplits() {
ApplicationInfo appInfo = ContextUtils.getApplicationContext().getApplicationInfo();
appInfo.splitNames = new String[] {"ignored.en"};
DexFixer.setHasIsolatedSplits(true);
ShadowOs.sWorldReadable = false;
SharedPreferencesManager.getInstance().writeLong(
ChromePreferenceKeys.ISOLATED_SPLITS_DEX_COMPILE_VERSION,
BuildInfo.getInstance().versionCode);
DexFixer.fixDexIfNecessary(mMockRuntime);
assertThat(getReason()).isEqualTo(DexFixerReason.NOT_READABLE);
verifyDexOpt();
}
}
......@@ -17349,6 +17349,16 @@ metrics consent we also won't be able to send UMA metrics. -->
<int value="7" label="Elements - Accessibility"/>
</enum>
<enum name="DexFixerReason">
<int value="0" label="Os.stat() failed"/>
<int value="1" label="Runtime.exec() failed"/>
<int value="2" label="Dexopt not needed"/>
<int value="5" label="Preemptive Dexopt after app updated"/>
<int value="6" label="DexFile.isDexOptNeeded() returned true"/>
<int value="7" label="DexFile.isDexOptNeeded() threw"/>
<int value="8" label="Dex files not world-readable"/>
</enum>
<enum name="DiagnosticsRecoveryRun">
<int value="0" label="Recovery not run"/>
<int value="1" label="Recovery run because of crash"/>
......@@ -607,6 +607,16 @@ reviews. Googlers can read more about this at go/gwsq-gerrit.
</summary>
</histogram>
<histogram name="Android.DexFixer" enum="DexFixerReason"
expires_after="2021-12-01">
<owner>agrieve@chromium.org</owner>
<owner>cduvall@chromium.org</owner>
<summary>
Records the number of times Chrome runs &quot;cmd package compile&quot; to
fix dexopt issues, and for what reason.
</summary>
</histogram>
<histogram name="Android.DirectAction.Perform" enum="DirectActionId"
expires_after="M90">
<owner>szermatt@chromium.org</owner>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment