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.BatchDexoptPass; 21 import static com.android.server.art.model.ArtFlags.ScheduleStatus; 22 import static com.android.server.art.model.Config.Callback; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.job.JobInfo; 27 import android.app.job.JobParameters; 28 import android.app.job.JobScheduler; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.os.Build; 32 import android.os.CancellationSignal; 33 import android.os.SystemClock; 34 import android.os.SystemProperties; 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.ArtServiceJobInterface; 43 import com.android.server.art.model.Config; 44 import com.android.server.art.model.DexoptResult; 45 import com.android.server.art.model.OperationProgress; 46 import com.android.server.pm.PackageManagerLocal; 47 48 import com.google.auto.value.AutoValue; 49 50 import java.util.Collections; 51 import java.util.HashMap; 52 import java.util.Map; 53 import java.util.Objects; 54 import java.util.Optional; 55 import java.util.concurrent.CompletableFuture; 56 import java.util.concurrent.TimeUnit; 57 import java.util.function.Consumer; 58 59 /** @hide */ 60 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 61 public class BackgroundDexoptJob implements ArtServiceJobInterface { 62 /** 63 * "android" is the package name for a <service> declared in 64 * frameworks/base/core/res/AndroidManifest.xml 65 */ 66 private static final String JOB_PKG_NAME = Utils.PLATFORM_PACKAGE_NAME; 67 /** An arbitrary number. Must be unique among all jobs owned by the system uid. */ 68 public static final int JOB_ID = 27873780; 69 70 @VisibleForTesting public static final long JOB_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); 71 72 @NonNull private final Injector mInjector; 73 74 @GuardedBy("this") @Nullable private CompletableFuture<Result> mRunningJob = null; 75 @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null; 76 @GuardedBy("this") @NonNull private Optional<Integer> mLastStopReason = Optional.empty(); 77 BackgroundDexoptJob(@onNull Context context, @NonNull ArtManagerLocal artManagerLocal, @NonNull Config config)78 public BackgroundDexoptJob(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal, 79 @NonNull Config config) { 80 this(new Injector(context, artManagerLocal, config)); 81 } 82 83 @VisibleForTesting BackgroundDexoptJob(@onNull Injector injector)84 public BackgroundDexoptJob(@NonNull Injector injector) { 85 mInjector = injector; 86 } 87 88 /** Handles {@link BackgroundDexoptJobService#onStartJob(JobParameters)}. */ 89 @Override onStartJob( @onNull BackgroundDexoptJobService jobService, @NonNull JobParameters params)90 public boolean onStartJob( 91 @NonNull BackgroundDexoptJobService jobService, @NonNull JobParameters params) { 92 start().thenAcceptAsync(result -> { 93 try { 94 writeStats(result); 95 } catch (RuntimeException e) { 96 // Not expected. Log wtf to surface it. 97 AsLog.wtf("Failed to write stats", e); 98 } 99 100 // This is a periodic job, where the interval is specified in the `JobInfo`. "true" 101 // means to execute again in the same interval with the default retry policy, while 102 // "false" means not to execute again in the same interval but to execute again in the 103 // next interval. 104 // This call will be ignored if `onStopJob` is called. 105 boolean wantsReschedule = 106 result instanceof CompletedResult && ((CompletedResult) result).isCancelled(); 107 jobService.jobFinished(params, wantsReschedule); 108 }); 109 // "true" means the job will continue running until `jobFinished` is called. 110 return true; 111 } 112 113 /** Handles {@link BackgroundDexoptJobService#onStopJob(JobParameters)}. */ 114 @Override onStopJob(@onNull JobParameters params)115 public boolean onStopJob(@NonNull JobParameters params) { 116 synchronized (this) { 117 mLastStopReason = Optional.of(params.getStopReason()); 118 } 119 cancel(); 120 // "true" means to execute again in the same interval with the default retry policy. 121 return true; 122 } 123 124 /** Handles {@link ArtManagerLocal#scheduleBackgroundDexoptJob()}. */ schedule()125 public @ScheduleStatus int schedule() { 126 if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { 127 throw new IllegalStateException("This job cannot be scheduled"); 128 } 129 130 if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) { 131 AsLog.i("Job is disabled by system property 'pm.dexopt.disable_bg_dexopt'"); 132 return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP; 133 } 134 135 JobInfo.Builder builder = 136 new JobInfo 137 .Builder(JOB_ID, 138 new ComponentName( 139 JOB_PKG_NAME, BackgroundDexoptJobService.class.getName())) 140 .setPeriodic(JOB_INTERVAL_MS) 141 .setRequiresDeviceIdle(true) 142 .setRequiresCharging(true) 143 .setRequiresBatteryNotLow(true); 144 145 Callback<ScheduleBackgroundDexoptJobCallback, Void> callback = 146 mInjector.getConfig().getScheduleBackgroundDexoptJobCallback(); 147 if (callback != null) { 148 Utils.executeAndWait( 149 callback.executor(), () -> { callback.get().onOverrideJobInfo(builder); }); 150 } 151 152 JobInfo info = builder.build(); 153 if (info.isRequireStorageNotLow()) { 154 // See the javadoc of 155 // `ArtManagerLocal.ScheduleBackgroundDexoptJobCallback.onOverrideJobInfo` for details. 156 throw new IllegalStateException("'setRequiresStorageNotLow' must not be set"); 157 } 158 159 return mInjector.getJobScheduler().schedule(info) == JobScheduler.RESULT_SUCCESS 160 ? ArtFlags.SCHEDULE_SUCCESS 161 : ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE; 162 } 163 164 /** Handles {@link ArtManagerLocal#unscheduleBackgroundDexoptJob()}. */ unschedule()165 public void unschedule() { 166 if (this != BackgroundDexoptJobService.getJob(JOB_ID)) { 167 throw new IllegalStateException("This job cannot be unscheduled"); 168 } 169 170 mInjector.getJobScheduler().cancel(JOB_ID); 171 } 172 173 @NonNull start()174 public synchronized CompletableFuture<Result> start() { 175 if (mRunningJob != null) { 176 AsLog.i("Job is already running"); 177 return mRunningJob; 178 } 179 180 mCancellationSignal = new CancellationSignal(); 181 mLastStopReason = Optional.empty(); 182 mRunningJob = new CompletableFuture().supplyAsync(() -> { 183 try (var tracing = new Utils.TracingWithTimingLogging(AsLog.getTag(), "jobExecution")) { 184 return run(mCancellationSignal); 185 } catch (RuntimeException e) { 186 AsLog.wtf("Fatal error", e); 187 return new FatalErrorResult(); 188 } finally { 189 synchronized (this) { 190 mRunningJob = null; 191 mCancellationSignal = null; 192 } 193 } 194 }); 195 return mRunningJob; 196 } 197 cancel()198 public synchronized void cancel() { 199 if (mRunningJob == null) { 200 AsLog.i("Job is not running"); 201 return; 202 } 203 204 mCancellationSignal.cancel(); 205 AsLog.i("Job cancelled"); 206 } 207 208 @Nullable get()209 public synchronized CompletableFuture<Result> get() { 210 return mRunningJob; 211 } 212 213 @NonNull run(@onNull CancellationSignal cancellationSignal)214 private CompletedResult run(@NonNull CancellationSignal cancellationSignal) { 215 // Create callbacks to time each pass. 216 Map<Integer, Long> startTimeMsByPass = new HashMap<>(); 217 Map<Integer, Long> durationMsByPass = new HashMap<>(); 218 Map<Integer, Consumer<OperationProgress>> progressCallbacks = new HashMap<>(); 219 for (@BatchDexoptPass int pass : ArtFlags.BATCH_DEXOPT_PASSES) { 220 progressCallbacks.put(pass, progress -> { 221 if (progress.getTotal() == 0) { 222 durationMsByPass.put(pass, 0l); 223 } else if (progress.getCurrent() == 0) { 224 startTimeMsByPass.put(pass, SystemClock.uptimeMillis()); 225 } else if (progress.getCurrent() == progress.getTotal()) { 226 durationMsByPass.put( 227 pass, SystemClock.uptimeMillis() - startTimeMsByPass.get(pass)); 228 } 229 }); 230 } 231 232 Map<Integer, DexoptResult> dexoptResultByPass; 233 try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) { 234 dexoptResultByPass = mInjector.getArtManagerLocal().dexoptPackages(snapshot, 235 ReasonMapping.REASON_BG_DEXOPT, cancellationSignal, Runnable::run, 236 progressCallbacks); 237 238 // For simplicity, we don't support cancelling the following operation in the middle. 239 // This is fine because it typically takes only a few seconds. 240 if (!cancellationSignal.isCanceled()) { 241 // We do the cleanup after dexopt so that it doesn't affect the `getSizeBeforeBytes` 242 // field in the result that we send to callbacks. Admittedly, this will cause us to 243 // lose some chance to dexopt when the storage is very low, but it's fine because we 244 // can still dexopt in the next run. 245 long freedBytes = mInjector.getArtManagerLocal().cleanup(snapshot); 246 AsLog.i(String.format("Freed %d bytes", freedBytes)); 247 } 248 } 249 return CompletedResult.create(dexoptResultByPass, durationMsByPass); 250 } 251 writeStats(@onNull Result result)252 private void writeStats(@NonNull Result result) { 253 Optional<Integer> stopReason; 254 synchronized (this) { 255 stopReason = mLastStopReason; 256 } 257 if (result instanceof CompletedResult completedResult) { 258 BackgroundDexoptJobStatsReporter.reportSuccess(completedResult, stopReason); 259 } else if (result instanceof FatalErrorResult) { 260 BackgroundDexoptJobStatsReporter.reportFailure(); 261 } 262 } 263 264 static abstract class Result {} 265 static class FatalErrorResult extends Result {} 266 267 @AutoValue 268 @SuppressWarnings("AutoValueImmutableFields") // Can't use ImmutableMap because it's in Guava. 269 static abstract class CompletedResult extends Result { dexoptResultByPass()270 abstract @NonNull Map<Integer, DexoptResult> dexoptResultByPass(); durationMsByPass()271 abstract @NonNull Map<Integer, Long> durationMsByPass(); 272 273 @NonNull create(@onNull Map<Integer, DexoptResult> dexoptResultByPass, @NonNull Map<Integer, Long> durationMsByPass)274 static CompletedResult create(@NonNull Map<Integer, DexoptResult> dexoptResultByPass, 275 @NonNull Map<Integer, Long> durationMsByPass) { 276 return new AutoValue_BackgroundDexoptJob_CompletedResult( 277 Collections.unmodifiableMap(dexoptResultByPass), 278 Collections.unmodifiableMap(durationMsByPass)); 279 } 280 isCancelled()281 public boolean isCancelled() { 282 return dexoptResultByPass().values().stream().anyMatch( 283 result -> result.getFinalStatus() == DexoptResult.DEXOPT_CANCELLED); 284 } 285 } 286 287 /** 288 * Injector pattern for testing purpose. 289 * 290 * @hide 291 */ 292 @VisibleForTesting 293 public static class Injector { 294 @NonNull private final Context mContext; 295 @NonNull private final ArtManagerLocal mArtManagerLocal; 296 @NonNull private final Config mConfig; 297 Injector(@onNull Context context, @NonNull ArtManagerLocal artManagerLocal, @NonNull Config config)298 Injector(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal, 299 @NonNull Config config) { 300 mContext = context; 301 mArtManagerLocal = artManagerLocal; 302 mConfig = config; 303 304 // Call the getters for various dependencies, to ensure correct initialization order. 305 getPackageManagerLocal(); 306 getJobScheduler(); 307 } 308 309 @NonNull getArtManagerLocal()310 public ArtManagerLocal getArtManagerLocal() { 311 return mArtManagerLocal; 312 } 313 314 @NonNull getPackageManagerLocal()315 public PackageManagerLocal getPackageManagerLocal() { 316 return Objects.requireNonNull( 317 LocalManagerRegistry.getManager(PackageManagerLocal.class)); 318 } 319 320 @NonNull getConfig()321 public Config getConfig() { 322 return mConfig; 323 } 324 325 @NonNull getJobScheduler()326 public JobScheduler getJobScheduler() { 327 return Objects.requireNonNull(mContext.getSystemService(JobScheduler.class)); 328 } 329 } 330 } 331