Implement ART Serivces GC that cleans up obsolete files.
Bug: 254013425
Test: m test-art-host-gtest-art_artd_tests
Test: atest ArtServiceTests
Test: -
1. adb shell pm art cleanup
2. See files being cleaned up.
3. adb shell pm art cleanup
4. See nothing being cleaned up.
5. adb shell pm art optimize-packages bg-dexopt
6. adb shell pm art cleanup
7. See nothing being cleaned up.
Ignore-AOSP-First: ART Services.
Change-Id: If6a495b58657e007a49863c055d0fbafb4417ce1
diff --git a/artd/artd.cc b/artd/artd.cc
index 5b91879..ffa30f4 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -37,6 +37,7 @@
#include <string_view>
#include <system_error>
#include <type_traits>
+#include <unordered_set>
#include <utility>
#include <vector>
@@ -1012,6 +1013,33 @@
return ScopedAStatus::ok();
}
+ScopedAStatus Artd::cleanup(const std::vector<ProfilePath>& in_profilesToKeep,
+ const std::vector<ArtifactsPath>& in_artifactsToKeep,
+ const std::vector<VdexPath>& in_vdexFilesToKeep,
+ int64_t* _aidl_return) {
+ std::unordered_set<std::string> files_to_keep;
+ for (const ProfilePath& profile : in_profilesToKeep) {
+ files_to_keep.insert(OR_RETURN_FATAL(BuildProfileOrDmPath(profile)));
+ }
+ for (const ArtifactsPath& artifacts : in_artifactsToKeep) {
+ std::string oat_path = OR_RETURN_FATAL(BuildOatPath(artifacts));
+ files_to_keep.insert(OatPathToVdexPath(oat_path));
+ files_to_keep.insert(OatPathToArtPath(oat_path));
+ files_to_keep.insert(std::move(oat_path));
+ }
+ for (const VdexPath& vdex : in_vdexFilesToKeep) {
+ files_to_keep.insert(OR_RETURN_FATAL(BuildVdexPath(vdex)));
+ }
+ *_aidl_return = 0;
+ for (const std::string& file : OR_RETURN_NON_FATAL(ListManagedFiles())) {
+ if (files_to_keep.find(file) == files_to_keep.end()) {
+ LOG(INFO) << "Cleaning up obsolete file '{}'"_format(file);
+ *_aidl_return += GetSizeAndDeleteFile(file);
+ }
+ }
+ return ScopedAStatus::ok();
+}
+
Result<void> Artd::Start() {
ScopedAStatus status = ScopedAStatus::fromStatus(
AServiceManager_registerLazyService(this->asBinder().get(), kServiceName));
diff --git a/artd/artd.h b/artd/artd.h
index fbaaed0..ea52752 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -158,6 +158,12 @@
std::shared_ptr<aidl::com::android::server::art::IArtdCancellationSignal>* _aidl_return)
override;
+ ndk::ScopedAStatus cleanup(
+ const std::vector<aidl::com::android::server::art::ProfilePath>& in_profilesToKeep,
+ const std::vector<aidl::com::android::server::art::ArtifactsPath>& in_artifactsToKeep,
+ const std::vector<aidl::com::android::server::art::VdexPath>& in_vdexFilesToKeep,
+ int64_t* _aidl_return) override;
+
android::base::Result<void> Start();
private:
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 8d03536..2f85887 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -37,6 +37,7 @@
#include <utility>
#include <vector>
+#include "aidl/com/android/server/art/ArtConstants.h"
#include "aidl/com/android/server/art/BnArtd.h"
#include "android-base/collections.h"
#include "android-base/errors.h"
@@ -54,6 +55,7 @@
#include "fmt/format.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include "oat_file.h"
#include "path_utils.h"
#include "profman/profman_result.h"
#include "testing.h"
@@ -63,12 +65,14 @@
namespace artd {
namespace {
+using ::aidl::com::android::server::art::ArtConstants;
using ::aidl::com::android::server::art::ArtdDexoptResult;
using ::aidl::com::android::server::art::ArtifactsPath;
using ::aidl::com::android::server::art::DexMetadataPath;
using ::aidl::com::android::server::art::DexoptOptions;
using ::aidl::com::android::server::art::FileVisibility;
using ::aidl::com::android::server::art::FsPermission;
+using ::aidl::com::android::server::art::GetDexoptStatusResult;
using ::aidl::com::android::server::art::IArtdCancellationSignal;
using ::aidl::com::android::server::art::OutputArtifacts;
using ::aidl::com::android::server::art::OutputProfile;
@@ -331,6 +335,11 @@
std::filesystem::create_directories(android_data_);
setenv("ANDROID_DATA", android_data_.c_str(), /*overwrite=*/1);
+ // Use an arbitrary existing directory as Android expand.
+ android_expand_ = scratch_path_ + "/mnt/expand";
+ std::filesystem::create_directories(android_expand_);
+ setenv("ANDROID_EXPAND", android_expand_.c_str(), /*overwrite=*/1);
+
dex_file_ = scratch_path_ + "/a/b.apk";
isa_ = "arm64";
artifacts_path_ = ArtifactsPath{
@@ -426,9 +435,12 @@
std::string scratch_path_;
std::string art_root_;
std::string android_data_;
+ std::string android_expand_;
MockFunction<android::base::LogFunction> mock_logger_;
ScopedUnsetEnvironmentVariable art_root_env_ = ScopedUnsetEnvironmentVariable("ANDROID_ART_ROOT");
ScopedUnsetEnvironmentVariable android_data_env_ = ScopedUnsetEnvironmentVariable("ANDROID_DATA");
+ ScopedUnsetEnvironmentVariable android_expand_env_ =
+ ScopedUnsetEnvironmentVariable("ANDROID_EXPAND");
MockSystemProperties* mock_props_;
MockExecUtils* mock_exec_utils_;
MockFunction<int(pid_t, int)> mock_kill_;
@@ -483,6 +495,8 @@
}
};
+TEST_F(ArtdTest, ConstantsAreInSync) { EXPECT_EQ(ArtConstants::REASON_VDEX, kReasonVdex); }
+
TEST_F(ArtdTest, isAlive) {
bool result = false;
artd_->isAlive(&result);
@@ -1817,6 +1831,123 @@
CheckContent(output_profile.profilePath.tmpPath, "dump");
}
+TEST_F(ArtdTest, cleanup) {
+ std::vector<std::string> gc_removed_files;
+ std::vector<std::string> gc_kept_files;
+
+ auto CreateGcRemovedFile = [&](const std::string& path) {
+ CreateFile(path);
+ gc_removed_files.push_back(path);
+ };
+
+ auto CreateGcKeptFile = [&](const std::string& path) {
+ CreateFile(path);
+ gc_kept_files.push_back(path);
+ };
+
+ // Unmanaged files.
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/1.odex");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/1.odex");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/1.txt");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.txt");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.tmp");
+
+ // Files to keep.
+ CreateGcKeptFile(android_data_ + "/misc/profiles/cur/1/com.android.foo/primary.prof");
+ CreateGcKeptFile(android_data_ + "/misc/profiles/cur/3/com.android.foo/primary.prof");
+ CreateGcKeptFile(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.dex");
+ CreateGcKeptFile(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.vdex");
+ CreateGcKeptFile(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.art");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.vdex");
+ CreateGcKeptFile(
+ android_expand_ +
+ "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.odex");
+ CreateGcKeptFile(
+ android_expand_ +
+ "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.vdex");
+ CreateGcKeptFile(
+ android_expand_ +
+ "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.art");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.odex");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.vdex");
+ CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.art");
+
+ // Files to remove.
+ CreateGcRemovedFile(android_data_ + "/misc/profiles/ref/com.android.foo/primary.prof");
+ CreateGcRemovedFile(android_data_ + "/misc/profiles/cur/2/com.android.foo/primary.prof");
+ CreateGcRemovedFile(android_data_ + "/misc/profiles/cur/3/com.android.bar/primary.prof");
+ CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/extra.odex");
+ CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/system@app@Bar@Bar.apk@classes.dex");
+ CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/system@app@Bar@Bar.apk@classes.vdex");
+ CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/system@app@Bar@Bar.apk@classes.art");
+ CreateGcRemovedFile(
+ android_expand_ +
+ "/123456-7890/app/~~daewfweaf==/com.android.foo-fjuwidhia==/oat/arm64/base.odex");
+ CreateGcRemovedFile(
+ android_expand_ +
+ "/123456-7890/app/~~daewfweaf==/com.android.foo-fjuwidhia==/oat/arm64/base.vdex");
+ CreateGcRemovedFile(
+ android_expand_ +
+ "/123456-7890/app/~~daewfweaf==/com.android.foo-fjuwidhia==/oat/arm64/base.art");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/1.prof");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/1.prof.123456.tmp");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.odex");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.vdex");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.art");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.odex.123456.tmp");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/2.odex.123456.tmp");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.odex");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.art");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.vdex.123456.tmp");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.odex");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.vdex");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.art");
+ CreateGcRemovedFile(android_data_ +
+ "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.art.123456.tmp");
+ CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.bar/aaa/oat/arm64/1.vdex");
+
+ int64_t aidl_return;
+ ASSERT_TRUE(
+ artd_
+ ->cleanup(
+ {
+ PrimaryCurProfilePath{
+ .userId = 1, .packageName = "com.android.foo", .profileName = "primary"},
+ PrimaryCurProfilePath{
+ .userId = 3, .packageName = "com.android.foo", .profileName = "primary"},
+ },
+ {
+ ArtifactsPath{.dexPath = "/system/app/Foo/Foo.apk",
+ .isa = "arm64",
+ .isInDalvikCache = true},
+ ArtifactsPath{
+ .dexPath =
+ android_expand_ +
+ "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/base.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false},
+ ArtifactsPath{.dexPath = android_data_ + "/user_de/0/com.android.foo/aaa/2.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false},
+ },
+ {
+ VdexPath{ArtifactsPath{
+ .dexPath = android_data_ + "/user_de/0/com.android.foo/aaa/1.apk",
+ .isa = "arm64",
+ .isInDalvikCache = false}},
+ },
+ &aidl_return)
+ .isOk());
+
+ for (const std::string& path : gc_removed_files) {
+ EXPECT_FALSE(std::filesystem::exists(path)) << "'{}' should be removed"_format(path);
+ }
+
+ for (const std::string& path : gc_kept_files) {
+ EXPECT_TRUE(std::filesystem::exists(path)) << "'{}' should be kept"_format(path);
+ }
+}
+
} // namespace
} // namespace artd
} // namespace art
diff --git a/artd/binder/com/android/server/art/ArtConstants.aidl b/artd/binder/com/android/server/art/ArtConstants.aidl
new file mode 100644
index 0000000..e9f702e
--- /dev/null
+++ b/artd/binder/com/android/server/art/ArtConstants.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.art;
+
+/**
+ * Constants used by ART Service Java code that must be kept in sync with those in ART native code.
+ *
+ * @hide
+ */
+parcelable ArtConstants {
+ /**
+ * A special compilation reason to indicate that only the VDEX file is usable. Keep in sync with
+ * {@code kReasonVdex} in art/runtime/oat_file.h.
+ *
+ * This isn't a valid reason to feed into DexoptParams.
+ */
+ const @utf8InCpp String REASON_VDEX = "vdex";
+}
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 5121063..603a40f 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -155,4 +155,18 @@
* Returns a cancellation signal which can be used to cancel {@code dexopt} calls.
*/
com.android.server.art.IArtdCancellationSignal createCancellationSignal();
+
+ /**
+ * Deletes all files that are managed by artd, except those specified in the arguments. Returns
+ * the size of the freed space, in bytes.
+ *
+ * For each entry in `artifactsToKeep`, all three kinds of artifacts (ODEX, VDEX, ART) are
+ * kept. For each entry in `vdexFilesToKeep`, only the VDEX file will be kept. Note that VDEX
+ * files included in `artifactsToKeep` don't have to be listed in `vdexFilesToKeep`.
+ *
+ * Throws fatal errors. Logs and ignores non-fatal errors.
+ */
+ long cleanup(in List<com.android.server.art.ProfilePath> profilesToKeep,
+ in List<com.android.server.art.ArtifactsPath> artifactsToKeep,
+ in List<com.android.server.art.VdexPath> vdexFilesToKeep);
}
diff --git a/artd/file_utils.cc b/artd/file_utils.cc
index 53fe9f9..0ce782a 100644
--- a/artd/file_utils.cc
+++ b/artd/file_utils.cc
@@ -81,6 +81,9 @@
std::error_code ec;
std::filesystem::rename(temp_path_, final_path_, ec);
if (ec) {
+ // If this fails because the temp file doesn't exist, it could be that the file is deleted by
+ // `Artd::cleanup` if that method is run simultaneously. At the time of writing, this should
+ // never happen because `Artd::cleanup` is only called at the end of the backgrond dexopt job.
return Errorf(
"Failed to move new file '{}' to path '{}': {}", temp_path_, final_path_, ec.message());
}
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 07d77aa..07e7598 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -16,6 +16,7 @@
package com.android.server.art;
+import static com.android.server.art.DexUseManagerLocal.DetailedSecondaryDexInfo;
import static com.android.server.art.DexUseManagerLocal.SecondaryDexInfo;
import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
@@ -56,6 +57,7 @@
import com.android.server.art.model.BatchDexoptParams;
import com.android.server.art.model.Config;
import com.android.server.art.model.DeleteResult;
+import com.android.server.art.model.DetailedDexInfo;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
import com.android.server.art.model.DexoptStatus;
@@ -65,6 +67,8 @@
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
+import dalvik.system.DexFile;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -815,6 +819,108 @@
}
/**
+ * Cleans up obsolete profiles and artifacts.
+ *
+ * This is done in a mark-and-sweep approach.
+ *
+ * @hide
+ */
+ public void cleanup(@NonNull PackageManagerLocal.FilteredSnapshot snapshot) {
+ try {
+ // For every primary dex container file or secondary dex container file of every app, if
+ // it has code, we keep the following types of files:
+ // - The reference profile and the current profiles, regardless of the hibernation state
+ // of the app.
+ // - The dexopt artifacts, if they are up-to-date and the app is not hibernating.
+ // - Only the VDEX part of the dexopt artifacts, if the dexopt artifacts are outdated
+ // but the VDEX part is still usable and the app is not hibernating.
+ List<ProfilePath> profilesToKeep = new ArrayList<>();
+ List<ArtifactsPath> artifactsToKeep = new ArrayList<>();
+ List<VdexPath> vdexFilesToKeep = new ArrayList<>();
+
+ for (PackageState pkgState : snapshot.getPackageStates().values()) {
+ if (!Utils.canDexoptPackage(pkgState, null /* appHibernationManager */)) {
+ continue;
+ }
+ AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+ boolean isInDalvikCache = Utils.isInDalvikCache(pkgState);
+ boolean keepArtifacts = !Utils.shouldSkipDexoptDueToHibernation(
+ pkgState, mInjector.getAppHibernationManager());
+ for (DetailedPrimaryDexInfo dexInfo :
+ PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
+ if (!dexInfo.hasCode()) {
+ continue;
+ }
+ profilesToKeep.add(PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo));
+ profilesToKeep.addAll(PrimaryDexUtils.getCurProfiles(
+ mInjector.getUserManager(), pkgState, dexInfo));
+ if (keepArtifacts) {
+ for (Abi abi : Utils.getAllAbis(pkgState)) {
+ maybeKeepArtifacts(artifactsToKeep, vdexFilesToKeep, pkgState, dexInfo,
+ abi, isInDalvikCache);
+ }
+ }
+ }
+ for (DetailedSecondaryDexInfo dexInfo :
+ mInjector.getDexUseManager().getFilteredDetailedSecondaryDexInfo(
+ pkgState.getPackageName())) {
+ profilesToKeep.add(
+ AidlUtils.buildProfilePathForSecondaryRef(dexInfo.dexPath()));
+ profilesToKeep.add(
+ AidlUtils.buildProfilePathForSecondaryCur(dexInfo.dexPath()));
+ if (keepArtifacts) {
+ for (Abi abi : Utils.getAllAbisForNames(dexInfo.abiNames(), pkgState)) {
+ maybeKeepArtifacts(artifactsToKeep, vdexFilesToKeep, pkgState, dexInfo,
+ abi, false /* isInDalvikCache */);
+ }
+ }
+ }
+ }
+ long freedBytes =
+ mInjector.getArtd().cleanup(profilesToKeep, artifactsToKeep, vdexFilesToKeep);
+ Log.i(TAG, String.format("Freed %d bytes", freedBytes));
+ } catch (RemoteException e) {
+ throw new IllegalStateException("An error occurred when calling artd", e);
+ }
+ }
+
+ /**
+ * Checks if the artifacts are up-to-date, and maybe adds them to {@code artifactsToKeep} or
+ * {@code vdexFilesToKeep} based on the result.
+ */
+ private void maybeKeepArtifacts(@NonNull List<ArtifactsPath> artifactsToKeep,
+ @NonNull List<VdexPath> vdexFilesToKeep, @NonNull PackageState pkgState,
+ @NonNull DetailedDexInfo dexInfo, @NonNull Abi abi, boolean isInDalvikCache)
+ throws RemoteException {
+ try {
+ GetDexoptStatusResult result = mInjector.getArtd().getDexoptStatus(
+ dexInfo.dexPath(), abi.isa(), dexInfo.classLoaderContext());
+ if (DexFile.isValidCompilerFilter(result.compilerFilter)) {
+ // TODO(b/263579377): This is a bit inaccurate. We may be keeping the artifacts in
+ // dalvik-cache while OatFileAssistant actually picks the ones not in dalvik-cache.
+ // However, this isn't a big problem because it is an edge case and it only causes
+ // us to delete less rather than deleting more.
+ ArtifactsPath artifacts =
+ AidlUtils.buildArtifactsPath(dexInfo.dexPath(), abi.isa(), isInDalvikCache);
+ if (result.compilationReason.equals(ArtConstants.REASON_VDEX)) {
+ // Only the VDEX file is usable.
+ vdexFilesToKeep.add(VdexPath.artifactsPath(artifacts));
+ } else {
+ artifactsToKeep.add(artifacts);
+ }
+ }
+ } catch (ServiceSpecificException e) {
+ // Don't add the artifacts to the lists. They should be cleaned up.
+ Log.e(TAG,
+ String.format("Failed to get dexopt status [packageName = %s, dexPath = %s, "
+ + "isa = %s, classLoaderContext = %s]",
+ pkgState.getPackageName(), dexInfo.dexPath(), abi.isa(),
+ dexInfo.classLoaderContext()),
+ e);
+ }
+ }
+
+ /**
* Should be used by {@link BackgroundDexoptJobService} ONLY.
*
* @hide
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 6fd1dc5..9ac9663 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -352,6 +352,10 @@
}
return 0;
}
+ case "cleanup": {
+ mArtManagerLocal.cleanup(snapshot);
+ return 0;
+ }
default:
pw.println(String.format("Unknown 'art' sub-command '%s'", subcmd));
pw.println("See 'cmd package help' for help");
@@ -453,6 +457,8 @@
pw.println(" The profile of the base APK is dumped to 'PACKAGE_NAME-primary.prof.txt'");
pw.println(" The profile of a split APK is dumped to");
pw.println(" 'PACKAGE_NAME-SPLIT_NAME.split.prof.txt'");
+ pw.println(" cleanup");
+ pw.println(" Cleanup obsolete files.");
}
private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java b/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java
index e6ba579..9298928 100644
--- a/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java
+++ b/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java
@@ -196,13 +196,22 @@
@NonNull
private CompletedResult run(@NonNull CancellationSignal cancellationSignal) {
// TODO(b/254013427): Cleanup dex use info.
- // TODO(b/254013425): Cleanup unused secondary dex file artifacts.
long startTimeMs = SystemClock.uptimeMillis();
DexoptResult dexoptResult;
try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) {
dexoptResult = mInjector.getArtManagerLocal().dexoptPackages(snapshot,
ReasonMapping.REASON_BG_DEXOPT, cancellationSignal,
null /* processCallbackExecutor */, null /* processCallback */);
+
+ // For simplicity, we don't support cancelling the following operation in the middle.
+ // This is fine because it typically takes only a few seconds.
+ if (!cancellationSignal.isCanceled()) {
+ // We do the cleanup after dexopt so that it doesn't affect the `getSizeBeforeBytes`
+ // field in the result that we send to callbacks. Admittedly, this will cause us to
+ // lose some chance to dexopt when the storage is very low, but it's fine because we
+ // can still dexopt in the next run.
+ mInjector.getArtManagerLocal().cleanup(snapshot);
+ }
}
return CompletedResult.create(dexoptResult, SystemClock.uptimeMillis() - startTimeMs);
}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 0d40923..5592850 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -257,14 +257,19 @@
// We do not dexopt unused packages.
// If `appHibernationManager` is null, the caller's intention is to skip the check.
if (appHibernationManager != null
- && appHibernationManager.isHibernatingGlobally(pkgState.getPackageName())
- && appHibernationManager.isOatArtifactDeletionEnabled()) {
+ && shouldSkipDexoptDueToHibernation(pkgState, appHibernationManager)) {
return false;
}
return true;
}
+ public static boolean shouldSkipDexoptDueToHibernation(
+ @NonNull PackageState pkgState, @NonNull AppHibernationManager appHibernationManager) {
+ return appHibernationManager.isHibernatingGlobally(pkgState.getPackageName())
+ && appHibernationManager.isOatArtifactDeletionEnabled();
+ }
+
public static long getPackageLastActiveTime(@NonNull PackageState pkgState,
@NonNull DexUseManagerLocal dexUseManager, @NonNull UserManager userManager) {
long lastUsedAtMs = dexUseManager.getPackageLastUsedAtMs(pkgState.getPackageName());
diff --git a/libartservice/service/java/com/android/server/art/model/DexoptParams.java b/libartservice/service/java/com/android/server/art/model/DexoptParams.java
index 4dc9471..f7ce894 100644
--- a/libartservice/service/java/com/android/server/art/model/DexoptParams.java
+++ b/libartservice/service/java/com/android/server/art/model/DexoptParams.java
@@ -24,6 +24,7 @@
import android.annotation.SystemApi;
import com.android.internal.annotations.Immutable;
+import com.android.server.art.ArtConstants;
import com.android.server.art.ReasonMapping;
import com.android.server.art.Utils;
@@ -120,6 +121,10 @@
if (mParams.mReason.isEmpty()) {
throw new IllegalArgumentException("Reason must not be empty");
}
+ if (mParams.mReason.equals(ArtConstants.REASON_VDEX)) {
+ throw new IllegalArgumentException(
+ "Reason must not be '" + ArtConstants.REASON_VDEX + "'");
+ }
if (mParams.mCompilerFilter.isEmpty()) {
mParams.mCompilerFilter = ReasonMapping.getCompilerFilterForReason(mParams.mReason);
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index 08db07f..9f83c86 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -18,7 +18,7 @@
import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
-import static com.android.server.art.DexUseManagerLocal.SecondaryDexInfo;
+import static com.android.server.art.DexUseManagerLocal.DetailedSecondaryDexInfo;
import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus;
import static com.android.server.art.testing.TestingUtils.deepEq;
import static com.android.server.art.testing.TestingUtils.inAnyOrder;
@@ -85,6 +85,7 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
@@ -190,8 +191,12 @@
// All packages are by default recently used.
lenient().when(mDexUseManager.getPackageLastUsedAtMs(any())).thenReturn(RECENT_TIME_MS);
- List<? extends SecondaryDexInfo> secondaryDexInfo = createSecondaryDexInfo();
+ List<DetailedSecondaryDexInfo> secondaryDexInfo = createSecondaryDexInfo();
lenient().doReturn(secondaryDexInfo).when(mDexUseManager).getSecondaryDexInfo(eq(PKG_NAME));
+ lenient()
+ .doReturn(secondaryDexInfo)
+ .when(mDexUseManager)
+ .getFilteredDetailedSecondaryDexInfo(eq(PKG_NAME));
simulateStorageNotLow();
@@ -794,6 +799,56 @@
mArtManagerLocal.snapshotBootImageProfile(mSnapshot);
}
+ @Test
+ public void testCleanup() throws Exception {
+ // It should keep all artifacts.
+ doReturn(createGetDexoptStatusResult("speed-profile", "bg-dexopt", "location"))
+ .when(mArtd)
+ .getDexoptStatus(eq("/data/app/foo/base.apk"), eq("arm64"), any());
+ doReturn(createGetDexoptStatusResult("verify", "cmdline", "location"))
+ .when(mArtd)
+ .getDexoptStatus(eq("/data/user/0/foo/1.apk"), eq("arm64"), any());
+
+ // It should only keep VDEX files.
+ doReturn(createGetDexoptStatusResult("verify", "vdex", "location"))
+ .when(mArtd)
+ .getDexoptStatus(eq("/data/app/foo/split_0.apk"), eq("arm64"), any());
+ doReturn(createGetDexoptStatusResult("verify", "vdex", "location"))
+ .when(mArtd)
+ .getDexoptStatus(eq("/data/app/foo/split_0.apk"), eq("arm"), any());
+
+ // It should not keep any artifacts.
+ doReturn(createGetDexoptStatusResult("run-from-apk", "unknown", "unknown"))
+ .when(mArtd)
+ .getDexoptStatus(eq("/data/app/foo/base.apk"), eq("arm"), any());
+
+ when(mSnapshot.getPackageStates()).thenReturn(Map.of(PKG_NAME, mPkgState));
+ mArtManagerLocal.cleanup(mSnapshot);
+
+ verify(mArtd).cleanup(
+ inAnyOrderDeepEquals(AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForSecondaryRef("/data/user/0/foo/1.apk"),
+ AidlUtils.buildProfilePathForSecondaryCur("/data/user/0/foo/1.apk")),
+ inAnyOrderDeepEquals(AidlUtils.buildArtifactsPath("/data/app/foo/base.apk", "arm64",
+ mIsInReadonlyPartition),
+ AidlUtils.buildArtifactsPath(
+ "/data/user/0/foo/1.apk", "arm64", false /* isInDalvikCache */)),
+ inAnyOrderDeepEquals(
+ VdexPath.artifactsPath(AidlUtils.buildArtifactsPath(
+ "/data/app/foo/split_0.apk", "arm64", mIsInReadonlyPartition)),
+ VdexPath.artifactsPath(AidlUtils.buildArtifactsPath(
+ "/data/app/foo/split_0.apk", "arm", mIsInReadonlyPartition))));
+ }
+
private AndroidPackage createPackage(boolean multiSplit) {
AndroidPackage pkg = mock(AndroidPackage.class);
@@ -882,8 +937,8 @@
return getDexoptStatusResult;
}
- private List<? extends SecondaryDexInfo> createSecondaryDexInfo() throws Exception {
- var dexInfo = mock(SecondaryDexInfo.class);
+ private List<DetailedSecondaryDexInfo> createSecondaryDexInfo() throws Exception {
+ var dexInfo = mock(DetailedSecondaryDexInfo.class);
lenient().when(dexInfo.dexPath()).thenReturn("/data/user/0/foo/1.apk");
lenient().when(dexInfo.abiNames()).thenReturn(Set.of("arm64-v8a"));
lenient().when(dexInfo.classLoaderContext()).thenReturn("CLC");
diff --git a/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java b/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java
index b6a0a81..c008fd4 100644
--- a/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java
+++ b/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java
@@ -121,6 +121,8 @@
Result result = Utils.getFuture(mBackgroundDexoptJob.start());
assertThat(result).isInstanceOf(CompletedResult.class);
assertThat(((CompletedResult) result).dexoptResult()).isSameInstanceAs(mDexoptResult);
+
+ verify(mArtManagerLocal).cleanup(same(mSnapshot));
}
@Test
diff --git a/libartservice/service/javatests/com/android/server/art/model/DexoptParamsTest.java b/libartservice/service/javatests/com/android/server/art/model/DexoptParamsTest.java
index aeff58c..6098641 100644
--- a/libartservice/service/javatests/com/android/server/art/model/DexoptParamsTest.java
+++ b/libartservice/service/javatests/com/android/server/art/model/DexoptParamsTest.java
@@ -36,6 +36,11 @@
}
@Test(expected = IllegalArgumentException.class)
+ public void testBuildReasonVdex() {
+ new DexoptParams.Builder("vdex").setCompilerFilter("speed").setPriorityClass(90).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
public void testBuildInvalidCompilerFilter() {
new DexoptParams.Builder("install").setCompilerFilter("invalid").build();
}
diff --git a/runtime/oat_file.cc b/runtime/oat_file.cc
index de14835..60f1393 100644
--- a/runtime/oat_file.cc
+++ b/runtime/oat_file.cc
@@ -1844,7 +1844,7 @@
InstructionSetFeatures::FromCppDefines();
SafeMap<std::string, std::string> store;
store.Put(OatHeader::kCompilerFilter, CompilerFilter::NameOfFilter(CompilerFilter::kVerify));
- store.Put(OatHeader::kCompilationReasonKey, "vdex");
+ store.Put(OatHeader::kCompilationReasonKey, kReasonVdex);
store.Put(OatHeader::kConcurrentCopying,
gUseReadBarrier ? OatHeader::kTrueValue : OatHeader::kFalseValue);
if (context != nullptr) {
diff --git a/runtime/oat_file.h b/runtime/oat_file.h
index d52723a..57fedfd 100644
--- a/runtime/oat_file.h
+++ b/runtime/oat_file.h
@@ -60,6 +60,10 @@
} // namespace collector
} // namespace gc
+// A special compilation reason to indicate that only the VDEX file is usable. Keep in sync with
+// `ArtConstants::REASON_VDEX` in artd/binder/com/android/server/art/ArtConstants.aidl.
+static constexpr const char* kReasonVdex = "vdex";
+
// OatMethodOffsets are currently 5x32-bits=160-bits long, so if we can
// save even one OatMethodOffsets struct, the more complicated encoding
// using a bitmap pays for itself since few classes will have 160