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
This commit is contained in:
Fabien Sanglard
2023-02-07 20:57:24 +00:00
parent 72b80b0bb2
commit 3878a7736e
8 changed files with 306 additions and 1 deletions

View File

@@ -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: [

View File

@@ -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("<pre-initialized>",

View File

@@ -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);
/*

View File

@@ -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);
}
}
}

View File

@@ -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 "<preinitalized>" 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();
}
}

View File

@@ -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

View File

@@ -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,
},
}

View File

@@ -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());
}
}