• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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