Ensure SKP serialization occurs on RenderThread
Instead of doing lazy serialization of SKP on the
background executor serialize to a byte[] immediately
at callback invocation. This ensures no potential
for later mutations, race conditions, or wrong-thread issues
at the expense of potentially impacting app rendering performance.
However it seems preferable for a debug-only tool to be a slow
instead of very crashy.
Bug: 141772764
Test: test app
Change-Id: I3316d49970b96f1c59bb0a28ff7335db608e539e
(cherry picked from commit 8d0da1a6c5271d7f803b665d6a0784a2ca74d3c8)
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
index a390db2..1ee3366 100644
--- a/core/java/android/view/ViewDebug.java
+++ b/core/java/android/view/ViewDebug.java
@@ -868,6 +868,94 @@
return null;
}
+ private static class StreamingPictureCallbackHandler implements AutoCloseable,
+ HardwareRenderer.PictureCapturedCallback, Runnable {
+ private final HardwareRenderer mRenderer;
+ private final Callable<OutputStream> mCallback;
+ private final Executor mExecutor;
+ private final ReentrantLock mLock = new ReentrantLock(false);
+ private final ArrayDeque<byte[]> mQueue = new ArrayDeque<>(3);
+ private final ByteArrayOutputStream mByteStream = new ByteArrayOutputStream();
+ private boolean mStopListening;
+ private Thread mRenderThread;
+
+ private StreamingPictureCallbackHandler(HardwareRenderer renderer,
+ Callable<OutputStream> callback, Executor executor) {
+ mRenderer = renderer;
+ mCallback = callback;
+ mExecutor = executor;
+ mRenderer.setPictureCaptureCallback(this);
+ }
+
+ @Override
+ public void close() {
+ mLock.lock();
+ mStopListening = true;
+ mLock.unlock();
+ mRenderer.setPictureCaptureCallback(null);
+ }
+
+ @Override
+ public void onPictureCaptured(Picture picture) {
+ mLock.lock();
+ if (mStopListening) {
+ mLock.unlock();
+ mRenderer.setPictureCaptureCallback(null);
+ return;
+ }
+ if (mRenderThread == null) {
+ mRenderThread = Thread.currentThread();
+ }
+ boolean needsInvoke = true;
+ if (mQueue.size() == 3) {
+ mQueue.removeLast();
+ needsInvoke = false;
+ }
+ picture.writeToStream(mByteStream);
+ mQueue.add(mByteStream.toByteArray());
+ mByteStream.reset();
+ mLock.unlock();
+
+ if (needsInvoke) {
+ mExecutor.execute(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ mLock.lock();
+ final byte[] picture = mQueue.poll();
+ final boolean isStopped = mStopListening;
+ mLock.unlock();
+ if (Thread.currentThread() == mRenderThread) {
+ close();
+ throw new IllegalStateException(
+ "ViewDebug#startRenderingCommandsCapture must be given an executor that "
+ + "invokes asynchronously");
+ }
+ if (isStopped) {
+ return;
+ }
+ OutputStream stream = null;
+ try {
+ stream = mCallback.call();
+ } catch (Exception ex) {
+ Log.w("ViewDebug", "Aborting rendering commands capture "
+ + "because callback threw exception", ex);
+ }
+ if (stream != null) {
+ try {
+ stream.write(picture);
+ } catch (IOException ex) {
+ Log.w("ViewDebug", "Aborting rendering commands capture "
+ + "due to IOException writing to output stream", ex);
+ }
+ } else {
+ close();
+ }
+ }
+ }
+
/**
* Begins capturing the entire rendering commands for the view tree referenced by the given
* view. The view passed may be any View in the tree as long as it is attached. That is,
@@ -913,18 +1001,7 @@
}
final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
if (renderer != null) {
- return new PictureCallbackHandler(renderer, (picture -> {
- try {
- OutputStream stream = callback.call();
- if (stream != null) {
- picture.writeToStream(stream);
- return true;
- }
- } catch (Exception ex) {
- // fall through
- }
- return false;
- }), executor);
+ return new StreamingPictureCallbackHandler(renderer, callback, executor);
}
return null;
}