blob: b93bdda6f8cc2f3b2b774bc5c0dd21034195740b [file] [log] [blame]
/*
* Copyright (C) 2022 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.art;
import static com.android.server.art.ArtManagerLocal.ScheduleBackgroundDexoptJobCallback;
import static com.android.server.art.model.ArtFlags.ScheduleStatus;
import static com.android.server.art.model.Config.Callback;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.os.CancellationSignal;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalManagerRegistry;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.Config;
import com.android.server.art.model.OptimizeResult;
import com.android.server.pm.PackageManagerLocal;
import com.google.auto.value.AutoValue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/** @hide */
public class BackgroundDexOptJob {
private static final String TAG = "BackgroundDexOptJob";
/**
* "android" is the package name for a <service> declared in
* frameworks/base/core/res/AndroidManifest.xml
*/
private static final String JOB_PKG_NAME = Utils.PLATFORM_PACKAGE_NAME;
/** An arbitrary number. Must be unique among all jobs owned by the system uid. */
private static final int JOB_ID = 27873780;
@VisibleForTesting public static final long JOB_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
@NonNull private final Injector mInjector;
@GuardedBy("this") @Nullable private CompletableFuture<Result> mRunningJob = null;
@GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null;
public BackgroundDexOptJob(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal,
@NonNull Config config) {
this(new Injector(context, artManagerLocal, config));
}
@VisibleForTesting
public BackgroundDexOptJob(@NonNull Injector injector) {
mInjector = injector;
}
/** Handles {@link BackgroundDexOptJobService#onStartJob(JobParameters)}. */
public boolean onStartJob(
@NonNull BackgroundDexOptJobService jobService, @NonNull JobParameters params) {
start().thenAcceptAsync(result -> {
writeStats(params, result);
// This is a periodic job, where the interval is specified in the `JobInfo`. "false"
// means not to execute again during a future idle maintenance window in the same
// interval. This job will be executed again in the next interval.
// This call will be ignored if `onStopJob` is called.
jobService.jobFinished(params, false /* wantReschedule */);
});
// "true" means the job will continue running until `jobFinished` is called.
return true;
}
/** Handles {@link BackgroundDexOptJobService#onStopJob(JobParameters)}. */
public boolean onStopJob(@NonNull JobParameters params) {
cancel();
// "true" means to execute again during a future idle maintenance window in the same
// interval.
return true;
}
/** Handles {@link ArtManagerLocal#scheduleBackgroundDexoptJob()}. */
public @ScheduleStatus int schedule() {
if (this != BackgroundDexOptJobService.getJob()) {
throw new IllegalStateException("This job cannot be scheduled");
}
if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) {
Log.i(TAG, "Job is disabled by system property 'pm.dexopt.disable_bg_dexopt'");
return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP;
}
JobInfo.Builder builder =
new JobInfo
.Builder(JOB_ID,
new ComponentName(
JOB_PKG_NAME, BackgroundDexOptJobService.class.getName()))
.setPeriodic(JOB_INTERVAL_MS)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true);
Callback<ScheduleBackgroundDexoptJobCallback> callback =
mInjector.getConfig().getScheduleBackgroundDexoptJobCallback();
if (callback != null) {
Utils.executeAndWait(
callback.executor(), () -> { callback.get().onOverrideJobInfo(builder); });
}
return mInjector.getJobScheduler().schedule(builder.build()) == JobScheduler.RESULT_SUCCESS
? ArtFlags.SCHEDULE_SUCCESS
: ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE;
}
/** Handles {@link ArtManagerLocal#unscheduleBackgroundDexoptJob()}. */
public void unschedule() {
if (this != BackgroundDexOptJobService.getJob()) {
throw new IllegalStateException("This job cannot be unscheduled");
}
mInjector.getJobScheduler().cancel(JOB_ID);
}
@NonNull
public synchronized CompletableFuture<Result> start() {
if (mRunningJob != null) {
Log.i(TAG, "Job is already running");
return mRunningJob;
}
mCancellationSignal = new CancellationSignal();
mRunningJob = new CompletableFuture().supplyAsync(() -> {
Log.i(TAG, "Job started");
try {
return run(mCancellationSignal);
} catch (RuntimeException e) {
Log.e(TAG, "Fatal error", e);
return new FatalErrorResult();
} finally {
Log.i(TAG, "Job finished");
synchronized (this) {
mRunningJob = null;
mCancellationSignal = null;
}
}
});
return mRunningJob;
}
public synchronized void cancel() {
if (mRunningJob == null) {
Log.i(TAG, "Job is not running");
return;
}
mCancellationSignal.cancel();
Log.i(TAG, "Job cancelled");
}
@NonNull
private CompletedResult run(@NonNull CancellationSignal cancellationSignal) {
// TODO(b/254013427): Cleanup dex use info.
// TODO(b/254013425): Cleanup unused secondary dex file artifacts.
// TODO(b/255565888): Downgrade inactive apps.
long startTimeMs = SystemClock.uptimeMillis();
OptimizeResult dexoptResult;
try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) {
dexoptResult = mInjector.getArtManagerLocal().optimizePackages(
snapshot, ReasonMapping.REASON_BG_DEXOPT, cancellationSignal);
}
return CompletedResult.create(dexoptResult, SystemClock.uptimeMillis() - startTimeMs);
}
private void writeStats(@NonNull JobParameters params, @NonNull Result result) {
if (result instanceof CompletedResult) {
var completedResult = (CompletedResult) result;
int status = completedResult.dexoptResult().getFinalStatus()
== OptimizeResult.OPTIMIZE_CANCELLED
? ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION
: ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED;
ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED, status,
params.getStopReason(), completedResult.durationMs(), 0 /* deprecated */);
} else if (result instanceof FatalErrorResult) {
ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN,
JobParameters.STOP_REASON_UNDEFINED, 0 /* durationMs */, 0 /* deprecated */);
}
}
static abstract class Result {}
static class FatalErrorResult extends Result {}
@AutoValue
static abstract class CompletedResult extends Result {
abstract @NonNull OptimizeResult dexoptResult();
abstract long durationMs();
@NonNull
static CompletedResult create(@NonNull OptimizeResult dexoptResult, long durationMs) {
return new AutoValue_BackgroundDexOptJob_CompletedResult(dexoptResult, durationMs);
}
}
/**
* Injector pattern for testing purpose.
*
* @hide
*/
@VisibleForTesting
public static class Injector {
@NonNull private final Context mContext;
@NonNull private final ArtManagerLocal mArtManagerLocal;
@NonNull private final Config mConfig;
Injector(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal,
@NonNull Config config) {
mContext = context;
mArtManagerLocal = artManagerLocal;
mConfig = config;
}
@NonNull
public ArtManagerLocal getArtManagerLocal() {
return mArtManagerLocal;
}
@NonNull
public PackageManagerLocal getPackageManagerLocal() {
return LocalManagerRegistry.getManager(PackageManagerLocal.class);
}
@NonNull
public Config getConfig() {
return mConfig;
}
@NonNull
public JobScheduler getJobScheduler() {
return mContext.getSystemService(JobScheduler.class);
}
}
}