Files
react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp
T
Ruslan Shestopalyuk 14ab76ac30 Hoist responsibility for clearMarks/Measures to NativePerformanceObserver (#36312)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/36312

## Changelog:

[Internal] -

`clearMarks` and `clearMeasures` methods are incidental to the `NativePerformance` TurboModule functionality, as in reality this responsibility belongs more on the `NativePerformanceObserver` and `PerformanceEntryReporter` side.

This is something that [the standard indirectly suggests](https://www.w3.org/TR/user-timing/#clearmarks-method) as well (referencing [performance entry buffer](https://www.w3.org/TR/performance-timeline/#dfn-performance-entry-buffer)).

The new implementation should be also a little bit more efficient, as it avoids calling the predicate for each entry.

Finally (and frankly, the main reason for this change, from my perspective), it will simplify mocking/testing the JS part of the PerfAPI code.

Reviewed By: rubennorte

Differential Revision: D43621174

fbshipit-source-id: c4217a0da1d8ecbce797240627f7b4f057d85b97
2023-02-28 04:10:32 -08:00

331 lines
9.5 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "PerformanceEntryReporter.h"
#include <cxxreact/JSExecutor.h>
#include <react/renderer/core/EventLogger.h>
#include "NativePerformanceObserver.h"
#include <unordered_map>
// 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;
}
void PerformanceEntryReporter::setReportingCallback(
std::optional<AsyncCallback<>> callback) {
callback_ = callback;
}
void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) {
int entryTypeIdx = static_cast<int>(entryType);
reportingType_[entryTypeIdx] = true;
durationThreshold_[entryTypeIdx] = DEFAULT_DURATION_THRESHOLD;
}
void PerformanceEntryReporter::setDurationThreshold(
PerformanceEntryType entryType,
double durationThreshold) {
durationThreshold_[static_cast<int>(entryType)] = durationThreshold;
}
void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) {
reportingType_[static_cast<int>(entryType)] = false;
}
GetPendingEntriesResult PerformanceEntryReporter::popPendingEntries() {
std::lock_guard<std::mutex> lock(entriesMutex_);
GetPendingEntriesResult res = {std::move(entries_), droppedEntryCount_};
entries_ = {};
droppedEntryCount_ = 0;
return res;
}
void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) {
const auto entryType = static_cast<PerformanceEntryType>(entry.entryType);
if (entryType == PerformanceEntryType::EVENT) {
eventCounts_[entry.name]++;
}
if (!isReportingType(entryType)) {
return;
}
if (entry.duration < durationThreshold_[entry.entryType]) {
// The entries duration is lower than the desired reporting threshold, skip
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
// PerformanceObserver callback.
droppedEntryCount_ += 1;
return;
}
entries_.emplace_back(entry);
if (entries_.size() == 1) {
// If the buffer was empty, it signals that JS side just has possibly
// consumed it and is ready to get more
scheduleFlushBuffer();
}
}
void PerformanceEntryReporter::mark(
const std::string &name,
double startTime,
double duration) {
// Register the mark for further possible "measure" lookup, as well as add
// it to a circular buffer:
PerformanceMark &mark = marksBuffer_[marksBufferPosition_];
marksBufferPosition_ = (marksBufferPosition_ + 1) % marksBuffer_.size();
if (!mark.name.empty()) {
// Drop off the oldest mark out of the queue, but only if that's indeed the
// oldest one
auto it = marksRegistry_.find(&mark);
if (it != marksRegistry_.end() && *it == &mark) {
marksRegistry_.erase(it);
}
}
mark.name = name;
mark.timeStamp = startTime;
marksRegistry_.insert(&mark);
logEntry(
{name,
static_cast<int>(PerformanceEntryType::MARK),
startTime,
duration,
std::nullopt,
std::nullopt,
std::nullopt});
}
void PerformanceEntryReporter::clearEntries(
PerformanceEntryType entryType,
const char *entryName) {
if (entryName != nullptr && entryType == PerformanceEntryType::MARK) {
// remove a named mark from the mark/measure registry
PerformanceMark mark{{entryName, 0}};
marksRegistry_.erase(&mark);
}
int lastPos = entries_.size() - 1;
int pos = lastPos;
while (pos >= 0) {
const RawPerformanceEntry &entry = entries_[pos];
if (entry.entryType == static_cast<int32_t>(entryType) &&
(entryName == nullptr || entry.name == entryName)) {
entries_[pos] = entries_[lastPos];
lastPos--;
}
pos--;
}
entries_.resize(lastPos + 1);
}
void PerformanceEntryReporter::measure(
const std::string &name,
double startTime,
double endTime,
const std::optional<double> &duration,
const std::optional<std::string> &startMark,
const std::optional<std::string> &endMark) {
double startTimeVal = startMark ? getMarkTime(*startMark) : startTime;
double endTimeVal = endMark ? getMarkTime(*endMark) : endTime;
double durationVal = duration ? *duration : endTimeVal - startTimeVal;
logEntry(
{name,
static_cast<int>(PerformanceEntryType::MEASURE),
startTimeVal,
durationVal,
std::nullopt,
std::nullopt,
std::nullopt});
}
double PerformanceEntryReporter::getMarkTime(
const std::string &markName) const {
PerformanceMark mark{{std::move(markName), 0}};
auto it = marksRegistry_.find(&mark);
if (it != marksRegistry_.end()) {
return (*it)->timeStamp;
} else {
return 0.0;
}
}
void PerformanceEntryReporter::event(
std::string name,
double startTime,
double duration,
double processingStart,
double processingEnd,
uint32_t interactionId) {
logEntry(
{std::move(name),
static_cast<int>(PerformanceEntryType::EVENT),
startTime,
duration,
processingStart,
processingEnd,
interactionId});
}
void PerformanceEntryReporter::scheduleFlushBuffer() {
if (callback_) {
callback_->callWithPriority(SchedulerPriority::IdlePriority);
}
}
struct StrKey {
uint32_t key;
constexpr StrKey(const char *s)
: key(folly::hash::fnv32_buf(s, sizeof(s) - 1)) {}
constexpr bool operator==(const StrKey &rhs) const {
return key == rhs.key;
}
};
struct StrKeyHash {
constexpr size_t operator()(const StrKey &strKey) const {
return static_cast<size_t>(strKey.key);
}
};
// Supported events for reporting, see
// https://www.w3.org/TR/event-timing/#sec-events-exposed
// Not all of these are currently supported by RN, but we map them anyway for
// future-proofing.
static const std::unordered_map<StrKey, const char *, StrKeyHash>
SUPPORTED_EVENTS = {
{"topAuxClick", "auxclick"},
{"topClick", "click"},
{"topContextMenu", "contextmenu"},
{"topDblClick", "dblclick"},
{"topMouseDown", "mousedown"},
{"topMouseEnter", "mouseenter"},
{"topMouseLeave", "mouseleave"},
{"topMouseOut", "mouseout"},
{"topMouseOver", "mouseover"},
{"topMouseUp", "mouseup"},
{"topPointerOver", "pointerover"},
{"topPointerEnter", "pointerenter"},
{"topPointerDown", "pointerdown"},
{"topPointerUp", "pointerup"},
{"topPointerCancel", "pointercancel"},
{"topPointerOut", "pointerout"},
{"topPointerLeave", "pointerleave"},
{"topGotPointerCapture", "gotpointercapture"},
{"topLostPointerCapture", "lostpointercapture"},
{"topTouchStart", "touchstart"},
{"topTouchEnd", "touchend"},
{"topTouchCancel", "touchcancel"},
{"topKeyDown", "keydown"},
{"topKeyPress", "keypress"},
{"topKeyUp", "keyup"},
{"topBeforeInput", "beforeinput"},
{"topInput", "input"},
{"topCompositionStart", "compositionstart"},
{"topCompositionUpdate", "compositionupdate"},
{"topCompositionEnd", "compositionend"},
{"topDragStart", "dragstart"},
{"topDragEnd", "dragend"},
{"topDragEnter", "dragenter"},
{"topDragLeave", "dragleave"},
{"topDragOver", "dragover"},
{"topDrop", "drop"},
};
EventTag PerformanceEntryReporter::onEventStart(const char *name) {
if (!isReportingEvents()) {
return 0;
}
auto it = SUPPORTED_EVENTS.find(name);
if (it == SUPPORTED_EVENTS.end()) {
return 0;
}
const char *reportedName = it->second;
sCurrentEventTag_++;
if (sCurrentEventTag_ == 0) {
// The tag wrapped around (which is highly unlikely, but still)
sCurrentEventTag_ = 1;
}
auto timeStamp = JSExecutor::performanceNow();
{
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
eventsInFlight_.emplace(std::make_pair(
sCurrentEventTag_, EventEntry{reportedName, 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;
// TODO: Define the way to assign interaction IDs to the event chains
// (T141358175)
const uint32_t interactionId = 0;
event(
name,
entry.startTime,
timeStamp - entry.startTime,
entry.dispatchTime,
timeStamp,
interactionId);
eventsInFlight_.erase(it);
}
}
} // namespace facebook::react