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