• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.prereboot;
18 
19 import static com.android.server.art.prereboot.PreRebootDriver.PreRebootResult;
20 import static com.android.server.art.proto.PreRebootStats.JobRun;
21 import static com.android.server.art.proto.PreRebootStats.JobType;
22 import static com.android.server.art.proto.PreRebootStats.Status;
23 
24 import android.annotation.NonNull;
25 import android.os.Build;
26 
27 import androidx.annotation.RequiresApi;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.server.LocalManagerRegistry;
31 import com.android.server.art.ArtManagerLocal;
32 import com.android.server.art.ArtStatsLog;
33 import com.android.server.art.ArtdRefCache;
34 import com.android.server.art.AsLog;
35 import com.android.server.art.ReasonMapping;
36 import com.android.server.art.Utils;
37 import com.android.server.art.model.DexoptStatus;
38 import com.android.server.art.proto.PreRebootStats;
39 import com.android.server.pm.PackageManagerLocal;
40 
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47 import java.nio.file.Files;
48 import java.nio.file.StandardCopyOption;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Objects;
52 import java.util.Set;
53 import java.util.concurrent.CompletableFuture;
54 import java.util.function.Function;
55 
56 /**
57  * A helper class to report the Pre-reboot Dexopt metrics to StatsD.
58  *
59  * This class is not thread-safe.
60  *
61  * During Pre-reboot Dexopt, both the old version and the new version of this code is run. The old
62  * version writes to disk first, and the new version writes to disk later. After reboot, the new
63  * version loads from disk.
64  *
65  * @hide
66  */
67 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
68 public class PreRebootStatsReporter {
69     private static final String FILENAME = "/data/system/pre-reboot-stats.pb";
70 
71     @NonNull private final Injector mInjector;
72 
PreRebootStatsReporter()73     public PreRebootStatsReporter() {
74         this(new Injector());
75     }
76 
77     /** @hide */
78     @VisibleForTesting
PreRebootStatsReporter(@onNull Injector injector)79     public PreRebootStatsReporter(@NonNull Injector injector) {
80         mInjector = injector;
81     }
82 
recordJobScheduled(boolean isAsync, boolean isOtaUpdate)83     public void recordJobScheduled(boolean isAsync, boolean isOtaUpdate) {
84         PreRebootStats.Builder statsBuilder = PreRebootStats.newBuilder();
85         statsBuilder.setStatus(Status.STATUS_SCHEDULED)
86                 .setJobType(isOtaUpdate ? JobType.JOB_TYPE_OTA : JobType.JOB_TYPE_MAINLINE);
87         // Omit job_scheduled_timestamp_millis to indicate a synchronous job.
88         if (isAsync) {
89             statsBuilder.setJobScheduledTimestampMillis(mInjector.getCurrentTimeMillis());
90         }
91         save(statsBuilder);
92     }
93 
recordJobNotScheduled(@onNull Status reason, boolean isOtaUpdate)94     public void recordJobNotScheduled(@NonNull Status reason, boolean isOtaUpdate) {
95         Utils.check(reason == Status.STATUS_NOT_SCHEDULED_DISABLED
96                 || reason == Status.STATUS_NOT_SCHEDULED_JOB_SCHEDULER);
97         PreRebootStats.Builder statsBuilder = PreRebootStats.newBuilder();
98         statsBuilder.setStatus(reason).setJobType(
99                 isOtaUpdate ? JobType.JOB_TYPE_OTA : JobType.JOB_TYPE_MAINLINE);
100         save(statsBuilder);
101     }
102 
recordJobStarted()103     public void recordJobStarted() {
104         PreRebootStats.Builder statsBuilder = load();
105         if (statsBuilder.getStatus() == Status.STATUS_UNKNOWN) {
106             // Failed to load, the error is already logged.
107             return;
108         }
109 
110         JobRun.Builder runBuilder =
111                 JobRun.newBuilder().setJobStartedTimestampMillis(mInjector.getCurrentTimeMillis());
112         statsBuilder.setStatus(Status.STATUS_STARTED)
113                 .addJobRuns(runBuilder)
114                 .setSkippedPackageCount(0)
115                 .setOptimizedPackageCount(0)
116                 .setFailedPackageCount(0)
117                 .setTotalPackageCount(0)
118                 // Some packages may have artifacts from a previously cancelled job, but we count
119                 // from scratch for simplicity.
120                 .setPackagesWithArtifactsBeforeRebootCount(0);
121         save(statsBuilder);
122     }
123 
124     public class ProgressSession {
125         private @NonNull PreRebootStats.Builder mStatsBuilder = load();
126 
recordProgress(int skippedPackageCount, int optimizedPackageCount, int failedPackageCount, int totalPackageCount, int packagesWithArtifactsBeforeRebootCount)127         public void recordProgress(int skippedPackageCount, int optimizedPackageCount,
128                 int failedPackageCount, int totalPackageCount,
129                 int packagesWithArtifactsBeforeRebootCount) {
130             if (mStatsBuilder.getStatus() == Status.STATUS_UNKNOWN) {
131                 // Failed to load, the error is already logged.
132                 return;
133             }
134 
135             mStatsBuilder.setSkippedPackageCount(skippedPackageCount)
136                     .setOptimizedPackageCount(optimizedPackageCount)
137                     .setFailedPackageCount(failedPackageCount)
138                     .setTotalPackageCount(totalPackageCount)
139                     .setPackagesWithArtifactsBeforeRebootCount(
140                             packagesWithArtifactsBeforeRebootCount);
141             save(mStatsBuilder);
142         }
143     }
144 
recordJobEnded(@onNull PreRebootResult result)145     public void recordJobEnded(@NonNull PreRebootResult result) {
146         PreRebootStats.Builder statsBuilder = load();
147         if (statsBuilder.getStatus() == Status.STATUS_UNKNOWN) {
148             // Failed to load, the error is already logged.
149             return;
150         }
151 
152         List<JobRun> jobRuns = statsBuilder.getJobRunsList();
153         Utils.check(jobRuns.size() > 0);
154         JobRun lastRun = jobRuns.get(jobRuns.size() - 1);
155         Utils.check(lastRun.getJobEndedTimestampMillis() == 0);
156 
157         JobRun.Builder runBuilder = JobRun.newBuilder(lastRun).setJobEndedTimestampMillis(
158                 mInjector.getCurrentTimeMillis());
159 
160         Status status;
161         if (result.success()) {
162             // The job is cancelled if it hasn't done package scanning (total package count is 0),
163             // or it's interrupted in the middle of package processing (package counts don't add up
164             // to the total).
165             // TODO(b/336239721): Move this logic to the server.
166             if (statsBuilder.getTotalPackageCount() > 0
167                     && (statsBuilder.getOptimizedPackageCount()
168                                + statsBuilder.getFailedPackageCount()
169                                + statsBuilder.getSkippedPackageCount())
170                             == statsBuilder.getTotalPackageCount()) {
171                 status = Status.STATUS_FINISHED;
172             } else {
173                 status = Status.STATUS_CANCELLED;
174             }
175         } else {
176             if (result.systemRequirementCheckFailed()) {
177                 status = Status.STATUS_ABORTED_SYSTEM_REQUIREMENTS;
178             } else {
179                 status = Status.STATUS_FAILED;
180             }
181         }
182 
183         statsBuilder.setStatus(status).setJobRuns(jobRuns.size() - 1, runBuilder);
184         save(statsBuilder);
185     }
186 
187     public class AfterRebootSession {
188         private @NonNull Set<String> mPackagesWithArtifacts = new HashSet<>();
189 
recordPackageWithArtifacts(@onNull String packageName)190         public void recordPackageWithArtifacts(@NonNull String packageName) {
191             mPackagesWithArtifacts.add(packageName);
192         }
193 
reportAsync()194         public void reportAsync() {
195             new CompletableFuture().runAsync(this::report).exceptionally(t -> {
196                 AsLog.e("Failed to report stats", t);
197                 return null;
198             });
199         }
200 
201         @VisibleForTesting
report()202         public void report() {
203             PreRebootStats.Builder statsBuilder = load();
204             delete();
205 
206             if (statsBuilder.getStatus() == Status.STATUS_UNKNOWN) {
207                 // Job not scheduled, probably because Pre-reboot Dexopt is not enabled.
208                 return;
209             }
210 
211             ArtManagerLocal artManagerLocal = mInjector.getArtManagerLocal();
212 
213             // This takes some time (~3ms per package). It probably fine because we are running
214             // asynchronously. Consider removing this in the future.
215             int packagesWithArtifactsUsableCount;
216             try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot();
217                     var pin = mInjector.createArtdPin()) {
218                 packagesWithArtifactsUsableCount =
219                         (int) mPackagesWithArtifacts.stream()
220                                 .map(packageName
221                                         -> artManagerLocal.getDexoptStatus(snapshot, packageName))
222                                 .filter(status -> hasUsablePreRebootArtifacts(status))
223                                 .count();
224             }
225 
226             List<JobRun> jobRuns = statsBuilder.getJobRunsList();
227             // The total duration of all runs, or -1 if any run didn't end.
228             long jobDurationMs = 0;
229             for (JobRun run : jobRuns) {
230                 if (run.getJobEndedTimestampMillis() == 0) {
231                     jobDurationMs = -1;
232                     break;
233                 }
234                 jobDurationMs +=
235                         run.getJobEndedTimestampMillis() - run.getJobStartedTimestampMillis();
236             }
237             if (jobRuns.size() == 0) {
238                 jobDurationMs = -1;
239             }
240             long jobLatencyMs =
241                     (jobRuns.size() > 0 && statsBuilder.getJobScheduledTimestampMillis() > 0)
242                     ? (jobRuns.get(0).getJobStartedTimestampMillis()
243                               - statsBuilder.getJobScheduledTimestampMillis())
244                     : -1;
245 
246             mInjector.writeStats(ArtStatsLog.PREREBOOT_DEXOPT_JOB_ENDED,
247                     getStatusForStatsd(statsBuilder.getStatus()),
248                     statsBuilder.getOptimizedPackageCount(), statsBuilder.getFailedPackageCount(),
249                     statsBuilder.getSkippedPackageCount(), statsBuilder.getTotalPackageCount(),
250                     jobDurationMs, jobLatencyMs, mPackagesWithArtifacts.size(),
251                     packagesWithArtifactsUsableCount, jobRuns.size(),
252                     statsBuilder.getPackagesWithArtifactsBeforeRebootCount(),
253                     getJobTypeForStatsd(statsBuilder.getJobType()));
254         }
255     }
256 
getStatusForStatsd(@onNull Status status)257     private int getStatusForStatsd(@NonNull Status status) {
258         switch (status) {
259             case STATUS_UNKNOWN:
260                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN;
261             case STATUS_SCHEDULED:
262                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_SCHEDULED;
263             case STATUS_STARTED:
264                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_STARTED;
265             case STATUS_FINISHED:
266                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FINISHED;
267             case STATUS_FAILED:
268                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_FAILED;
269             case STATUS_CANCELLED:
270                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_CANCELLED;
271             case STATUS_ABORTED_SYSTEM_REQUIREMENTS:
272                 return ArtStatsLog
273                         .PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORTED_SYSTEM_REQUIREMENTS;
274             case STATUS_NOT_SCHEDULED_DISABLED:
275                 return ArtStatsLog
276                         .PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_NOT_SCHEDULED_DISABLED;
277             case STATUS_NOT_SCHEDULED_JOB_SCHEDULER:
278                 return ArtStatsLog
279                         .PRE_REBOOT_DEXOPT_JOB_ENDED__STATUS__STATUS_NOT_SCHEDULED_JOB_SCHEDULER;
280             default:
281                 throw new IllegalStateException("Unknown status: " + status.getNumber());
282         }
283     }
284 
getJobTypeForStatsd(@onNull JobType jobType)285     private int getJobTypeForStatsd(@NonNull JobType jobType) {
286         switch (jobType) {
287             case JOB_TYPE_UNKNOWN:
288                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_UNKNOWN;
289             case JOB_TYPE_OTA:
290                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_OTA;
291             case JOB_TYPE_MAINLINE:
292                 return ArtStatsLog.PRE_REBOOT_DEXOPT_JOB_ENDED__JOB_TYPE__JOB_TYPE_MAINLINE;
293             default:
294                 throw new IllegalStateException("Unknown job type: " + jobType.getNumber());
295         }
296     }
297 
hasUsablePreRebootArtifacts(@onNull DexoptStatus status)298     private boolean hasUsablePreRebootArtifacts(@NonNull DexoptStatus status) {
299         // For simplicity, we consider all artifacts of a package usable if we see at least one
300         // `REASON_PRE_REBOOT_DEXOPT` because it's not easy to know which files are committed.
301         return status.getDexContainerFileDexoptStatuses().stream().anyMatch(fileStatus
302                 -> fileStatus.getCompilationReason().equals(
303                         ReasonMapping.REASON_PRE_REBOOT_DEXOPT));
304     }
305 
306     @NonNull
load()307     private PreRebootStats.Builder load() {
308         PreRebootStats.Builder statsBuilder = PreRebootStats.newBuilder();
309         try (InputStream in = new FileInputStream(mInjector.getFilename())) {
310             statsBuilder.mergeFrom(in);
311         } catch (IOException e) {
312             // Nothing else we can do but to start from scratch.
313             AsLog.e("Failed to load pre-reboot stats", e);
314         }
315         return statsBuilder;
316     }
317 
save(@onNull PreRebootStats.Builder statsBuilder)318     private void save(@NonNull PreRebootStats.Builder statsBuilder) {
319         var file = new File(mInjector.getFilename());
320         File tempFile = null;
321         try {
322             tempFile = File.createTempFile(file.getName(), null /* suffix */, file.getParentFile());
323             try (OutputStream out = new FileOutputStream(tempFile.getPath())) {
324                 statsBuilder.build().writeTo(out);
325             }
326             Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING,
327                     StandardCopyOption.ATOMIC_MOVE);
328         } catch (IOException e) {
329             AsLog.e("Failed to save pre-reboot stats", e);
330         } finally {
331             Utils.deleteIfExistsSafe(tempFile);
332         }
333     }
334 
delete()335     public void delete() {
336         Utils.deleteIfExistsSafe(new File(mInjector.getFilename()));
337     }
338 
339     /**
340      * Injector pattern for testing purpose.
341      *
342      * @hide
343      */
344     @VisibleForTesting
345     public static class Injector {
346         @NonNull
getFilename()347         public String getFilename() {
348             return FILENAME;
349         }
350 
getCurrentTimeMillis()351         public long getCurrentTimeMillis() {
352             return System.currentTimeMillis();
353         }
354 
355         @NonNull
getPackageManagerLocal()356         public PackageManagerLocal getPackageManagerLocal() {
357             return Objects.requireNonNull(
358                     LocalManagerRegistry.getManager(PackageManagerLocal.class));
359         }
360 
361         @NonNull
getArtManagerLocal()362         public ArtManagerLocal getArtManagerLocal() {
363             return Objects.requireNonNull(LocalManagerRegistry.getManager(ArtManagerLocal.class));
364         }
365 
366         @NonNull
createArtdPin()367         public ArtdRefCache.Pin createArtdPin() {
368             return ArtdRefCache.getInstance().new Pin();
369         }
370 
371         // Wrap the static void method to make it easier to mock. There is no good way to mock a
372         // method that is both void and static, due to the poor design of Mockito API.
writeStats(int code, int status, int optimizedPackageCount, int failedPackageCount, int skippedPackageCount, int totalPackageCount, long jobDurationMillis, long jobLatencyMillis, int packagesWithArtifactsAfterRebootCount, int packagesWithArtifactsUsableAfterRebootCount, int jobRunCount, int packagesWithArtifactsBeforeRebootCount, int jobType)373         public void writeStats(int code, int status, int optimizedPackageCount,
374                 int failedPackageCount, int skippedPackageCount, int totalPackageCount,
375                 long jobDurationMillis, long jobLatencyMillis,
376                 int packagesWithArtifactsAfterRebootCount,
377                 int packagesWithArtifactsUsableAfterRebootCount, int jobRunCount,
378                 int packagesWithArtifactsBeforeRebootCount, int jobType) {
379             ArtStatsLog.write(code, status, optimizedPackageCount, failedPackageCount,
380                     skippedPackageCount, totalPackageCount, jobDurationMillis, jobLatencyMillis,
381                     packagesWithArtifactsAfterRebootCount,
382                     packagesWithArtifactsUsableAfterRebootCount, jobRunCount,
383                     packagesWithArtifactsBeforeRebootCount, jobType);
384         }
385     }
386 }
387