Bug 1581240 - Return collected frames as a promise to JS r=bzbarsky,mstange,nika

The `setCompositionRecording` API on nsIDOMWindowUtils has been broken up into
two new APIs:

* `startCompositionRecording()`, which starts the composition recorder; and
* `stopCompositionRecording(bool writeToDisk)` which stops the composition
  recorder and either returns a Promise that resolves to the collected frames
  or returns a Promise that resolves when the frames have been written to disk.

The collected frames are serialized over IPC as part of a Shmem as to not
approach the IPC data transfer limit.

Differential Revision: https://phabricator.services.mozilla.com/D47818

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Barret Rennie 2019-11-06 20:48:49 +00:00
parent 3f8aaa035d
commit d9aec252da
12 changed files with 293 additions and 37 deletions

View File

@ -20,14 +20,16 @@
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BlobBinding.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/DOMCollectedFramesBinding.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/Touch.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/PendingAnimationTracker.h"
#include "nsIObjectLoadingContent.h"
#include "nsFrame.h"
#include "mozilla/layers/ShadowLayers.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "mozilla/layers/PCompositorBridgeTypes.h"
#include "mozilla/layers/ShadowLayers.h"
#include "ClientLayerManager.h"
#include "nsQueryObject.h"
#include "CubebDeviceEnumerator.h"
@ -43,9 +45,11 @@
#include "nsJSUtils.h"
#include "mozilla/ChaosMode.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/Span.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TextEventDispatcher.h"
@ -4031,6 +4035,56 @@ nsDOMWindowUtils::WrCapture() {
NS_IMETHODIMP
nsDOMWindowUtils::SetCompositionRecording(bool aValue, Promise** aOutPromise) {
return aValue ? StartCompositionRecording(aOutPromise)
: StopCompositionRecording(true, aOutPromise);
}
NS_IMETHODIMP
nsDOMWindowUtils::StartCompositionRecording(Promise** aOutPromise) {
NS_ENSURE_ARG(aOutPromise);
*aOutPromise = nullptr;
nsCOMPtr<nsPIDOMWindowOuter> outer = do_QueryReferent(mWindow);
NS_ENSURE_STATE(outer);
nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow();
NS_ENSURE_STATE(inner);
ErrorResult err;
RefPtr<Promise> promise = Promise::Create(inner->AsGlobal(), err);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}
CompositorBridgeChild* cbc = GetCompositorBridge();
if (NS_WARN_IF(!cbc)) {
promise->MaybeReject(NS_ERROR_UNEXPECTED);
} else {
cbc->SendBeginRecording(TimeStamp::Now())
->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[promise](const bool& aSuccess) {
if (aSuccess) {
promise->MaybeResolve(true);
} else {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_INVALID_STATE_ERR,
"The composition recorder is already running.");
}
},
[promise](const mozilla::ipc::ResponseRejectReason&) {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_INVALID_STATE_ERR,
"Could not start the composition recorder.");
});
}
promise.forget(aOutPromise);
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::StopCompositionRecording(bool aWriteToDisk,
Promise** aOutPromise) {
NS_ENSURE_ARG_POINTER(aOutPromise);
*aOutPromise = nullptr;
@ -4048,30 +4102,8 @@ nsDOMWindowUtils::SetCompositionRecording(bool aValue, Promise** aOutPromise) {
CompositorBridgeChild* cbc = GetCompositorBridge();
if (NS_WARN_IF(!cbc)) {
promise->MaybeReject(NS_ERROR_UNEXPECTED);
promise.forget(aOutPromise);
return NS_ERROR_UNEXPECTED;
}
if (aValue) {
cbc->SendBeginRecording(TimeStamp::Now())
->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[promise](const bool& aSuccess) {
if (aSuccess) {
promise->MaybeResolveWithUndefined();
} else {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_INVALID_STATE_ERR,
"The composition recorder is already running.");
}
},
[promise](const mozilla::ipc::ResponseRejectReason&) {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_UNKNOWN_ERR,
"Could not start the composition recorder.");
});
} else {
cbc->SendEndRecording()->Then(
} else if (aWriteToDisk) {
cbc->SendEndRecordingToDisk()->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[promise](const bool& aSuccess) {
if (aSuccess) {
@ -4087,6 +4119,67 @@ nsDOMWindowUtils::SetCompositionRecording(bool aValue, Promise** aOutPromise) {
NS_ERROR_DOM_UNKNOWN_ERR,
"Could not stop the composition recorder.");
});
} else {
cbc->SendEndRecordingToMemory()->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[promise](Maybe<CollectedFramesParams>&& aFrames) {
if (!aFrames) {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_UNKNOWN_ERR,
"Could not stop the composition recorder.");
return;
}
DOMCollectedFrames domFrames;
if (!domFrames.mFrames.SetCapacity(aFrames->frames().Length(),
fallible)) {
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return;
}
domFrames.mRecordingStart = aFrames->recordingStart();
CheckedInt<size_t> totalSize(0);
for (const CollectedFrameParams& frame : aFrames->frames()) {
totalSize += frame.length();
}
if (totalSize.isValid() &&
totalSize.value() > aFrames->buffer().Size<char>()) {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_UNKNOWN_ERR,
"Could not interpret returned frames.");
return;
}
Span<const char> buffer(aFrames->buffer().get<char>(),
aFrames->buffer().Size<char>());
for (const CollectedFrameParams& frame : aFrames->frames()) {
size_t length = frame.length();
Span<const char> dataUri = buffer.First(length);
// We have to do a fallible AppendElement() because WebIDL
// dictionaries use FallibleTArray. However, this cannot fail due to
// the pre-allocation earlier.
DOMCollectedFrame* domFrame =
domFrames.mFrames.AppendElement(fallible);
MOZ_ASSERT(domFrame);
domFrame->mTimeOffset = frame.timeOffset();
domFrame->mDataUri = nsCString(dataUri);
buffer = buffer.Subspan(length);
}
promise->MaybeResolve(domFrames);
},
[promise](const mozilla::ipc::ResponseRejectReason&) {
promise->MaybeRejectWithDOMException(
NS_ERROR_DOM_UNKNOWN_ERR,
"Could not stop the composition recorder.");
});
}
promise.forget(aOutPromise);
@ -4101,7 +4194,6 @@ nsDOMWindowUtils::SetTransactionLogging(bool aValue) {
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::SetSystemFont(const nsACString& aFontName) {
nsIWidget* widget = GetWidget();

View File

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* A single frame collected by the |CompositionRecorder|.
*/
[GenerateConversionToJS]
dictionary DOMCollectedFrame {
/**
* The offset of the frame past the start point of the recording (in
* milliseconds).
*/
required double timeOffset;
/** A data: URI containing the PNG image data of the frame. */
required ByteString dataUri;
};
/**
* Information about frames collected by the |CompositionRecorder|.
*/
[GenerateConversionToJS]
dictionary DOMCollectedFrames {
/** The collected frames. */
required sequence<DOMCollectedFrame> frames;
/** The start point of the recording (in milliseconds). */
required double recordingStart;
};

View File

@ -39,6 +39,7 @@ WEBIDL_FILES = [
'DebuggerNotificationObserver.webidl',
'DebuggerUtils.webidl',
'DocumentL10n.webidl',
'DOMCollectedFrames.webidl',
'DominatorTree.webidl',
'DOMLocalization.webidl',
'Flex.webidl',
@ -79,4 +80,3 @@ if CONFIG['MOZ_PLACES']:
WEBIDL_FILES += [
'PrioEncoder.webidl',
]

View File

@ -1914,9 +1914,32 @@ interface nsIDOMWindowUtils : nsISupports {
/**
* Toggle recording of composition on and off.
*
* This is equivalent to calling |startCompositionRecorder()| or
* |stopCompositionRecorder(true)|.
*/
Promise setCompositionRecording(in boolean aValue);
/**
* Start the composition recorder.
*
* @return A promise that is resolved to true if the composion recorder was
* started successfully.
*/
Promise startCompositionRecording();
/**
* Stop the composition recorder.
*
* @param aWriteToDisk Whether or not the frames should be written to disk.
* If false, they will be returned in the promise.
* @return A promise that resolves when the frames have been collected.
* When |aWriteToDisk| is true, the promise will resolve to |undefined|.
* Otherwise, the promise will resolve to a |DOMCollectedFrames| dictionary,
* which contains the timestamps and contents of the captured frames.
*/
Promise stopCompositionRecording(in boolean aWriteToDisk);
/**
* Toggle transaction logging on and off.
*/

View File

@ -137,6 +137,17 @@ void HostLayerManager::WriteCollectedFrames() {
}
}
Maybe<CollectedFrames> HostLayerManager::GetCollectedFrames() {
Maybe<CollectedFrames> maybeFrames;
if (mCompositionRecorder) {
maybeFrames.emplace(mCompositionRecorder->GetCollectedFrames());
mCompositionRecorder = nullptr;
}
return maybeFrames;
}
/**
* LayerManagerComposite
*/

View File

@ -211,6 +211,8 @@ class HostLayerManager : public LayerManager {
*/
void WriteCollectedFrames();
Maybe<CollectedFrames> GetCollectedFrames();
protected:
bool mDebugOverlayWantsNextFrame;
nsTArray<ImageCompositeNotificationInfo> mImageCompositeNotifications;

View File

@ -68,6 +68,7 @@
#include "mozilla/media/MediaSystemResourceService.h" // for MediaSystemResourceService
#include "mozilla/mozalloc.h" // for operator new, etc
#include "mozilla/PerfStats.h"
#include "mozilla/PodOperations.h"
#include "mozilla/Telemetry.h"
#ifdef MOZ_WIDGET_GTK
# include "basic/X11BasicCompositor.h" // for X11BasicCompositor
@ -2743,8 +2744,8 @@ mozilla::ipc::IPCResult CompositorBridgeParent::RecvBeginRecording(
return IPC_OK();
}
mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecording(
EndRecordingResolver&& aResolve) {
mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecordingToDisk(
EndRecordingToDiskResolver&& aResolve) {
if (!mHaveCompositionRecorder) {
aResolve(false);
return IPC_OK();
@ -2767,5 +2768,63 @@ mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecording(
return IPC_OK();
}
mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecordingToMemory(
EndRecordingToMemoryResolver&& aResolve) {
if (!mHaveCompositionRecorder) {
aResolve(Nothing());
return IPC_OK();
}
if (mLayerManager) {
Maybe<CollectedFrames> frames = mLayerManager->GetCollectedFrames();
if (frames) {
aResolve(WrapCollectedFrames(std::move(*frames)));
} else {
aResolve(Nothing());
}
} else if (mWrBridge) {
RefPtr<CompositorBridgeParent> self = this;
mWrBridge->GetCollectedFrames()->Then(
MessageLoop::current()->SerialEventTarget(), __func__,
[self, resolve{aResolve}](CollectedFrames&& frames) {
resolve(self->WrapCollectedFrames(std::move(frames)));
},
[resolve{aResolve}]() { resolve(Nothing()); });
}
return IPC_OK();
}
Maybe<CollectedFramesParams> CompositorBridgeParent::WrapCollectedFrames(
CollectedFrames&& aFrames) {
CollectedFramesParams ipcFrames;
ipcFrames.recordingStart() = aFrames.mRecordingStart;
size_t totalLength = 0;
for (const CollectedFrame& frame : aFrames.mFrames) {
totalLength += frame.mDataUri.Length();
}
Shmem shmem;
if (!AllocShmem(totalLength, SharedMemory::TYPE_BASIC, &shmem)) {
return Nothing();
}
{
char* raw = shmem.get<char>();
for (CollectedFrame& frame : aFrames.mFrames) {
size_t length = frame.mDataUri.Length();
PodCopy(raw, frame.mDataUri.get(), length);
raw += length;
ipcFrames.frames().EmplaceBack(frame.mTimeOffset, length);
}
}
ipcFrames.buffer() = std::move(shmem);
return Some(std::move(ipcFrames));
}
} // namespace layers
} // namespace mozilla

View File

@ -35,6 +35,7 @@
#include "mozilla/layers/ISurfaceAllocator.h" // for ShmemAllocator
#include "mozilla/layers/LayersMessages.h" // for TargetConfig
#include "mozilla/layers/MetricsSharingController.h"
#include "mozilla/layers/PCompositorBridgeTypes.h"
#include "mozilla/layers/PCompositorBridgeParent.h"
#include "mozilla/layers/APZTestData.h"
#include "mozilla/webrender/WebRenderTypes.h"
@ -246,8 +247,10 @@ class CompositorBridgeParentBase : public PCompositorBridgeParent,
virtual mozilla::ipc::IPCResult RecvAllPluginsCaptured() = 0;
virtual mozilla::ipc::IPCResult RecvBeginRecording(
const TimeStamp& aRecordingStart, BeginRecordingResolver&& aResolve) = 0;
virtual mozilla::ipc::IPCResult RecvEndRecording(
EndRecordingResolver&& aResolve) = 0;
virtual mozilla::ipc::IPCResult RecvEndRecordingToDisk(
EndRecordingToDiskResolver&& aResolve) = 0;
virtual mozilla::ipc::IPCResult RecvEndRecordingToMemory(
EndRecordingToMemoryResolver&& aResolve) = 0;
virtual mozilla::ipc::IPCResult RecvInitialize(
const LayersId& rootLayerTreeId) = 0;
virtual mozilla::ipc::IPCResult RecvGetFrameUniformity(
@ -363,8 +366,10 @@ class CompositorBridgeParent final : public CompositorBridgeParentBase,
mozilla::ipc::IPCResult RecvBeginRecording(
const TimeStamp& aRecordingStart,
BeginRecordingResolver&& aResolve) override;
mozilla::ipc::IPCResult RecvEndRecording(
EndRecordingResolver&& aResolve) override;
mozilla::ipc::IPCResult RecvEndRecordingToDisk(
EndRecordingToDiskResolver&& aResolve) override;
mozilla::ipc::IPCResult RecvEndRecordingToMemory(
EndRecordingToMemoryResolver&& aResolve) override;
void NotifyMemoryPressure() override;
void AccumulateMemoryReport(wr::MemoryReport*) override;
@ -676,6 +681,11 @@ class CompositorBridgeParent final : public CompositorBridgeParentBase,
*/
static void DeallocateLayerTreeId(LayersId aId);
/**
* Wrap the data structure to be sent over IPC.
*/
Maybe<CollectedFramesParams> WrapCollectedFrames(CollectedFrames&& aFrames);
protected:
// Protected destructor, to discourage deletion outside of Release():
virtual ~CompositorBridgeParent();

View File

@ -96,12 +96,18 @@ class ContentCompositorBridgeParent final : public CompositorBridgeParentBase {
return IPC_OK();
}
mozilla::ipc::IPCResult RecvEndRecording(
EndRecordingResolver&& aResolve) override {
mozilla::ipc::IPCResult RecvEndRecordingToDisk(
EndRecordingToDiskResolver&& aResolve) override {
aResolve(false);
return IPC_OK();
}
mozilla::ipc::IPCResult RecvEndRecordingToMemory(
EndRecordingToMemoryResolver&& aResolve) override {
aResolve(Nothing());
return IPC_OK();
}
mozilla::ipc::IPCResult RecvGetFrameUniformity(
FrameUniformityData* aOutData) override {
// Don't support calculating frame uniformity on the child process and

View File

@ -8,6 +8,7 @@
include LayersSurfaces;
include LayersMessages;
include PlatformWidgetTypes;
include PCompositorBridgeTypes;
include protocol PAPZ;
include protocol PAPZCTreeManager;
include protocol PBrowser;
@ -64,7 +65,6 @@ struct FrameStats {
nsCString url;
};
/**
* The PCompositorBridge protocol is a top-level protocol for the compositor.
* There is an instance of the protocol for each compositor, plus one for each
@ -271,9 +271,12 @@ parent:
async BeginRecording(TimeStamp aRecordingStart)
returns (bool success);
async EndRecording()
async EndRecordingToDisk()
returns (bool success);
async EndRecordingToMemory()
returns (CollectedFramesParams? frames);
// To set up sharing the composited output to Firefox Reality on Desktop
async RequestFxrOutput();

View File

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace mozilla {
namespace layers {
struct CollectedFrameParams {
double timeOffset;
uint32_t length;
};
struct CollectedFramesParams {
double recordingStart;
CollectedFrameParams[] frames;
Shmem buffer;
};
} // namespace layers
} // namespace mozilla

View File

@ -568,6 +568,7 @@ IPDL_SOURCES += [
'ipc/PAPZInputBridge.ipdl',
'ipc/PCanvas.ipdl',
'ipc/PCompositorBridge.ipdl',
'ipc/PCompositorBridgeTypes.ipdlh',
'ipc/PCompositorManager.ipdl',
'ipc/PImageBridge.ipdl',
'ipc/PLayerTransaction.ipdl',