• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 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.compos;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.app.job.JobInfo;
22 import android.app.job.JobParameters;
23 import android.app.job.JobScheduler;
24 import android.app.job.JobService;
25 import android.content.ComponentName;
26 import android.os.IBinder;
27 import android.os.RemoteException;
28 import android.os.ServiceManager;
29 import android.system.composd.ICompilationTask;
30 import android.system.composd.ICompilationTaskCallback;
31 import android.system.composd.IIsolatedCompilationService;
32 import android.util.Log;
33 
34 import com.android.server.compos.IsolatedCompilationMetrics.CompilationResult;
35 
36 import java.util.NoSuchElementException;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.atomic.AtomicReference;
39 
40 /**
41  * A job scheduler service responsible for performing Isolated Compilation when scheduled.
42  *
43  * @hide
44  */
45 public class IsolatedCompilationJobService extends JobService {
46     private static final String TAG = IsolatedCompilationJobService.class.getName();
47     private static final int STAGED_APEX_JOB_ID = 5132251;
48 
49     private final AtomicReference<CompilationJob> mCurrentJob = new AtomicReference<>();
50 
scheduleStagedApexJob(JobScheduler scheduler)51     static void scheduleStagedApexJob(JobScheduler scheduler) {
52         ComponentName serviceName =
53                 new ComponentName("android", IsolatedCompilationJobService.class.getName());
54 
55         int result =
56                 scheduler.schedule(
57                         new JobInfo.Builder(STAGED_APEX_JOB_ID, serviceName)
58                                 // Wait in case more APEXes are staged
59                                 .setMinimumLatency(TimeUnit.MINUTES.toMillis(60))
60                                 // We consume CPU, power, and storage
61                                 .setRequiresDeviceIdle(true)
62                                 .setRequiresCharging(true)
63                                 .setRequiresStorageNotLow(true)
64                                 .build());
65         if (result == JobScheduler.RESULT_SUCCESS) {
66             IsolatedCompilationMetrics.onCompilationScheduled(
67                     IsolatedCompilationMetrics.SCHEDULING_SUCCESS);
68         } else {
69             IsolatedCompilationMetrics.onCompilationScheduled(
70                     IsolatedCompilationMetrics.SCHEDULING_FAILURE);
71             Log.e(TAG, "Failed to schedule staged APEX job");
72         }
73     }
74 
isStagedApexJobScheduled(JobScheduler scheduler)75     static boolean isStagedApexJobScheduled(JobScheduler scheduler) {
76         return scheduler.getPendingJob(STAGED_APEX_JOB_ID) != null;
77     }
78 
79     @Override
onStartJob(JobParameters params)80     public boolean onStartJob(JobParameters params) {
81         Log.i(TAG, "Starting job");
82 
83         // This function (and onStopJob) are only ever called on the main thread, so we don't have
84         // to worry about two starts at once, or start and stop happening at once. But onCompletion
85         // can be called on any thread, so we need to be careful with that.
86 
87         CompilationJob oldJob = mCurrentJob.get();
88         if (oldJob != null) {
89             // We're already running a job, give up on this one
90             Log.w(TAG, "Another job is in progress, skipping");
91             return false; // Already finished
92         }
93 
94         IsolatedCompilationMetrics metrics = new IsolatedCompilationMetrics();
95 
96         CompilationJob newJob =
97                 new CompilationJob(
98                         IsolatedCompilationJobService.this::onCompletion, params, metrics);
99         mCurrentJob.set(newJob);
100 
101         // This can take some time - we need to start up a VM - so we do it on a separate
102         // thread. This thread exits as soon as the compilation Task has been started (or
103         // there's a failure), and then compilation continues in composd and the VM.
104         new Thread("IsolatedCompilationJob_starter") {
105             @Override
106             public void run() {
107                 try {
108                     newJob.start();
109                 } catch (RuntimeException e) {
110                     Log.e(TAG, "Starting CompilationJob failed", e);
111                     metrics.onCompilationEnded(IsolatedCompilationMetrics.RESULT_FAILED_TO_START);
112                     mCurrentJob.set(null);
113                     newJob.stop(); // Just in case it managed to start before failure
114                     jobFinished(params, /* wantReschedule= */ false);
115                 }
116             }
117         }.start();
118         return true; // Job is running in the background
119     }
120 
121     @Override
onStopJob(JobParameters params)122     public boolean onStopJob(JobParameters params) {
123         CompilationJob job = mCurrentJob.getAndSet(null);
124         if (job == null) {
125             return false; // No need to reschedule, we'd finished
126         } else {
127             job.stop();
128             return true; // We didn't get to finish, please re-schedule
129         }
130     }
131 
onCompletion(JobParameters params, boolean succeeded)132     void onCompletion(JobParameters params, boolean succeeded) {
133         Log.i(TAG, "onCompletion, succeeded=" + succeeded);
134 
135         CompilationJob job = mCurrentJob.getAndSet(null);
136         if (job == null) {
137             // No need to call jobFinished if we've been told to stop.
138             return;
139         }
140         // On success we don't need to reschedule.
141         // On failure we could reschedule, but that could just use a lot of resources and still
142         // fail; instead we just let odsign do compilation on reboot if necessary.
143         jobFinished(params, /* wantReschedule= */ false);
144     }
145 
146     interface CompilationCallback {
onCompletion(JobParameters params, boolean succeeded)147         void onCompletion(JobParameters params, boolean succeeded);
148     }
149 
150     static class CompilationJob extends ICompilationTaskCallback.Stub
151             implements IBinder.DeathRecipient {
152         private final IsolatedCompilationMetrics mMetrics;
153         private final AtomicReference<ICompilationTask> mTask = new AtomicReference<>();
154         private final CompilationCallback mCallback;
155         private final JobParameters mParams;
156         private volatile boolean mStopRequested = false;
157 
CompilationJob( CompilationCallback callback, JobParameters params, IsolatedCompilationMetrics metrics)158         CompilationJob(
159                 CompilationCallback callback,
160                 JobParameters params,
161                 IsolatedCompilationMetrics metrics) {
162             mCallback = requireNonNull(callback);
163             mParams = params;
164             mMetrics = requireNonNull(metrics);
165         }
166 
start()167         void start() {
168             IBinder binder = ServiceManager.waitForService("android.system.composd");
169             IIsolatedCompilationService composd =
170                     IIsolatedCompilationService.Stub.asInterface(binder);
171 
172             if (composd == null) {
173                 throw new IllegalStateException("Unable to find composd service");
174             }
175 
176             try {
177                 ICompilationTask composTask = composd.startStagedApexCompile(this, "microdroid");
178                 mMetrics.onCompilationStarted();
179                 mTask.set(composTask);
180                 composTask.asBinder().linkToDeath(this, 0);
181             } catch (RemoteException e) {
182                 throw e.rethrowAsRuntimeException();
183             }
184 
185             if (mStopRequested) {
186                 // We were asked to stop while we were starting the task. We need to
187                 // cancel it now, since we couldn't before.
188                 cancelTask();
189             }
190         }
191 
stop()192         void stop() {
193             mStopRequested = true;
194             cancelTask();
195         }
196 
cancelTask()197         private void cancelTask() {
198             ICompilationTask task = mTask.getAndSet(null);
199             if (task == null) {
200                 return;
201             }
202 
203             Log.i(TAG, "Cancelling task");
204             try {
205                 task.cancel();
206             } catch (RuntimeException | RemoteException e) {
207                 // If canceling failed we'll assume it means that the task has already failed;
208                 // there's nothing else we can do anyway.
209                 Log.w(TAG, "Failed to cancel CompilationTask", e);
210             }
211 
212             mMetrics.onCompilationJobCanceled(mParams.getStopReason());
213             try {
214                 task.asBinder().unlinkToDeath(this, 0);
215             } catch (NoSuchElementException e) {
216                 // Harmless
217             }
218         }
219 
220         @Override
binderDied()221         public void binderDied() {
222             onCompletion(false, IsolatedCompilationMetrics.RESULT_COMPOSD_DIED);
223         }
224 
225         @Override
onSuccess()226         public void onSuccess() {
227             onCompletion(true, IsolatedCompilationMetrics.RESULT_SUCCESS);
228         }
229 
230         @Override
onFailure(byte reason, String message)231         public void onFailure(byte reason, String message) {
232             int result;
233             switch (reason) {
234                 case ICompilationTaskCallback.FailureReason.CompilationFailed:
235                     result = IsolatedCompilationMetrics.RESULT_COMPILATION_FAILED;
236                     break;
237 
238                 case ICompilationTaskCallback.FailureReason.UnexpectedCompilationResult:
239                     result = IsolatedCompilationMetrics.RESULT_UNEXPECTED_COMPILATION_RESULT;
240                     break;
241 
242                 case ICompilationTaskCallback.FailureReason.FailedToEnableFsverity:
243                     result = IsolatedCompilationMetrics.RESULT_FAILED_TO_ENABLE_FSVERITY;
244                     break;
245 
246                 default:
247                     result = IsolatedCompilationMetrics.RESULT_UNKNOWN_FAILURE;
248                     break;
249             }
250             Log.w(TAG, "Compilation failed: " + message);
251             onCompletion(false, result);
252         }
253 
onCompletion(boolean succeeded, @CompilationResult int result)254         private void onCompletion(boolean succeeded, @CompilationResult int result) {
255             ICompilationTask task = mTask.getAndSet(null);
256             if (task != null) {
257                 mMetrics.onCompilationEnded(result);
258                 mCallback.onCompletion(mParams, succeeded);
259                 try {
260                     task.asBinder().unlinkToDeath(this, 0);
261                 } catch (NoSuchElementException e) {
262                     // Harmless
263                 }
264             }
265         }
266     }
267 }
268