[go: nahoru, domu]

blob: 70f43053e632adc21930777e4c5f61363e026bf0 [file] [log] [blame]
/*
* Copyright 2021 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 androidx.profileinstaller;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
/**
* Install ahead of time tracing profiles to configure ART to precompile bundled libraries.
*
* This will automatically be called by {@link ProfileInstallerInitializer} and you should never
* call this unless you have disabled the initializer in your manifest.
*
* This reads profiles from the assets directory, where they must be embedded during the build
* process. This will have no effect if there is not a profile embedded in the current APK.
*/
public class ProfileInstaller {
// cannot construct
private ProfileInstaller() {}
private static final String TAG = "ProfileInstaller";
private static final String PROFILE_BASE_DIR = "/data/misc/profiles/cur/0";
private static final String PROFILE_FILE = "primary.prof";
private static final String PROFILE_SOURCE_LOCATION = "dexopt/baseline.prof";
private static final String PROFILE_META_LOCATION = "dexopt/baseline.profm";
private static final String PROFILE_INSTALLER_SKIP_FILE_NAME =
"profileinstaller_profileWrittenFor_lastUpdateTime.dat";
/**
* An object which can be passed to the ProfileInstaller which will receive information
* during the installation process which can be used for logging and telemetry.
*/
public interface DiagnosticsCallback {
/**
* The diagnostic method will get called 0 to many times during the installation process,
* and is passed a [code] and optionally [data] which provides some information around
* the install process.
* @param code An int specifying which diagnostic situation has occurred.
* @param data Optional data passed in that relates to the code passed.
*/
void onDiagnosticReceived(@DiagnosticCode int code, @Nullable Object data);
/**
* The result method will get called exactly once per installation, with a [code]
* indicating what the result of the installation was.
*
* @param code An int specifying which result situation has occurred.
* @param data Optional data passed in that relates to the code that was passed.
*/
void onResultReceived(@ResultCode int code, @Nullable Object data);
}
@SuppressWarnings("SameParameterValue")
static void result(
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics,
@ResultCode int code,
@Nullable Object data
) {
executor.execute(() -> diagnostics.onResultReceived(code, data));
}
@SuppressWarnings("SameParameterValue")
static void diagnostic(
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics,
@DiagnosticCode int code,
@Nullable Object data
) {
executor.execute(() -> diagnostics.onDiagnosticReceived(code, data));
}
private static final DiagnosticsCallback EMPTY_DIAGNOSTICS = new DiagnosticsCallback() {
@Override
public void onDiagnosticReceived(int code, @Nullable Object data) {
// do nothing
}
@Override
public void onResultReceived(int code, @Nullable Object data) {
// do nothing
}
};
@NonNull
static final DiagnosticsCallback LOG_DIAGNOSTICS = new DiagnosticsCallback() {
static final String TAG = "ProfileInstaller";
@Override
public void onDiagnosticReceived(int code, @Nullable Object data) {
String msg = "";
switch (code) {
case DIAGNOSTIC_CURRENT_PROFILE_EXISTS:
msg = "DIAGNOSTIC_CURRENT_PROFILE_EXISTS";
break;
case DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST:
msg = "DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST";
break;
case DIAGNOSTIC_REF_PROFILE_EXISTS:
msg = "DIAGNOSTIC_REF_PROFILE_EXISTS";
break;
case DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST:
msg = "DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST";
break;
case DIAGNOSTIC_PROFILE_IS_COMPRESSED:
msg = "DIAGNOSTIC_PROFILE_IS_COMPRESSED";
break;
}
Log.d(TAG, msg);
}
@Override
public void onResultReceived(int code, @Nullable Object data) {
String msg = "";
switch (code) {
case RESULT_INSTALL_SUCCESS: msg = "RESULT_INSTALL_SUCCESS";
break;
case RESULT_ALREADY_INSTALLED: msg = "RESULT_ALREADY_INSTALLED";
break;
case RESULT_UNSUPPORTED_ART_VERSION: msg = "RESULT_UNSUPPORTED_ART_VERSION";
break;
case RESULT_NOT_WRITABLE: msg = "RESULT_NOT_WRITABLE";
break;
case RESULT_DESIRED_FORMAT_UNSUPPORTED: msg = "RESULT_DESIRED_FORMAT_UNSUPPORTED";
break;
case RESULT_BASELINE_PROFILE_NOT_FOUND: msg = "RESULT_BASELINE_PROFILE_NOT_FOUND";
break;
case RESULT_IO_EXCEPTION: msg = "RESULT_IO_EXCEPTION";
break;
case RESULT_PARSE_EXCEPTION: msg = "RESULT_PARSE_EXCEPTION";
break;
case RESULT_INSTALL_SKIP_FILE_SUCCESS: msg = "RESULT_INSTALL_SKIP_FILE_SUCCESS";
break;
case RESULT_DELETE_SKIP_FILE_SUCCESS: msg = "RESULT_DELETE_SKIP_FILE_SUCCESS";
break;
}
switch (code) {
case RESULT_BASELINE_PROFILE_NOT_FOUND:
case RESULT_IO_EXCEPTION:
case RESULT_PARSE_EXCEPTION:
Log.e(TAG, msg, (Throwable) data);
break;
default:
Log.d(TAG, msg);
break;
}
}
};
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DIAGNOSTIC_CURRENT_PROFILE_EXISTS,
DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST,
DIAGNOSTIC_REF_PROFILE_EXISTS,
DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST,
DIAGNOSTIC_PROFILE_IS_COMPRESSED
})
public @interface DiagnosticCode {}
/**
* Indicates that when tryInstallSync was run, an existing profile was found in the "cur"
* directory. The associated [data] passed in for this call will be the size, in bytes, of
* the profile that was found.
*/
@DiagnosticCode public static final int DIAGNOSTIC_CURRENT_PROFILE_EXISTS = 1;
/**
* Indicates that when tryInstallSync was run, no existing profile was found in the "cur"
* directory.
*/
@DiagnosticCode public static final int DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST = 2;
/**
* Indicates that when tryInstallSync was run, an existing profile was found in the "cur"
* directory. The associated [data] passed in for this call will be the size, in bytes, of
* the profile that was found.
*/
@DiagnosticCode public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3;
/**
* Indicates that when tryInstallSync was run, no existing profile was found in the "cur"
* directory.
*/
@DiagnosticCode public static final int DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST = 4;
/**
* Indicates that the profile is compressed and a version of bundletool newer than 1.13.2
* needs to be used to build the app.
*/
@DiagnosticCode public static final int DIAGNOSTIC_PROFILE_IS_COMPRESSED = 5;
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
RESULT_INSTALL_SUCCESS,
RESULT_ALREADY_INSTALLED,
RESULT_UNSUPPORTED_ART_VERSION,
RESULT_NOT_WRITABLE,
RESULT_DESIRED_FORMAT_UNSUPPORTED,
RESULT_BASELINE_PROFILE_NOT_FOUND,
RESULT_IO_EXCEPTION,
RESULT_PARSE_EXCEPTION,
RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND,
RESULT_INSTALL_SKIP_FILE_SUCCESS,
RESULT_DELETE_SKIP_FILE_SUCCESS,
RESULT_SAVE_PROFILE_SIGNALLED,
RESULT_SAVE_PROFILE_SKIPPED,
RESULT_BENCHMARK_OPERATION_SUCCESS,
RESULT_BENCHMARK_OPERATION_FAILURE,
RESULT_BENCHMARK_OPERATION_UNKNOWN
})
public @interface ResultCode {}
/**
* Indicates that the profile got installed and written to disk successfully.
*
* Note that this should happen but is not the only condition that indicates "nothing went
* wrong". Several result codes are indicative of expected behavior.
*/
@ResultCode public static final int RESULT_INSTALL_SUCCESS = 1;
/**
* Indicates that no installation occurred because it was determined that the baseline
* profile had already been installed previously.
*/
@ResultCode public static final int RESULT_ALREADY_INSTALLED = 2;
/**
* Indicates that the current SDK level is such that installing a profile is not supported by
* ART.
*/
@ResultCode public static final int RESULT_UNSUPPORTED_ART_VERSION = 3;
/**
* Indicates that the installation was aborted because the app was found to not have adequate
* permissions to write the profile to disk.
*/
@ResultCode public static final int RESULT_NOT_WRITABLE = 4;
/**
* Indicates that the format required by this SDK version is not supported by this version of
* the ProfileInstaller library.
*/
@ResultCode public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5;
/**
* Indicates that no baseline profile was bundled with the APK, and as a result, no
* installation could take place.
*/
@ResultCode public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6;
/**
* Indicates that an IO Exception took place during install. The associated [data] with this
* result is the exception.
*/
@ResultCode public static final int RESULT_IO_EXCEPTION = 7;
/**
* Indicates that a parsing exception occurred during install. The associated [data] with
* this result is the exception.
*/
@ResultCode public static final int RESULT_PARSE_EXCEPTION = 8;
/**
* Indicates that the device requires a metadata file in order to install the profile
* successfully, but there was not one included in the APK.
*
* The correct metadata files are produced when using Android Gradle Plugin `7.1.0-alpha05` or
* newer.
*/
@ResultCode public static final int RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND = 9;
/**
* Indicates that a skip file was successfully written and profile installation will be skipped.
*/
@ResultCode public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10;
/**
* Indicates that a skip file was successfully deleted and profile installation will resume.
*/
@ResultCode public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11;
/**
* Indicates that this process was signalled to save it's profile information
*/
@ResultCode public static final int RESULT_SAVE_PROFILE_SIGNALLED = 12;
/**
* Indicates that this process was not able to signal itself to save profile information
*/
@ResultCode public static final int RESULT_SAVE_PROFILE_SKIPPED = 13;
/**
* Indicates that the benchmark operation was successful
*/
@ResultCode public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14;
/**
* Indicates that the benchmark operation failed
*/
@ResultCode public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15;
/**
* Indicates that the benchmark operation was unknown, likely meaning profileinstaller needs
* to update to support the operation
*/
@ResultCode public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16;
/**
* Check if we've already installed a profile for this app installation.
*
*
* @param packageInfo used to lookup the last install time for this apk
* @param appFilesDir directory to store a file to note prior installation
* @param diagnostics for noting IO errors
* @return true every time the APK is installed or upgraded until markProfileWritten is called.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@WorkerThread
static boolean hasAlreadyWrittenProfileForThisInstall(PackageInfo packageInfo,
File appFilesDir,
DiagnosticsCallback diagnostics) {
File skipFile = new File(appFilesDir, PROFILE_INSTALLER_SKIP_FILE_NAME);
if (!skipFile.exists()) {
/* We've never saved a skip file, fastest path */
return false;
}
long lastProfileWritePackageUpdateTime;
try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream(skipFile))) {
lastProfileWritePackageUpdateTime = dataInputStream.readLong();
} catch (IOException e) {
/* Consider the file as not a valid match */
return false;
}
// check if the last write package update time matches the current install
boolean result = lastProfileWritePackageUpdateTime == packageInfo.lastUpdateTime;
if (result) {
diagnostics.onResultReceived(RESULT_ALREADY_INSTALLED, null);
}
return result;
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
static void noteProfileWrittenFor(@NonNull PackageInfo packageInfo, @NonNull File appFilesDir) {
File skipFile = new File(appFilesDir, PROFILE_INSTALLER_SKIP_FILE_NAME);
try (DataOutputStream os = new DataOutputStream(new FileOutputStream(skipFile))) {
os.writeLong(packageInfo.lastUpdateTime);
} catch (IOException e) {
/* nothing */
}
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
static boolean deleteProfileWrittenFor(@NonNull File appFilesDir) {
File skipFile = new File(appFilesDir, PROFILE_INSTALLER_SKIP_FILE_NAME);
return skipFile.delete();
}
/**
* Transcode the source file to an appropriate destination format for this OS version, and
* write it to the ART aot directory.
* @param assets the asset manager to read source file from dexopt/baseline.prof
* @param packageName package name of the current apk
* @param packageInfo for noting successful installation
* @param filesDir for noting successful installation
* @param apkName The apk file name the profile is targeting
* @param diagnostics The diagnostics callback to pass diagnostics to
* @return True whether the operation was successful, false otherwise
*/
private static boolean transcodeAndWrite(
@NonNull AssetManager assets,
@NonNull String packageName,
@NonNull PackageInfo packageInfo,
@NonNull File filesDir,
@NonNull String apkName,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
result(executor, diagnostics, ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, null);
return false;
}
File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);
DeviceProfileWriter deviceProfileWriter = new DeviceProfileWriter(assets, executor,
diagnostics, apkName, PROFILE_SOURCE_LOCATION, PROFILE_META_LOCATION, curProfile);
if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
return false; /* nothing else to do here */
}
boolean success = deviceProfileWriter.read()
.transcodeIfNeeded()
.write();
if (success) {
noteProfileWrittenFor(packageInfo, filesDir);
}
return success;
}
/**
* Try to write the profile from assets into the ART aot profile directory.
*
* You do not need to call this method if {@link ProfileInstallerInitializer} is enabled for
* your application.
*
* If you disable the initializer, you should <b>call this method within 5-10 seconds</b> of
* app launch, to ensure that art can use the generated profile.
*
* This should always be called after the first screen is shown to the user, to avoid
* delaying application startup to install AOT profiles.
*
* It is encouraged that you call this method during <b>every</b> app startup to ensure
* profiles are written correctly after app upgrades, or if the profile failed to write on the
* previous launch.
*
* Profiles will be correctly formatted based on the current API level of the device, and only
* installed if profileinstaller can determine that it is safe to do so.
*
* If the profile is not written, no action needs to be taken.
*
* @param context context to read assets from
*/
@WorkerThread
public static void writeProfile(@NonNull Context context) {
writeProfile(context, Runnable::run, EMPTY_DIAGNOSTICS);
}
/**
* Try to write the profile from assets into the ART aot profile directory.
*
* You do not need to call this method if {@link ProfileInstallerInitializer} is enabled for
* your application.
*
* If you disable the initializer, you should call this method within 5-10 seconds of app
* launch, to ensure that art can use the generated profile.
*
* This should always be called after the first screen is shown to the user, to avoid
* delaying application startup to install AOT profiles.
*
* It is encouraged that you call this method during <b>every</b> app startup to ensure
* profiles are written correctly after app upgrades, or if the profile failed to write on the
* previous launch.
*
* Profiles will be correctly formatted based on the current API level of the device, and only
* installed if profileinstaller can determine that it is safe to do so.
*
* If the profile is not written, no action needs to be taken.
*
* @param context context to read assets from
* @param diagnostics an object which will receive diagnostic information about the
* installation
* @param executor the executor to run the diagnostic events through
*/
@WorkerThread
public static void writeProfile(
@NonNull Context context,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics
) {
writeProfile(context, executor, diagnostics, false);
}
/**
* Try to write the profile from assets into the ART aot profile directory.
*
* You do not need to call this method if {@link ProfileInstallerInitializer} is enabled for
* your application.
*
* If you disable the initializer, you should call this method within 5-10 seconds of app
* launch, to ensure that art can use the generated profile.
*
* This should always be called after the first screen is shown to the user, to avoid
* delaying application startup to install AOT profiles.
*
* It is encouraged that you call this method during <b>every</b> app startup to ensure
* profiles are written correctly after app upgrades, or if the profile failed to write on the
* previous launch.
*
* Profiles will be correctly formatted based on the current API level of the device, and only
* installed if profileinstaller can determine that it is safe to do so.
*
* If the profile is not written, no action needs to be taken unlesss {@code
* forceWriteProfile} is {@code true}.
*
* @param context context to read assets from
* @param executor the executor to run the diagnostic events through
* @param diagnostics an object which will receive diagnostic information about the installation
* @param forceWriteProfile an override to always install the profile
*
*/
@WorkerThread
@SuppressWarnings("deprecation")
static void writeProfile(
@NonNull Context context,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics,
boolean forceWriteProfile
) {
Context appContext = context.getApplicationContext();
String packageName = appContext.getPackageName();
ApplicationInfo appInfo = appContext.getApplicationInfo();
AssetManager assetManager = appContext.getAssets();
String apkName = new File(appInfo.sourceDir).getName();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
diagnostics.onResultReceived(RESULT_IO_EXCEPTION, e);
// Calls the verification. Note that in this case since the force install failed we
// don't need to report it to the ProfileVerifier.
ProfileVerifier.writeProfileVerification(context, false);
return;
}
File filesDir = context.getFilesDir();
if (forceWriteProfile
|| !hasAlreadyWrittenProfileForThisInstall(packageInfo, filesDir, diagnostics)) {
Log.d(TAG, "Installing profile for " + context.getPackageName());
boolean profileWritten = transcodeAndWrite(assetManager, packageName, packageInfo,
filesDir, apkName, executor, diagnostics);
ProfileVerifier.writeProfileVerification(
context, profileWritten && forceWriteProfile);
} else {
Log.d(TAG, "Skipping profile installation for " + context.getPackageName());
ProfileVerifier.writeProfileVerification(context, false);
}
}
/**
* Writes a profile installation skip file, which makes {@link ProfileInstaller} skip profile
* installation. This is being done so that Macrobenchmarks can request a skip file for
* `CompilationMode.None()`, and avoid any interference from {@link ProfileInstaller}.
*
* @param context context to read assets from
* @param diagnostics an object which will receive diagnostic information
* @param executor the executor to run the diagnostic events through
*/
@WorkerThread
@SuppressWarnings("deprecation")
static void writeSkipFile(
@NonNull Context context,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics
) {
Context appContext = context.getApplicationContext();
String packageName = appContext.getPackageName();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
result(executor, diagnostics, RESULT_IO_EXCEPTION, e);
return;
}
File filesDir = context.getFilesDir();
ProfileInstaller.noteProfileWrittenFor(packageInfo, filesDir);
result(executor, diagnostics, RESULT_INSTALL_SKIP_FILE_SUCCESS, null);
}
/**
* Deletes a profile installation skip so profile installation can continue after
* CompilationMode.None()`.
*
* @param context context to read assets from
* @param diagnostics an object which will receive diagnostic information
* @param executor the executor to run the diagnostic events through
*/
@WorkerThread
static void deleteSkipFile(
@NonNull Context context,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics
) {
File filesDir = context.getFilesDir();
ProfileInstaller.deleteProfileWrittenFor(filesDir);
result(executor, diagnostics, RESULT_DELETE_SKIP_FILE_SUCCESS, null);
}
}