1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.art; 18 19 import static com.android.server.art.ArtManagerLocal.ScheduleBackgroundDexoptJobCallback; 20 import static com.android.server.art.model.ArtFlags.ScheduleStatus; 21 import static com.android.server.art.model.Config.Callback; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.job.JobInfo; 26 import android.app.job.JobParameters; 27 import android.app.job.JobScheduler; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.os.Build; 31 import android.os.CancellationSignal; 32 import android.os.SystemClock; 33 import android.os.SystemProperties; 34 import android.util.Log; 35 36 import androidx.annotation.RequiresApi; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.server.LocalManagerRegistry; 41 import com.android.server.art.model.ArtFlags; 42 import com.android.server.art.model.Config; 43 import com.android.server.art.model.DexoptResult; 44 import com.android.server.pm.PackageManagerLocal; 45 46 import com.google.auto.value.AutoValue; 47 48 import java.util.Objects; 49 import java.util.Optional; 50 import java.util.concurrent.CompletableFuture; 51 import java.util.concurrent.TimeUnit; 52 53 /** @hide */ 54 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 55 public class BackgroundDexoptJob { 56 private static final String TAG = ArtManagerLocal.TAG; 57 58 /** 59 * "android" is the package name for a <service> declared in 60 * frameworks/base/core/res/AndroidManifest.xml 61 */ 62 private static final String JOB_PKG_NAME = Utils.PLATFORM_PACKAGE_NAME; 63 /** An arbitrary number. Must be unique among all jobs owned by the system uid. */ 64 private static final int JOB_ID = 27873780; 65 66 @VisibleForTesting public static final long JOB_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); 67 68 @NonNull private final Injector mInjector; 69 70 @GuardedBy("this") @Nullable private CompletableFuture<Result> mRunningJob = null; 71 @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null; 72 @GuardedBy("this") @NonNull private Optional<Integer> mLastStopReason = Optional.empty(); 73 BackgroundDexoptJob(@onNull Context context, @NonNull ArtManagerLocal artManagerLocal, @NonNull Config config)74 public BackgroundDexoptJob(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal, 75 @NonNull Config config) { 76 this(new Injector(context, artManagerLocal, config)); 77 } 78 79 @VisibleForTesting BackgroundDexoptJob(@onNull Injector injector)80 public BackgroundDexoptJob(@NonNull Injector injector) { 81 mInjector = injector; 82 } 83 84 /** Handles {@link BackgroundDexoptJobService#onStartJob(JobParameters)}. */ onStartJob( @onNull BackgroundDexoptJobService jobService, @NonNull JobParameters params)85 public boolean onStartJob( 86 @NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) { 87 start().thenAcceptAsync(result -> { 88 writeStats(result); 89 // This is a periodic job, where the interval is specified in the `JobInfo`. "true" 90 // means to execute again during a future idle maintenance window in the same 91 // interval, while "false" means not to execute again during a future idle maintenance 92 // window in the same interval but to execute again in the next interval. 93 // This call will be ignored if `onStopJob` is called. 94 boolean wantsReschedule = result instanceof CompletedResult 95 && ((CompletedResult) result).dexoptResult().getFinalStatus() 96 == DexoptResult.DEXOPT_CANCELLED; 97 jobService.jobFinished(params, wantsReschedule); 98 }); 99 // "true" means the job will continue running until `jobFinished` is called. 100 return true; 101 } 102 103 /** Handles {@link BackgroundDexoptJobService#onStopJob(JobParameters)}. */ onStopJob(@onNull JobParameters params)104 public boolean onStopJob(@NonNull JobParameters params) { 105 synchronized (this) { 106 mLastStopReason = Optional.of(params.getStopReason()); 107 } 108 cancel(); 109 // "true" means to execute again during a future idle maintenance window in the same 110 // interval. 111 return true; 112 } 113 114 /** Handles {@link ArtManagerLocal#scheduleBackgroundDexoptJob()}. */ schedule()115 public @ScheduleStatus int schedule() { 116 if (this != BackgroundDexoptJobService.getJob()) { 117 throw new IllegalStateException("This job cannot be scheduled"); 118 } 119 120 if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) { 121 Log.i(TAG, "Job is disabled by system property 'pm.dexopt.disable_bg_dexopt'"); 122 return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; 123 } 124 125 JobInfo.Builder builder = 126 new JobInfo 127 .Builder(JOB_ID, 128 new ComponentName( 129 JOB_PKG_NAME, BackgroundDexoptJobService.class.getName())) 130 .setPeriodic(JOB_INTERVAL_MS) 131 .setRequiresDeviceIdle(true) 132 .setRequiresCharging(true) 133 .setRequiresBatteryNotLow(true); 134 135 Callback<ScheduleBackgroundDexoptJobCallback, Void> callback = 136 mInjector.getConfig().getScheduleBackgroundDexoptJobCallback(); 137 if (callback != null) { 138 Utils.executeAndWait( 139 callback.executor(), () -> { callback.get().onOverrideJobInfo(builder); }); 140 } 141 142 JobInfo info = builder.build(); 143 if (info.isRequireStorageNotLow()) { 144 // See the javadoc of 145 // `ArtManagerLocal.ScheduleBackgroundDexoptJobCallback.onOverrideJobInfo` for details. 146 throw new IllegalStateException("'setRequiresStorageNotLow' must not be set"); 147 } 148 149 return mInjector.getJobScheduler().schedule(info) == JobScheduler.RESULT_SUCCESS 150 ? ArtFlags.SCHEDULE_SUCCESS 151 : ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE; 152 } 153 154 /** Handles {@link ArtManagerLocal#unscheduleBackgroundDexoptJob()}. */ unschedule()155 public void unschedule() { 156 if (this != BackgroundDexoptJobService.getJob()) { 157 throw new IllegalStateException("This job cannot be unscheduled"); 158 } 159 160 mInjector.getJobScheduler().cancel(JOB_ID); 161 } 162 163 @NonNull start()164 public synchronized CompletableFuture<Result> start() { 165 if (mRunningJob != null) { 166 Log.i(TAG, "Job is already running"); 167 return mRunningJob; 168 } 169 170 mCancellationSignal = new CancellationSignal(); 171 mLastStopReason = Optional.empty(); 172 mRunningJob = new CompletableFuture().supplyAsync(() -> { 173 try (var tracing = new Utils.TracingWithTimingLogging(TAG, "jobExecution")) { 174 return run(mCancellationSignal); 175 } catch (RuntimeException e) { 176 Log.e(TAG, "Fatal error", e); 177 return new FatalErrorResult(); 178 } finally { 179 synchronized (this) { 180 mRunningJob = null; 181 mCancellationSignal = null; 182 } 183 } 184 }); 185 return mRunningJob; 186 } 187 cancel()188 public synchronized void cancel() { 189 if (mRunningJob == null) { 190 Log.i(TAG, "Job is not running"); 191 return; 192 } 193 194 mCancellationSignal.cancel(); 195 Log.i(TAG, "Job cancelled"); 196 } 197 198 @Nullable get()199 public synchronized CompletableFuture<Result> get() { 200 return mRunningJob; 201 } 202 203 @NonNull run(@onNull CancellationSignal cancellationSignal)204 private CompletedResult run(@NonNull CancellationSignal cancellationSignal) { 205 long startTimeMs = SystemClock.uptimeMillis(); 206 DexoptResult dexoptResult; 207 try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) { 208 dexoptResult = mInjector.getArtManagerLocal().dexoptPackages(snapshot, 209 ReasonMapping.REASON_BG_DEXOPT, cancellationSignal, 210 null /* processCallbackExecutor */, null /* processCallback */); 211 212 // For simplicity, we don't support cancelling the following operation in the middle. 213 // This is fine because it typically takes only a few seconds. 214 if (!cancellationSignal.isCanceled()) { 215 // We do the cleanup after dexopt so that it doesn't affect the `getSizeBeforeBytes` 216 // field in the result that we send to callbacks. Admittedly, this will cause us to 217 // lose some chance to dexopt when the storage is very low, but it's fine because we 218 // can still dexopt in the next run. 219 long freedBytes = mInjector.getArtManagerLocal().cleanup(snapshot); 220 Log.i(TAG, String.format("Freed %d bytes", freedBytes)); 221 } 222 } 223 return CompletedResult.create(dexoptResult, SystemClock.uptimeMillis() - startTimeMs); 224 } 225 writeStats(@onNull Result result)226 private void writeStats(@NonNull Result result) { 227 Optional<Integer> stopReason; 228 synchronized (this) { 229 stopReason = mLastStopReason; 230 } 231 if (result instanceof CompletedResult) { 232 var completedResult = (CompletedResult) result; 233 ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED, 234 getStatusForStats(completedResult, stopReason), 235 stopReason.orElse(JobParameters.STOP_REASON_UNDEFINED), 236 completedResult.durationMs(), 0 /* deprecated */); 237 } else if (result instanceof FatalErrorResult) { 238 ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED, 239 ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_FATAL_ERROR, 240 JobParameters.STOP_REASON_UNDEFINED, 0 /* durationMs */, 0 /* deprecated */); 241 } 242 } 243 getStatusForStats(@onNull CompletedResult result, Optional<Integer> stopReason)244 private int getStatusForStats(@NonNull CompletedResult result, Optional<Integer> stopReason) { 245 if (result.dexoptResult().getFinalStatus() == DexoptResult.DEXOPT_CANCELLED) { 246 if (stopReason.isPresent()) { 247 return ArtStatsLog 248 .BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION; 249 } else { 250 return ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_API; 251 } 252 } 253 254 boolean isSkippedDueToStorageLow = 255 result.dexoptResult() 256 .getPackageDexoptResults() 257 .stream() 258 .flatMap(packageResult 259 -> packageResult.getDexContainerFileDexoptResults().stream()) 260 .anyMatch(fileResult -> fileResult.isSkippedDueToStorageLow()); 261 if (isSkippedDueToStorageLow) { 262 return ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_NO_SPACE_LEFT; 263 } 264 265 return ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED; 266 } 267 268 static abstract class Result {} 269 static class FatalErrorResult extends Result {} 270 271 @AutoValue 272 static abstract class CompletedResult extends Result { dexoptResult()273 abstract @NonNull DexoptResult dexoptResult(); durationMs()274 abstract long durationMs(); 275 276 @NonNull create(@onNull DexoptResult dexoptResult, long durationMs)277 static CompletedResult create(@NonNull DexoptResult dexoptResult, long durationMs) { 278 return new AutoValue_BackgroundDexoptJob_CompletedResult(dexoptResult, durationMs); 279 } 280 } 281 282 /** 283 * Injector pattern for testing purpose. 284 * 285 * @hide 286 */ 287 @VisibleForTesting 288 public static class Injector { 289 @NonNull private final Context mContext; 290 @NonNull private final ArtManagerLocal mArtManagerLocal; 291 @NonNull private final Config mConfig; 292 Injector(@onNull Context context, @NonNull ArtManagerLocal artManagerLocal, @NonNull Config config)293 Injector(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal, 294 @NonNull Config config) { 295 mContext = context; 296 mArtManagerLocal = artManagerLocal; 297 mConfig = config; 298 299 // Call the getters for various dependencies, to ensure correct initialization order. 300 getPackageManagerLocal(); 301 getJobScheduler(); 302 } 303 304 @NonNull getArtManagerLocal()305 public ArtManagerLocal getArtManagerLocal() { 306 return mArtManagerLocal; 307 } 308 309 @NonNull getPackageManagerLocal()310 public PackageManagerLocal getPackageManagerLocal() { 311 return Objects.requireNonNull( 312 LocalManagerRegistry.getManager(PackageManagerLocal.class)); 313 } 314 315 @NonNull getConfig()316 public Config getConfig() { 317 return mConfig; 318 } 319 320 @NonNull getJobScheduler()321 public JobScheduler getJobScheduler() { 322 return Objects.requireNonNull(mContext.getSystemService(JobScheduler.class)); 323 } 324 } 325 } 326