/*
* 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.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.File;
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 PROFILE_BASE_DIR = "/data/misc/profiles/cur/0";
private static final String PROFILE_REF_BASE_DIR = "/data/misc/profiles/ref";
private static final String PROFILE_FILE = "primary.prof";
private static final String PROFILE_SOURCE_LOCATION = "dexopt/baseline.prof";
/**
* ART may generate an empty profile automatically, and so we use this number to determine a
* minimum length/size that is indicative of the profile being non-empty. This is a number of
* bytes.
*/
private static final int MIN_MEANINGFUL_LENGTH = 10;
/**
* 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;
}
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;
}
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;
}
}
};
/**
* @hide
*/
@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
})
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;
/**
* @hide
*/
@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
})
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;
static boolean shouldSkipInstall(
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics,
long baselineLength,
boolean curExists,
long curLength,
boolean refExists,
long refLength
) {
if (curExists && curLength > MIN_MEANINGFUL_LENGTH) {
// There's a non-empty profile sitting in this directory
diagnostic(executor, diagnostics, DIAGNOSTIC_CURRENT_PROFILE_EXISTS, null);
} else {
diagnostic(executor, diagnostics, DIAGNOSTIC_CURRENT_PROFILE_DOES_NOT_EXIST, null);
}
if (refExists && refLength > MIN_MEANINGFUL_LENGTH) {
diagnostic(executor, diagnostics, DIAGNOSTIC_REF_PROFILE_EXISTS, null);
} else {
diagnostic(executor, diagnostics, DIAGNOSTIC_REF_PROFILE_DOES_NOT_EXIST, null);
}
if (baselineLength > 0 && baselineLength == curLength) {
// If the profiles are exactly the same size, we make the assumption that
// they are in fact the same profile. In this case, there is no work for
// us to do and we can exit early.
result(executor, diagnostics, RESULT_ALREADY_INSTALLED, null);
return true;
}
if (baselineLength > 0 && baselineLength == refLength) {
// If the profiles are exactly the same size, we make the assumption that
// they are in fact the same profile. In this case, there is no work for
// us to do and we can exit early.
result(executor, diagnostics, RESULT_ALREADY_INSTALLED, null);
return true;
}
if (
baselineLength > 0 &&
(baselineLength < curLength || baselineLength < refLength)
) {
// if the baseline profile is smaller than the current profile or
// reference profile, then we assume that it already has the baseline
// profile in it. We avoid doing anything in this case as we don't want
// to introduce unnecessary work on the app or ART every time the app runs.
// TODO: we could do something a bit smarter here to indicate that we've
// already written the profile. For instance, we could save a file marking the
// install and look at that.
result(executor, diagnostics, RESULT_ALREADY_INSTALLED, null);
return true;
}
return false;
}
/**
* 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 apkName The apk file name the profile is targeting
* @param diagnostics The diagnostics callback to pass diagnostics to
*/
private static void transcodeAndWrite(
@NonNull AssetManager assets,
@NonNull String packageName,
@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;
}
File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);
File refProfile = new File(new File(PROFILE_REF_BASE_DIR, packageName), PROFILE_FILE);
DeviceProfileWriter deviceProfileWriter = new DeviceProfileWriter(assets,
executor,
diagnostics,
apkName,
PROFILE_SOURCE_LOCATION,
curProfile,
refProfile
);
if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
return; /* nothing else to do here */
}
DeviceProfileWriter.SkipStrategy skipStrategy =
(newProfileLength, existingProfileState) -> shouldSkipInstall(
executor,
diagnostics,
newProfileLength,
existingProfileState.hasCurFile(),
existingProfileState.getCurLength(),
existingProfileState.hasRefFile(),
existingProfileState.getRefLength()
);
deviceProfileWriter.copyProfileOrRead(skipStrategy)
.transcodeIfNeeded()
.writeIfNeeded(skipStrategy);
}
/**
* 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
) {
Context appContext = context.getApplicationContext();
String packageName = appContext.getPackageName();
ApplicationInfo appInfo = appContext.getApplicationInfo();
AssetManager assetManager = appContext.getAssets();
String apkName = new File(appInfo.sourceDir).getName();
transcodeAndWrite(assetManager, packageName, apkName, executor, diagnostics);
}
}