Implement reporting of events from native side to WebPerformance API (#35768)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/35768

Changelog: [Internal]

This implements native side mechanics for reporting user events timing to JS  (PerformanceObserver API).

See the standard for more details: https://www.w3.org/TR/event-timing/

The events are only logged when there are any active subscriptions (via `PerformanceObserver.observe`), also we only log "discrete events" (i.e. no likes of mouse move), so the overhead is non-existing.

There are two main metrics of interest for an event lifecycle:
* Time the event is spent in the queue, i.e. the time between it's created and dispatched
* Time that is spend in the event handler on the JS side (event dispatch), or processing time

Both of these are measured, and the corresponding fields are populated.

Reviewed By: sammy-SC

Differential Revision: D42294947

fbshipit-source-id: 4fd7938c04b942400befa4057d4929fb2763cee1
This commit is contained in:
Ruslan Shestopalyuk
2023-01-03 11:11:37 -08:00
committed by Facebook GitHub Bot
parent 28a06d2fd1
commit 09ad0cc0c6
13 changed files with 224 additions and 22 deletions
@@ -6,16 +6,20 @@
*/
#include "PerformanceEntryReporter.h"
#include <glog/logging.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
#include <cxxreact/JSExecutor.h>
#include <react/renderer/core/EventLogger.h>
#include "NativePerformanceObserver.h"
#include <algorithm>
// All the unflushed entries beyond this amount will get discarded, with
// the amount of discarded ones sent back to the observers' callbacks as
// "droppedEntryCount" value
static constexpr size_t MAX_ENTRY_BUFFER_SIZE = 1024;
namespace facebook::react {
EventTag PerformanceEntryReporter::sCurrentEventTag_{0};
PerformanceEntryReporter &PerformanceEntryReporter::getInstance() {
static PerformanceEntryReporter instance;
return instance;
@@ -33,27 +37,22 @@ void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) {
reportingType_[static_cast<int>(entryType)] = false;
}
const std::vector<RawPerformanceEntry>
&PerformanceEntryReporter::getPendingEntries() const {
return entries_;
}
GetPendingEntriesResult PerformanceEntryReporter::popPendingEntries() {
std::lock_guard<std::mutex> lock(entriesMutex_);
GetPendingEntriesResult res = {std::move(entries_), droppedEntryCount_};
entries_ = {};
droppedEntryCount_ = 0;
return res;
}
void PerformanceEntryReporter::clearPendingEntries() {
entries_.clear();
}
void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) {
if (!isReportingType(static_cast<PerformanceEntryType>(entry.entryType))) {
return;
}
std::lock_guard<std::mutex> lock(entriesMutex_);
if (entries_.size() == MAX_ENTRY_BUFFER_SIZE) {
// Start dropping entries once reached maximum buffer size.
// The number of dropped entries will be reported back to the corresponding
@@ -179,7 +178,7 @@ void PerformanceEntryReporter::event(
{name,
static_cast<int>(
isFirstInput ? PerformanceEntryType::FIRST_INPUT
: PerformanceEntryType::MEASURE),
: PerformanceEntryType::EVENT),
startTime,
duration,
processingStart,
@@ -207,4 +206,78 @@ void PerformanceEntryReporter::scheduleFlushBuffer() {
}
}
static bool isDiscreteEvent(const char *name) {
return !std::strstr(name, "Move") && !std::strstr(name, "Layout");
}
EventTag PerformanceEntryReporter::onEventStart(const char *name) {
if (!isReportingEvents() || !isDiscreteEvent(name)) {
return 0;
}
sCurrentEventTag_++;
if (sCurrentEventTag_ == 0) {
// The tag wrapped around (which is highly unlikely, but still)
sCurrentEventTag_ = 1;
}
if (std::strstr(name, "top") == name) {
// Skip the "top" prefix if present
name += 3;
}
auto timeStamp = JSExecutor::performanceNow();
{
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
eventsInFlight_.emplace(
std::make_pair(sCurrentEventTag_, EventEntry{name, timeStamp, 0.0}));
}
return sCurrentEventTag_;
}
void PerformanceEntryReporter::onEventDispatch(EventTag tag) {
if (!isReportingEvents() || tag == 0) {
return;
}
auto timeStamp = JSExecutor::performanceNow();
{
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
auto it = eventsInFlight_.find(tag);
if (it != eventsInFlight_.end()) {
it->second.dispatchTime = timeStamp;
}
}
}
void PerformanceEntryReporter::onEventEnd(EventTag tag) {
if (!isReportingEvents() || tag == 0) {
return;
}
auto timeStamp = JSExecutor::performanceNow();
{
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
auto it = eventsInFlight_.find(tag);
if (it == eventsInFlight_.end()) {
return;
}
auto &entry = it->second;
auto &name = entry.name;
std::transform(name.begin(), name.end(), name.begin(), ::tolower);
// TODO: Define the way to assign interaction IDs to the event chains
// (T141358175)
const uint32_t interactionId = 0;
bool firstInput = isFirstInput(name);
event(
std::move(name),
entry.startTime,
timeStamp - entry.startTime,
firstInput,
entry.dispatchTime,
timeStamp,
interactionId);
eventsInFlight_.erase(it);
}
}
} // namespace facebook::react