Introduce multi-frame SKP capturing in SkiaPipeline
Capture script usage is the same as before, but all frames are combined
into a single file which shares the images between frames, reducing the
file size and serialization time somewhat. This brings us closer to the
objective of realistic performance measurements, but to do that
correctly, a second format is needed, (skbug.com/9174)
Single frame captures still produce the same format.
Test: The method used for serialization is tested in skia's DM unit
tests, in MultiSkpTest.cpp
Test: Tested manually with the libs/hwui/tests/scripts/skp-capture.sh script
Bug: skbug.com/9210
Change-Id: I69da8d191640ebb444991f107d60389f1519a9db
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index ff29a5b..84c0d13 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -19,14 +19,17 @@
#include <SkImageEncoder.h>
#include <SkImageInfo.h>
#include <SkImagePriv.h>
+#include <SkMultiPictureDocument.h>
#include <SkOverdrawCanvas.h>
#include <SkOverdrawColorFilter.h>
#include <SkPicture.h>
#include <SkPictureRecorder.h>
+#include <SkSerialProcs.h>
#include "LightingInfo.h"
#include "TreeInfo.h"
#include "VectorDrawable.h"
#include "thread/CommonPool.h"
+#include "tools/SkSharingProc.h"
#include "utils/TraceUtils.h"
#include <unistd.h>
@@ -99,7 +102,7 @@
SkASSERT(layerNode->getLayerSurface());
SkiaDisplayList* displayList = (SkiaDisplayList*)layerNode->getDisplayList();
if (!displayList || displayList->isEmpty()) {
- SkDEBUGF(("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()));
+ ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName());
return;
}
@@ -233,58 +236,138 @@
if (stream.isValid()) {
stream.write(data->data(), data->size());
stream.flush();
- SkDebugf("SKP Captured Drawing Output (%d bytes) for frame. %s", stream.bytesWritten(),
+ ALOGD("SKP Captured Drawing Output (%zu bytes) for frame. %s", stream.bytesWritten(),
filename.c_str());
}
});
}
-SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) {
- if (CC_UNLIKELY(Properties::skpCaptureEnabled)) {
+// Note multiple SkiaPipeline instances may be loaded if more than one app is visible.
+// Each instance may observe the filename changing and try to record to a file of the same name.
+// Only the first one will succeed. There is no scope available here where we could coordinate
+// to cause this function to return true for only one of the instances.
+bool SkiaPipeline::shouldStartNewFileCapture() {
+ // Don't start a new file based capture if one is currently ongoing.
+ if (mCaptureMode != CaptureMode::None) { return false; }
+
+ // A new capture is started when the filename property changes.
+ // Read the filename property.
+ std::string prop = base::GetProperty(PROPERTY_CAPTURE_SKP_FILENAME, "0");
+ // if the filename property changed to a valid value
+ if (prop[0] != '0' && mCapturedFile != prop) {
+ // remember this new filename
+ mCapturedFile = prop;
+ // and get a property indicating how many frames to capture.
+ mCaptureSequence = base::GetIntProperty(PROPERTY_CAPTURE_SKP_FRAMES, 1);
if (mCaptureSequence <= 0) {
- std::string prop = base::GetProperty(PROPERTY_CAPTURE_SKP_FILENAME, "0");
- if (prop[0] != '0' && mCapturedFile != prop) {
- mCapturedFile = prop;
- mCaptureSequence = base::GetIntProperty(PROPERTY_CAPTURE_SKP_FRAMES, 1);
- }
+ return false;
+ } else if (mCaptureSequence == 1) {
+ mCaptureMode = CaptureMode::SingleFrameSKP;
+ } else {
+ mCaptureMode = CaptureMode::MultiFrameSKP;
}
- if (mCaptureSequence > 0 || mPictureCapturedCallback) {
- mRecorder.reset(new SkPictureRecorder());
- SkCanvas* pictureCanvas =
- mRecorder->beginRecording(surface->width(), surface->height(), nullptr,
- SkPictureRecorder::kPlaybackDrawPicture_RecordFlag);
- mNwayCanvas = std::make_unique<SkNWayCanvas>(surface->width(), surface->height());
- mNwayCanvas->addCanvas(surface->getCanvas());
- mNwayCanvas->addCanvas(pictureCanvas);
- return mNwayCanvas.get();
+ return true;
+ }
+ return false;
+}
+
+// performs the first-frame work of a multi frame SKP capture. Returns true if successful.
+bool SkiaPipeline::setupMultiFrameCapture() {
+ ALOGD("Set up multi-frame capture, frames = %d", mCaptureSequence);
+ // We own this stream and need to hold it until close() finishes.
+ auto stream = std::make_unique<SkFILEWStream>(mCapturedFile.c_str());
+ if (stream->isValid()) {
+ mOpenMultiPicStream = std::move(stream);
+ mSerialContext.reset(new SkSharingSerialContext());
+ SkSerialProcs procs;
+ procs.fImageProc = SkSharingSerialContext::serializeImage;
+ procs.fImageCtx = mSerialContext.get();
+ // SkDocuments don't take owership of the streams they write.
+ // we need to keep it until after mMultiPic.close()
+ // procs is passed as a pointer, but just as a method of having an optional default.
+ // procs doesn't need to outlive this Make call.
+ mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs);
+ return true;
+ } else {
+ ALOGE("Could not open \"%s\" for writing.", mCapturedFile.c_str());
+ mCaptureSequence = 0;
+ mCaptureMode = CaptureMode::None;
+ return false;
+ }
+}
+
+SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) {
+ if (CC_LIKELY(!Properties::skpCaptureEnabled)) {
+ return surface->getCanvas(); // Bail out early when capture is not turned on.
+ }
+ // Note that shouldStartNewFileCapture tells us if this is the *first* frame of a capture.
+ if (shouldStartNewFileCapture() && mCaptureMode == CaptureMode::MultiFrameSKP) {
+ if (!setupMultiFrameCapture()) {
+ return surface->getCanvas();
}
}
- return surface->getCanvas();
+
+ // Create a canvas pointer, fill it depending on what kind of capture is requested (if any)
+ SkCanvas* pictureCanvas = nullptr;
+ switch (mCaptureMode) {
+ case CaptureMode::CallbackAPI:
+ case CaptureMode::SingleFrameSKP:
+ mRecorder.reset(new SkPictureRecorder());
+ pictureCanvas = mRecorder->beginRecording(surface->width(), surface->height(),
+ nullptr, SkPictureRecorder::kPlaybackDrawPicture_RecordFlag);
+ break;
+ case CaptureMode::MultiFrameSKP:
+ // If a multi frame recording is active, initialize recording for a single frame of a
+ // multi frame file.
+ pictureCanvas = mMultiPic->beginPage(surface->width(), surface->height());
+ break;
+ case CaptureMode::None:
+ // Returning here in the non-capture case means we can count on pictureCanvas being
+ // non-null below.
+ return surface->getCanvas();
+ }
+
+ // Setting up an nway canvas is common to any kind of capture.
+ mNwayCanvas = std::make_unique<SkNWayCanvas>(surface->width(), surface->height());
+ mNwayCanvas->addCanvas(surface->getCanvas());
+ mNwayCanvas->addCanvas(pictureCanvas);
+ return mNwayCanvas.get();
}
void SkiaPipeline::endCapture(SkSurface* surface) {
+ if (CC_LIKELY(mCaptureMode == CaptureMode::None)) { return; }
mNwayCanvas.reset();
- if (CC_UNLIKELY(mRecorder.get())) {
- ATRACE_CALL();
+ ATRACE_CALL();
+ if (mCaptureSequence > 0 && mCaptureMode == CaptureMode::MultiFrameSKP) {
+ mMultiPic->endPage();
+ mCaptureSequence--;
+ if (mCaptureSequence == 0) {
+ mCaptureMode = CaptureMode::None;
+ // Pass mMultiPic and mOpenMultiPicStream to a background thread, which will handle
+ // the heavyweight serialization work and destroy them. mOpenMultiPicStream is released
+ // to a bare pointer because keeping it in a smart pointer makes the lambda
+ // non-copyable. The lambda is only called once, so this is safe.
+ SkFILEWStream* stream = mOpenMultiPicStream.release();
+ CommonPool::post([doc = std::move(mMultiPic), stream]{
+ ALOGD("Finalizing multi frame SKP");
+ doc->close();
+ delete stream;
+ ALOGD("Multi frame SKP complete.");
+ });
+ }
+ } else {
sk_sp<SkPicture> picture = mRecorder->finishRecordingAsPicture();
if (picture->approximateOpCount() > 0) {
- if (mCaptureSequence > 0) {
- ATRACE_BEGIN("picture->serialize");
- auto data = picture->serialize();
- ATRACE_END();
-
- // offload saving to file in a different thread
- if (1 == mCaptureSequence) {
- savePictureAsync(data, mCapturedFile);
- } else {
- savePictureAsync(data, mCapturedFile + "_" + std::to_string(mCaptureSequence));
- }
- mCaptureSequence--;
- }
if (mPictureCapturedCallback) {
std::invoke(mPictureCapturedCallback, std::move(picture));
+ } else {
+ // single frame skp to file
+ auto data = picture->serialize();
+ savePictureAsync(data, mCapturedFile);
+ mCaptureSequence = 0;
}
}
+ mCaptureMode = CaptureMode::None;
mRecorder.reset();
}
}
@@ -305,7 +388,6 @@
// initialize the canvas for the current frame, that might be a recording canvas if SKP
// capture is enabled.
- std::unique_ptr<SkPictureRecorder> recorder;
SkCanvas* canvas = tryCapture(surface.get());
renderFrameImpl(layers, clip, nodes, opaque, contentDrawBounds, canvas, preTransform);
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index 5fc1d61..37b559f 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -17,12 +17,15 @@
#pragma once
#include <SkSurface.h>
+#include <SkDocument.h>
+#include <SkMultiPictureDocument.h>
#include "Lighting.h"
#include "hwui/AnimatedImageDrawable.h"
#include "renderthread/CanvasContext.h"
#include "renderthread/IRenderPipeline.h"
class SkPictureRecorder;
+struct SkSharingSerialContext;
namespace android {
namespace uirenderer {
@@ -60,9 +63,12 @@
void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque);
+ // Sets the recording callback to the provided function and the recording mode
+ // to CallbackAPI
void setPictureCapturedCallback(
const std::function<void(sk_sp<SkPicture>&&)>& callback) override {
mPictureCapturedCallback = callback;
+ mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None;
}
protected:
@@ -92,8 +98,18 @@
*/
void renderVectorDrawableCache();
+ // Called every frame. Normally returns early with screen canvas.
+ // But when capture is enabled, returns an nwaycanvas where commands are also recorded.
SkCanvas* tryCapture(SkSurface* surface);
+ // Called at the end of every frame, closes the recording if necessary.
void endCapture(SkSurface* surface);
+ // Determine if a new file-based capture should be started.
+ // If so, sets mCapturedFile and mCaptureSequence and returns true.
+ // Should be called every frame when capture is enabled.
+ // sets mCaptureMode.
+ bool shouldStartNewFileCapture();
+ // Set up a multi frame capture.
+ bool setupMultiFrameCapture();
std::vector<sk_sp<SkImage>> mPinnedImages;
@@ -103,22 +119,46 @@
std::vector<VectorDrawableRoot*> mVectorDrawables;
// Block of properties used only for debugging to record a SkPicture and save it in a file.
+ // There are three possible ways of recording drawing commands.
+ enum class CaptureMode {
+ // return to this mode when capture stops.
+ None,
+ // A mode where every frame is recorded into an SkPicture and sent to a provided callback,
+ // until that callback is cleared
+ CallbackAPI,
+ // A mode where a finite number of frames are recorded to a file with
+ // SkMultiPictureDocument
+ MultiFrameSKP,
+ // A mode which records a single frame to a normal SKP file.
+ SingleFrameSKP,
+ };
+ CaptureMode mCaptureMode = CaptureMode::None;
+
/**
- * mCapturedFile is used to enforce we don't capture more than once for a given name (cause
- * permissions don't allow to reset a property from render thread).
+ * mCapturedFile - the filename to write a recorded SKP to in either MultiFrameSKP or
+ * SingleFrameSKP mode.
*/
std::string mCapturedFile;
/**
- * mCaptureSequence counts how many frames are left to take in the sequence.
+ * mCaptureSequence counts down how many frames are left to take in the sequence. Applicable
+ * only to MultiFrameSKP or SingleFrameSKP mode.
*/
int mCaptureSequence = 0;
+ // Multi frame serialization stream and writer used when serializing more than one frame.
+ std::unique_ptr<SkFILEWStream> mOpenMultiPicStream;
+ sk_sp<SkDocument> mMultiPic;
+ std::unique_ptr<SkSharingSerialContext> mSerialContext;
+
/**
- * mRecorder holds the current picture recorder. We could store it on the stack to support
- * parallel tryCapture calls (not really needed).
+ * mRecorder holds the current picture recorder when serializing in either SingleFrameSKP or
+ * CallbackAPI modes.
*/
std::unique_ptr<SkPictureRecorder> mRecorder;
std::unique_ptr<SkNWayCanvas> mNwayCanvas;
+
+ // Set by setPictureCapturedCallback and when set, CallbackAPI mode recording is ongoing.
+ // Not used in other recording modes.
std::function<void(sk_sp<SkPicture>&&)> mPictureCapturedCallback;
};
diff --git a/libs/hwui/tests/scripts/skp-capture.sh b/libs/hwui/tests/scripts/skp-capture.sh
index 54fa229..aad31fc 100755
--- a/libs/hwui/tests/scripts/skp-capture.sh
+++ b/libs/hwui/tests/scripts/skp-capture.sh
@@ -4,6 +4,12 @@
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+#
+# Before this can be used, the device must be rooted and the filesystem must be writable by Skia
+# - These steps are necessary once after flashing to enable capture -
+# adb root
+# adb remount
+# adb reboot
if [ -z "$1" ]; then
printf 'Usage:\n skp-capture.sh PACKAGE_NAME OPTIONAL_FRAME_COUNT\n\n'
@@ -20,8 +26,8 @@
exit 2
fi
fi
-phase1_timeout_seconds=15
-phase2_timeout_seconds=60
+phase1_timeout_seconds=60
+phase2_timeout_seconds=300
package="$1"
filename="$(date '+%H%M%S').skp"
remote_path="/data/data/${package}/cache/${filename}"
@@ -29,11 +35,14 @@
local_path="${local_path_prefix}.skp"
enable_capture_key='debug.hwui.capture_skp_enabled'
enable_capture_value=$(adb shell "getprop '${enable_capture_key}'")
-#printf 'captureflag=' "$enable_capture_value" '\n'
+
+# TODO(nifong): check if filesystem is writable here with "avbctl get-verity"
+# result will either start with "verity is disabled" or "verity is enabled"
+
if [ -z "$enable_capture_value" ]; then
- printf 'Capture SKP property need to be enabled first. Please use\n'
- printf "\"adb shell setprop debug.hwui.capture_skp_enabled true\" and then restart\n"
- printf "the process.\n\n"
+ printf 'debug.hwui.capture_skp_enabled was found to be disabled, enabling it now.\n'
+ printf " restart the process you want to capture on the device, then retry this script.\n\n"
+ adb shell "setprop '${enable_capture_key}' true"
exit 1
fi
if [ ! -z "$2" ]; then
@@ -57,33 +66,17 @@
printf ' %s' "$*"
printf '\n=====================\n'
}
-banner '...WAITING...'
-adb_test_exist() {
- test '0' = "$(adb shell "test -e \"$1\"; echo \$?")";
-}
-timeout=$(( $(date +%s) + $phase1_timeout_seconds))
-while ! adb_test_exist "$remote_path"; do
- spin 0.05
- if [ $(date +%s) -gt $timeout ] ; then
- printf '\bTimed out.\n'
- adb shell "setprop '${filename_key}' ''"
- exit 3
- fi
-done
-printf '\b'
-
-#read -n1 -r -p "Press any key to continue..." key
-
-banner '...SAVING...'
+banner '...WAITING FOR APP INTERACTION...'
+# Waiting for nonzero file is an indication that the pipeline has both opened the file and written
+# the header. With multiple frames this does not occur until the last frame has been recorded,
+# so we continue to show the "waiting for app interaction" message as long as the app still requires
+# interaction to draw more frames.
adb_test_file_nonzero() {
# grab first byte of `du` output
X="$(adb shell "du \"$1\" 2> /dev/null | dd bs=1 count=1 2> /dev/null")"
test "$X" && test "$X" -ne 0
}
-#adb_filesize() {
-# adb shell "wc -c \"$1\"" 2> /dev/null | awk '{print $1}'
-#}
-timeout=$(( $(date +%s) + $phase2_timeout_seconds))
+timeout=$(( $(date +%s) + $phase1_timeout_seconds))
while ! adb_test_file_nonzero "$remote_path"; do
spin 0.05
if [ $(date +%s) -gt $timeout ] ; then
@@ -94,8 +87,37 @@
done
printf '\b'
+# Disable further capturing
adb shell "setprop '${filename_key}' ''"
+banner '...SAVING...'
+# return the size of a file in bytes
+adb_filesize() {
+ adb shell "wc -c \"$1\"" 2> /dev/null | awk '{print $1}'
+}
+timeout=$(( $(date +%s) + $phase2_timeout_seconds))
+last_size='0' # output of last size check command
+unstable=true # false once the file size stops changing
+counter=0 # used to perform size check only 1/sec though we update spinner 20/sec
+# loop until the file size is unchanged for 1 second.
+while [ $unstable != 0 ] ; do
+ spin 0.05
+ counter=$(( $counter+1 ))
+ if ! (( $counter % 20)) ; then
+ new_size=$(adb_filesize "$remote_path")
+ unstable=$(($(adb_filesize "$remote_path") != last_size))
+ last_size=$new_size
+ fi
+ if [ $(date +%s) -gt $timeout ] ; then
+ printf '\bTimed out.\n'
+ adb shell "setprop '${filename_key}' ''"
+ exit 3
+ fi
+done
+printf '\b'
+
+printf "SKP file serialized: %s\n" $(echo $last_size | numfmt --to=iec)
+
i=0; while [ $i -lt 10 ]; do spin 0.10; i=$(($i + 1)); done; echo
adb pull "$remote_path" "$local_path"
@@ -105,12 +127,4 @@
fi
adb shell rm "$remote_path"
printf '\nSKP saved to file:\n %s\n\n' "$local_path"
-if [ ! -z "$2" ]; then
- bridge="_"
- adb shell "setprop 'debug.hwui.capture_skp_frames' ''"
- for i in $(seq 2 $2); do
- adb pull "${remote_path}_${i}" "${local_path_prefix}_${i}.skp"
- adb shell rm "${remote_path}_${i}"
- done
-fi