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