Persist dex use information.
After this change, dex use information can be saved in a proto.
Also, this CL enables Proguard in order to reduce the cost of
introducing protobuf.
Bug: 249984283
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: Ifae646e12241364d5edb3cc5ecd4c8548539b9c4
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index 5c3ca8e..f11d902 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -87,12 +87,36 @@
static_libs: [
"artd-aidl-java",
"modules-utils-shell-command-handler",
+ "service-art-proto-java",
],
plugins: [
"auto_value_plugin",
"java_api_finder",
],
jarjar_rules: "jarjar-rules.txt",
+ optimize: {
+ enabled: true,
+ optimize: true,
+ shrink: true,
+ proguard_compatibility: false,
+ proguard_flags_files: ["proguard.flags"],
+ },
+}
+
+java_library {
+ name: "service-art-proto-java",
+ proto: {
+ type: "lite",
+ },
+ srcs: [
+ "proto/**/*.proto",
+ ],
+ sdk_version: "system_server_current",
+ min_sdk_version: "31",
+ apex_available: [
+ "com.android.art",
+ "com.android.art.debug",
+ ],
}
art_cc_defaults {
diff --git a/libartservice/service/jarjar-rules.txt b/libartservice/service/jarjar-rules.txt
index c7d39e6..54ff0a1 100644
--- a/libartservice/service/jarjar-rules.txt
+++ b/libartservice/service/jarjar-rules.txt
@@ -1,2 +1,3 @@
# Repackages static libraries to make them private to ART Services.
rule com.android.modules.utils.** com.android.server.art.jarjar.@0
+rule com.google.protobuf.** com.android.server.art.jarjar.@0
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index e61b294..b9485a0 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -170,6 +170,26 @@
}
return 0;
}
+ case "dex-use-dump": {
+ pw.println(mDexUseManager.dump());
+ return 0;
+ }
+ case "dex-use-save": {
+ try {
+ mDexUseManager.save(getNextArgRequired());
+ return 0;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ case "dex-use-load": {
+ try {
+ mDexUseManager.load(getNextArgRequired());
+ return 0;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
default:
// Handles empty, help, and invalid commands.
return handleDefaultCommands(cmd);
@@ -217,6 +237,12 @@
pw.println(" dex-use-get-secondary PACKAGE_NAME");
pw.println(" Print the dex use information about all secondary dex files owned by the");
pw.println(" given package.");
+ pw.println(" dex-use-dump");
+ pw.println(" Print all dex use information in textproto format.");
+ pw.println(" dex-use-save PATH");
+ pw.println(" Save dex use information to a file in binary proto format.");
+ pw.println(" dex-use-load PATH");
+ pw.println(" Load dex use information from a file in binary proto format.");
}
private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/DexUseManager.java b/libartservice/service/java/com/android/server/art/DexUseManager.java
index 790cd86..5c023e1 100644
--- a/libartservice/service/java/com/android/server/art/DexUseManager.java
+++ b/libartservice/service/java/com/android/server/art/DexUseManager.java
@@ -25,6 +25,13 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.Immutable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.proto.DexUseProto;
+import com.android.server.art.proto.Int32Value;
+import com.android.server.art.proto.PackageDexUseProto;
+import com.android.server.art.proto.PrimaryDexUseProto;
+import com.android.server.art.proto.PrimaryDexUseRecordProto;
+import com.android.server.art.proto.SecondaryDexUseProto;
+import com.android.server.art.proto.SecondaryDexUseRecordProto;
import com.android.server.art.wrapper.Environment;
import com.android.server.art.wrapper.Process;
import com.android.server.pm.PackageManagerLocal;
@@ -271,6 +278,35 @@
mDexUse = new DexUse();
}
+ public @NonNull String dump() {
+ var builder = DexUseProto.newBuilder();
+ synchronized (this) {
+ mDexUse.toProto(builder);
+ }
+ return builder.build().toString();
+ }
+
+ public void save(@NonNull String filename) throws IOException {
+ try (OutputStream out = new FileOutputStream(filename)) {
+ var builder = DexUseProto.newBuilder();
+ synchronized (this) {
+ mDexUse.toProto(builder);
+ }
+ builder.build().writeTo(out);
+ }
+ }
+
+ public void load(@NonNull String filename) throws IOException {
+ try (InputStream in = new FileInputStream(filename)) {
+ var proto = DexUseProto.parseFrom(in);
+ var dexUse = new DexUse();
+ dexUse.fromProto(proto);
+ synchronized (this) {
+ mDexUse = dexUse;
+ }
+ }
+ }
+
private static boolean isUsedByOtherApps(
@NonNull Set<DexLoader> loaders, @NonNull String owningPackageName) {
// If the dex file is loaded by an isolated process of the same app, it can also be
@@ -364,6 +400,24 @@
private static class DexUse {
@NonNull Map<String, PackageDexUse> mPackageDexUseByOwningPackageName = new HashMap<>();
+
+ void toProto(@NonNull DexUseProto.Builder builder) {
+ for (var entry : mPackageDexUseByOwningPackageName.entrySet()) {
+ var packageBuilder =
+ PackageDexUseProto.newBuilder().setOwningPackageName(entry.getKey());
+ entry.getValue().toProto(packageBuilder);
+ builder.addPackageDexUse(packageBuilder);
+ }
+ }
+
+ void fromProto(@NonNull DexUseProto proto) {
+ for (PackageDexUseProto packageProto : proto.getPackageDexUseList()) {
+ var packageDexUse = new PackageDexUse();
+ packageDexUse.fromProto(packageProto);
+ mPackageDexUseByOwningPackageName.put(
+ Utils.assertNonEmpty(packageProto.getOwningPackageName()), packageDexUse);
+ }
+ }
}
private static class PackageDexUse {
@@ -378,15 +432,84 @@
* JARs in CE and DE directories).
*/
@NonNull Map<String, SecondaryDexUse> mSecondaryDexUseByDexFile = new HashMap<>();
+
+ void toProto(@NonNull PackageDexUseProto.Builder builder) {
+ for (var entry : mPrimaryDexUseByDexFile.entrySet()) {
+ var primaryBuilder = PrimaryDexUseProto.newBuilder().setDexFile(entry.getKey());
+ entry.getValue().toProto(primaryBuilder);
+ builder.addPrimaryDexUse(primaryBuilder);
+ }
+ for (var entry : mSecondaryDexUseByDexFile.entrySet()) {
+ var secondaryBuilder = SecondaryDexUseProto.newBuilder().setDexFile(entry.getKey());
+ entry.getValue().toProto(secondaryBuilder);
+ builder.addSecondaryDexUse(secondaryBuilder);
+ }
+ }
+
+ void fromProto(@NonNull PackageDexUseProto proto) {
+ for (PrimaryDexUseProto primaryProto : proto.getPrimaryDexUseList()) {
+ var primaryDexUse = new PrimaryDexUse();
+ primaryDexUse.fromProto(primaryProto);
+ mPrimaryDexUseByDexFile.put(
+ Utils.assertNonEmpty(primaryProto.getDexFile()), primaryDexUse);
+ }
+ for (SecondaryDexUseProto secondaryProto : proto.getSecondaryDexUseList()) {
+ var secondaryDexUse = new SecondaryDexUse();
+ secondaryDexUse.fromProto(secondaryProto);
+ mSecondaryDexUseByDexFile.put(
+ Utils.assertNonEmpty(secondaryProto.getDexFile()), secondaryDexUse);
+ }
+ }
}
private static class PrimaryDexUse {
@NonNull Set<DexLoader> mLoaders = new HashSet<>();
+
+ void toProto(@NonNull PrimaryDexUseProto.Builder builder) {
+ for (DexLoader loader : mLoaders) {
+ builder.addRecord(PrimaryDexUseRecordProto.newBuilder()
+ .setLoadingPackageName(loader.loadingPackageName())
+ .setIsolatedProcess(loader.isolatedProcess()));
+ }
+ }
+
+ void fromProto(@NonNull PrimaryDexUseProto proto) {
+ for (PrimaryDexUseRecordProto recordProto : proto.getRecordList()) {
+ mLoaders.add(
+ DexLoader.create(Utils.assertNonEmpty(recordProto.getLoadingPackageName()),
+ recordProto.getIsolatedProcess()));
+ }
+ }
}
private static class SecondaryDexUse {
@Nullable UserHandle mUserHandle = null;
@NonNull Map<DexLoader, SecondaryDexUseRecord> mRecordByLoader = new HashMap<>();
+
+ void toProto(@NonNull SecondaryDexUseProto.Builder builder) {
+ builder.setUserId(Int32Value.newBuilder().setValue(mUserHandle.getIdentifier()));
+ for (var entry : mRecordByLoader.entrySet()) {
+ var recordBuilder =
+ SecondaryDexUseRecordProto.newBuilder()
+ .setLoadingPackageName(entry.getKey().loadingPackageName())
+ .setIsolatedProcess(entry.getKey().isolatedProcess());
+ entry.getValue().toProto(recordBuilder);
+ builder.addRecord(recordBuilder);
+ }
+ }
+
+ void fromProto(@NonNull SecondaryDexUseProto proto) {
+ Utils.check(proto.hasUserId());
+ mUserHandle = UserHandle.of(proto.getUserId().getValue());
+ for (SecondaryDexUseRecordProto recordProto : proto.getRecordList()) {
+ var record = new SecondaryDexUseRecord();
+ record.fromProto(recordProto);
+ mRecordByLoader.put(
+ DexLoader.create(Utils.assertNonEmpty(recordProto.getLoadingPackageName()),
+ recordProto.getIsolatedProcess()),
+ record);
+ }
+ }
}
/** Represents an entity that loads a dex file. */
@@ -409,5 +532,14 @@
// reported by the app.
@Nullable String mClassLoaderContext = null;
@Nullable String mAbiName = null;
+
+ void toProto(@NonNull SecondaryDexUseRecordProto.Builder builder) {
+ builder.setClassLoaderContext(mClassLoaderContext).setAbiName(mAbiName);
+ }
+
+ void fromProto(@NonNull SecondaryDexUseRecordProto proto) {
+ mClassLoaderContext = Utils.assertNonEmpty(proto.getClassLoaderContext());
+ mAbiName = Utils.assertNonEmpty(proto.getAbiName());
+ }
}
}
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
index 6c66190..aebc502 100644
--- a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -187,7 +187,17 @@
/** Checks that it ignores and dedups things correctly. */
@Test
- public void testPrimaryDexMultipleEntries() {
+ public void testPrimaryDexMultipleEntries() throws Exception {
+ verifyPrimaryDexMultipleEntries(false /* saveAndLoad */);
+ }
+
+ /** Checks that it saves and loads data correctly. */
+ @Test
+ public void testPrimaryDexMultipleEntriesPersisted() throws Exception {
+ verifyPrimaryDexMultipleEntries(true /*saveAndLoad */);
+ }
+
+ private void verifyPrimaryDexMultipleEntries(boolean saveAndLoad) throws Exception {
// These should be ignored.
mDexUseManager.addDexUse(mSnapshot, "android", Map.of(BASE_APK, "CLC"));
mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
@@ -206,6 +216,13 @@
mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME, Map.of(BASE_APK, "CLC"));
+ if (saveAndLoad) {
+ File tempFile = File.createTempFile("dex-use", ".pb");
+ mDexUseManager.save(tempFile.getPath());
+ mDexUseManager.clear();
+ mDexUseManager.load(tempFile.getPath());
+ }
+
assertThat(mDexUseManager.getPrimaryDexLoaders(OWNING_PKG_NAME, BASE_APK))
.containsExactly(DexLoader.create(OWNING_PKG_NAME, false /* isolatedProcess */),
DexLoader.create(OWNING_PKG_NAME, true /* isolatedProcess */),
@@ -287,7 +304,17 @@
/** Checks that it ignores and dedups things correctly. */
@Test
- public void testSecondaryDexMultipleEntries() {
+ public void testSecondaryDexMultipleEntries() throws Exception {
+ verifySecondaryDexMultipleEntries(false /*saveAndLoad */);
+ }
+
+ /** Checks that it saves and loads data correctly. */
+ @Test
+ public void testSecondaryDexMultipleEntriesPersisted() throws Exception {
+ verifySecondaryDexMultipleEntries(true /*saveAndLoad */);
+ }
+
+ private void verifySecondaryDexMultipleEntries(boolean saveAndLoad) throws Exception {
// These should be ignored.
mDexUseManager.addDexUse(mSnapshot, "android", Map.of(mCeDir + "/foo.apk", "CLC"));
mDexUseManager.addDexUse(
@@ -316,6 +343,13 @@
mDexUseManager.addDexUse(mSnapshot, OWNING_PKG_NAME,
Map.of(mCeDir + "/foo.apk", SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT));
+ if (saveAndLoad) {
+ File tempFile = File.createTempFile("dex-use", ".pb");
+ mDexUseManager.save(tempFile.getPath());
+ mDexUseManager.clear();
+ mDexUseManager.load(tempFile.getPath());
+ }
+
List<SecondaryDexInfo> dexInfoList = mDexUseManager.getSecondaryDexInfo(OWNING_PKG_NAME);
assertThat(dexInfoList)
.containsExactly(SecondaryDexInfo.create(mCeDir + "/foo.apk", mUserHandle,
diff --git a/libartservice/service/proguard.flags b/libartservice/service/proguard.flags
new file mode 100644
index 0000000..3589496
--- /dev/null
+++ b/libartservice/service/proguard.flags
@@ -0,0 +1,12 @@
+# Only keep APIs.
+-keep @android.annotation.SystemApi class * {
+ public *;
+}
+
+# Proto field names are used by MessageLiteToString.toString through reflection.
+-keepclassmembers class * extends
+ com.android.server.art.jarjar.com.google.protobuf.GeneratedMessageLite {
+ *** get*();
+ *** set*(***);
+ *** has*();
+}
diff --git a/libartservice/service/proto/common.proto b/libartservice/service/proto/common.proto
new file mode 100644
index 0000000..c8bb565
--- /dev/null
+++ b/libartservice/service/proto/common.proto
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto3";
+
+package com.android.server.art.proto;
+option java_multiple_files = true;
+
+// Wrapper message for `int32`, to distinguish between the absence of a field
+// and its default value.
+message Int32Value {
+ int32 value = 1;
+}
diff --git a/libartservice/service/proto/dex_use.proto b/libartservice/service/proto/dex_use.proto
new file mode 100644
index 0000000..53e5107
--- /dev/null
+++ b/libartservice/service/proto/dex_use.proto
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto3";
+
+package com.android.server.art.proto;
+option java_multiple_files = true;
+
+import "art/libartservice/service/proto/common.proto";
+
+// The protobuf representation of `DexUseManager.DexUse`. See classes in
+// java/com/android/server/art/DexUseManager.java for details.
+// This proto is persisted on disk and both forward and backward compatibility are considerations.
+message DexUseProto {
+ repeated PackageDexUseProto package_dex_use = 1;
+}
+
+message PackageDexUseProto {
+ string owning_package_name = 1;
+ repeated PrimaryDexUseProto primary_dex_use = 2;
+ repeated SecondaryDexUseProto secondary_dex_use = 3;
+}
+
+message PrimaryDexUseProto {
+ string dex_file = 1;
+ repeated PrimaryDexUseRecordProto record = 2;
+}
+
+message PrimaryDexUseRecordProto {
+ string loading_package_name = 1;
+ bool isolated_process = 2;
+}
+
+message SecondaryDexUseProto {
+ string dex_file = 1;
+ Int32Value user_id = 2; // Must be explicitly set.
+ repeated SecondaryDexUseRecordProto record = 3;
+}
+
+message SecondaryDexUseRecordProto {
+ string loading_package_name = 1;
+ bool isolated_process = 2;
+ string class_loader_context = 3;
+ string abi_name = 4;
+}