From 3878a7736ec3465c212180985f6c986be08d310a Mon Sep 17 00:00:00 2001 From: Fabien Sanglard Date: Tue, 7 Feb 2023 20:57:24 +0000 Subject: [PATCH] DDM: Introduce STAG packets This is a new mecanism to help DDM client synchronize with app state. Design doc: go/ddm-stag Test: Android Studio Bug: NA Change-Id: I4699c62756f50106962ec8481bf67f103fa41b8f --- Android.bp | 7 + core/java/android/app/ActivityThread.java | 11 ++ core/java/android/ddm/DdmHandleHello.java | 10 +- core/java/android/os/DdmSyncStageUpdater.java | 63 +++++++++ core/java/android/os/DdmSyncState.java | 127 ++++++++++++++++++ core/java/android/os/OWNERS | 4 + tests/utils/testutils/Android.bp | 14 ++ .../android/os/test/DdmSyncStateTest.java | 71 ++++++++++ 8 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 core/java/android/os/DdmSyncStageUpdater.java create mode 100644 core/java/android/os/DdmSyncState.java create mode 100644 tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java diff --git a/Android.bp b/Android.bp index effd7ce2a63a6..3709d7548d98d 100644 --- a/Android.bp +++ b/Android.bp @@ -502,6 +502,13 @@ filegroup { ], } +filegroup { + name: "framework-android-os-unit-testable-src", + srcs: [ + "core/java/android/os/DdmSyncState.java", + ], +} + filegroup { name: "framework-networkstack-shared-srcs", srcs: [ diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 54961917a06e4..6e9d0554b9af9 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -117,6 +117,8 @@ import android.os.BluetoothServiceManager; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.DdmSyncStageUpdater; +import android.os.DdmSyncState.Stage; import android.os.Debug; import android.os.Environment; import android.os.FileUtils; @@ -258,6 +260,9 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal { + + private final DdmSyncStageUpdater mDdmSyncStageUpdater = new DdmSyncStageUpdater(); + /** @hide */ public static final String TAG = "ActivityThread"; private static final android.graphics.Bitmap.Config THUMBNAIL_FORMAT = Bitmap.Config.RGB_565; @@ -6656,6 +6661,8 @@ public final class ActivityThread extends ClientTransactionHandler @UnsupportedAppUsage private void handleBindApplication(AppBindData data) { + mDdmSyncStageUpdater.next(Stage.Bind); + // Register the UI Thread as a sensitive thread to the runtime. VMRuntime.registerSensitiveThread(); // In the case the stack depth property exists, pass it down to the runtime. @@ -6699,6 +6706,7 @@ public final class ActivityThread extends ClientTransactionHandler data.appInfo.packageName, UserHandle.myUserId()); VMRuntime.setProcessPackageName(data.appInfo.packageName); + mDdmSyncStageUpdater.next(Stage.Named); // Pass data directory path to ART. This is used for caching information and // should be set before any application code is loaded. @@ -6903,6 +6911,7 @@ public final class ActivityThread extends ClientTransactionHandler final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy(); if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) { + mDdmSyncStageUpdater.next(Stage.Debugger); if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) { waitForDebugger(data); } else if (data.debugMode == ApplicationThreadConstants.DEBUG_SUSPEND) { @@ -6910,6 +6919,7 @@ public final class ActivityThread extends ClientTransactionHandler } // Nothing special to do in case of DEBUG_ON. } + mDdmSyncStageUpdater.next(Stage.Running); try { // If the app is being launched for full backup or restore, bring it up in @@ -7801,6 +7811,7 @@ public final class ActivityThread extends ClientTransactionHandler mConfigurationController = new ConfigurationController(this); mSystemThread = system; mStartSeq = startSeq; + mDdmSyncStageUpdater.next(Stage.Attach); if (!system) { android.ddm.DdmHandleAppName.setAppName("", diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index 41600294bfb54..a51a740752980 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -16,6 +16,7 @@ package android.ddm; +import android.os.DdmSyncState; import android.os.Debug; import android.os.UserHandle; import android.util.Log; @@ -44,6 +45,7 @@ public class DdmHandleHello extends DdmHandle { private static final String[] FRAMEWORK_FEATURES = new String[] { "opengl-tracing", "view-hierarchy", + "support_boot_stages" }; /* singleton, do not instantiate */ @@ -145,7 +147,9 @@ public class DdmHandleHello extends DdmHandle { + instructionSetDescription.length() * 2 + vmFlags.length() * 2 + 1 - + pkgName.length() * 2); + + pkgName.length() * 2 + // STAG id (int) + + Integer.BYTES); out.order(ChunkHandler.CHUNK_ORDER); out.putInt(CLIENT_PROTOCOL_VERSION); out.putInt(android.os.Process.myPid()); @@ -162,6 +166,10 @@ public class DdmHandleHello extends DdmHandle { out.putInt(pkgName.length()); putString(out, pkgName); + // Added API 34 (and advertised via FEAT ddm packet) + // Send the current boot stage in ActivityThread + out.putInt(DdmSyncState.getStage().toInt()); + Chunk reply = new Chunk(CHUNK_HELO, out); /* diff --git a/core/java/android/os/DdmSyncStageUpdater.java b/core/java/android/os/DdmSyncStageUpdater.java new file mode 100644 index 0000000000000..90f707652088d --- /dev/null +++ b/core/java/android/os/DdmSyncStageUpdater.java @@ -0,0 +1,63 @@ +/* + * 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 android.os; + +import android.os.DdmSyncState.Stage; +import android.util.Slog; + +import org.apache.harmony.dalvik.ddmc.Chunk; +import org.apache.harmony.dalvik.ddmc.ChunkHandler; +import org.apache.harmony.dalvik.ddmc.DdmServer; + +import java.nio.ByteBuffer; + +/** + * @hide + */ +// Making it public so ActivityThread can access it. +public class DdmSyncStageUpdater { + + private static final String TAG = "DdmSyncStageUpdater"; + + private static final int CHUNK_STAGE = ChunkHandler.type("STAG"); + + /** + * @hide + */ + public DdmSyncStageUpdater() { + } + + /** + * @hide + */ + // Making it public so ActivityThread can access it. + public synchronized void next(Stage stage) { + try { + DdmSyncState.next(stage); + + // Request DDMServer to send a STAG chunk + ByteBuffer data = ByteBuffer.allocate(Integer.BYTES); + data.putInt(stage.toInt()); + Chunk stagChunk = new Chunk(CHUNK_STAGE, data); + DdmServer.sendChunk(stagChunk); + } catch (Exception e) { + // Catch everything to make sure we don't impact ActivityThread + Slog.w(TAG, "Unable to go to next stage" + stage, e); + } + } + +} diff --git a/core/java/android/os/DdmSyncState.java b/core/java/android/os/DdmSyncState.java new file mode 100644 index 0000000000000..09c9ef2e8a119 --- /dev/null +++ b/core/java/android/os/DdmSyncState.java @@ -0,0 +1,127 @@ +/* + * 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 android.os; + +import java.util.Arrays; + +/** + * Keep track of an app boot state. The main purpose is to stream back DDM packet so a DDM client + * can synchronize with the app state. + * + * The state is static so it can be accessed from HELO handler. + * + * @hide + **/ +public final class DdmSyncState { + + /** + * @hide + */ + public enum Stage { + // From zygote to attach + Boot("BOOT"), + + // From attach to handleBindApplication + Attach("ATCH"), + + // When handleBindApplication is finally reached + Bind("BIND"), + + // When the actual package name is known (not the early "" value). + Named("NAMD"), + + // Can be skipped if the app is not debugged. + Debugger("DEBG"), + + // App is in RunLoop + Running("A_GO"); + + final String mLabel; + + Stage(String label) { + if (label.length() != 4) { + throw new IllegalStateException( + "Bad stage id '" + label + "'. Must be four letters"); + } + this.mLabel = label; + } + + /** + * To be included in a DDM packet payload, the stage is encoded in a big-endian int + * @hide + */ + public int toInt() { + int result = 0; + for (int i = 0; i < 4; ++i) { + result = ((result << 8) | (mLabel.charAt(i) & 0xff)); + } + return result; + } + } + + private static int sCurrentStageIndex = 0; + + /** + * @hide + */ + public static synchronized Stage getStage() { + return Stage.values()[sCurrentStageIndex]; + } + + /** + * @hide + */ + public static void reset() { + sCurrentStageIndex = 0; + } + + /** + * Search for the next level down the list of Stage. Only succeed if the next stage + * if a later stage (no cycling allowed). + * + * @hide + */ + public static synchronized void next(Stage nextStage) { + Stage[] stages = Stage.values(); + // Search for the requested next stage + int rover = sCurrentStageIndex; + while (rover < stages.length && stages[rover] != nextStage) { + rover++; + } + + if (rover == stages.length || stages[rover] != nextStage) { + throw new IllegalStateException( + "Cannot go to " + nextStage + " from:" + getInternalState()); + } + + sCurrentStageIndex = rover; + } + + /** + * Use to build error messages + * @hide + */ + private static String getInternalState() { + StringBuilder sb = new StringBuilder("\n"); + sb.append("level = ").append(sCurrentStageIndex); + sb.append("\n"); + sb.append("stages = "); + sb.append(Arrays.toString(Arrays.stream(Stage.values()).map(Enum::name).toArray())); + sb.append("\n"); + return sb.toString(); + } +} diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS index e9a32544e58e5..69889c5f14c57 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -79,3 +79,7 @@ per-file PermissionEnforcer.java = tweek@google.com, brufino@google.com # ART per-file ArtModuleServiceManager.java = file:platform/art:/OWNERS + +# DDM Protocol +per-file DdmSyncState.java = sanglardf@google.com, rpaquay@google.com +per-file DdmSyncStageUpdater.java = sanglardf@google.com, rpaquay@google.com diff --git a/tests/utils/testutils/Android.bp b/tests/utils/testutils/Android.bp index deff42a27f477..82953ac311ace 100644 --- a/tests/utils/testutils/Android.bp +++ b/tests/utils/testutils/Android.bp @@ -41,3 +41,17 @@ java_library { "mockito-target-extended-minus-junit4", ], } + +java_test_host { + name: "frameworks-base-android-os-unittests", + srcs: [ + ":framework-android-os-unit-testable-src", + "java/android/os/test/DdmSyncStateTest.java", + ], + static_libs: [ + "junit", + ], + test_options: { + unit_test: true, + }, +} diff --git a/tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java b/tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java new file mode 100644 index 0000000000000..8274ce4e941d8 --- /dev/null +++ b/tests/utils/testutils/java/android/os/test/DdmSyncStateTest.java @@ -0,0 +1,71 @@ +/* + * 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 android.os.test; + +import android.os.DdmSyncState; +import android.os.DdmSyncState.Stage; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test DdmSyncState, the Android app stage boot sync system for DDM Client. + */ + +public class DdmSyncStateTest { + + @Test + public void testNoCycle() { + DdmSyncState.reset(); + try { + DdmSyncState.next(Stage.Attach); + DdmSyncState.next(Stage.Bind); + DdmSyncState.next(Stage.Named); + DdmSyncState.next(Stage.Debugger); + DdmSyncState.next(Stage.Running); + + // Cycling back here which is not allowed + DdmSyncState.next(Stage.Attach); + Assert.fail("Going back to attach should have failed"); + } catch (IllegalStateException ignored) { + + } + } + + @Test + public void testDebuggerFlow() { + DdmSyncState.reset(); + DdmSyncState.next(Stage.Attach); + DdmSyncState.next(Stage.Bind); + DdmSyncState.next(Stage.Named); + DdmSyncState.next(Stage.Debugger); + DdmSyncState.next(Stage.Running); + Assert.assertEquals(Stage.Running, DdmSyncState.getStage()); + + } + + @Test + public void testNoDebugFlow() { + DdmSyncState.reset(); + DdmSyncState.next(Stage.Attach); + DdmSyncState.next(Stage.Bind); + DdmSyncState.next(Stage.Named); + // Notice how Stage.Debugger stage is skipped + DdmSyncState.next(Stage.Running); + Assert.assertEquals(Stage.Running, DdmSyncState.getStage()); + } +}