first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_performance_timeline_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_performance_timeline OBJECT ${react_performance_timeline_SRC})
target_compile_reactnative_options(react_performance_timeline PRIVATE)
target_compile_options(react_performance_timeline PRIVATE -Wpedantic)
target_include_directories(react_performance_timeline PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_performance_timeline
jsinspector_tracing
reactperflogger
react_featureflags
react_timing
folly_runtime)

View File

@@ -0,0 +1,137 @@
/*
* 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.
*/
#pragma once
#include <algorithm>
#include <functional>
#include <vector>
namespace facebook::react {
/**
* A container for storing entries of type T, with the following properties:
* - It can only grow up to a specified max size
* - It's a circular buffer (the oldest elements are dropped if reached max
* size and adding a new element)
*
* Note that the space for maxSize elements is reserved on construction. This
* ensures that pointers to elements remain stable across add() operations.
*/
template <class T>
class CircularBuffer {
public:
explicit CircularBuffer(size_t maxSize) : maxSize_(maxSize)
{
entries_.reserve(maxSize_);
}
/**
* Adds (pushes) element into the buffer.
*
* Returns the result of the operation, which will depend on whether the
* buffer reached the max allowed size, in which case `true` is returned. If
* no items were overridden `false` is returned.
*/
bool add(const T &el)
{
if (entries_.size() < maxSize_) {
// Haven't reached max buffer size yet, just add and grow the buffer
entries_.emplace_back(el);
return false;
} else {
// Overwrite the oldest (but already consumed) element in the buffer
entries_[position_] = el;
position_ = (position_ + 1) % entries_.size();
return true;
}
}
T &operator[](size_t idx)
{
return entries_[(position_ + idx) % entries_.size()];
}
size_t size() const
{
return entries_.size();
}
void clear()
{
entries_.clear();
position_ = 0;
}
/**
* Clears buffer entries by predicate
*/
void clear(std::function<bool(const T &)> predicate)
{
std::vector<T> entries;
entries.reserve(maxSize_);
for (size_t i = 0; i < entries_.size(); i++) {
T &el = entries_[(i + position_) % entries_.size()];
if (predicate(el)) {
continue;
}
entries.push_back(std::move(el));
}
position_ = 0;
entries.swap(entries_);
}
/**
* Retrieves buffer entries, whether consumed or not
*/
std::vector<T> getEntries() const
{
std::vector<T> res;
getEntries(res);
return res;
}
/**
* Retrieves buffer entries, whether consumed or not, with predicate
*/
std::vector<T> getEntries(std::function<bool(const T &)> predicate) const
{
std::vector<T> res;
getEntries(res, predicate);
return res;
}
void getEntries(std::vector<T> &res) const
{
const size_t oldSize = res.size();
res.resize(oldSize + entries_.size());
std::copy(entries_.begin() + position_, entries_.end(), res.begin() + oldSize);
std::copy(entries_.begin(), entries_.begin() + position_, res.begin() + oldSize + entries_.size() - position_);
}
void getEntries(std::vector<T> &res, std::function<bool(const T &)> predicate) const
{
for (size_t i = 0; i < entries_.size(); i++) {
const T &el = entries_[(i + position_) % entries_.size()];
if (predicate(el)) {
res.push_back(el);
}
}
}
private:
std::vector<T> entries_;
const size_t maxSize_;
// Current starting position in the circular buffer:
size_t position_{0};
};
} // namespace facebook::react

View File

@@ -0,0 +1,97 @@
/*
* 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.
*/
#pragma once
#include <react/timing/primitives.h>
#include <optional>
#include <string>
#include <variant>
namespace facebook::react {
using PerformanceEntryInteractionId = uint32_t;
enum class PerformanceEntryType {
// We need to preserve these values for backwards compatibility.
MARK = 1,
MEASURE = 2,
EVENT = 3,
LONGTASK = 4,
RESOURCE = 5,
_NEXT = 6,
};
struct AbstractPerformanceEntry {
std::string name;
HighResTimeStamp startTime;
HighResDuration duration = HighResDuration::zero();
};
struct PerformanceMark : AbstractPerformanceEntry {
static constexpr PerformanceEntryType entryType = PerformanceEntryType::MARK;
};
struct PerformanceMeasure : AbstractPerformanceEntry {
static constexpr PerformanceEntryType entryType = PerformanceEntryType::MEASURE;
};
struct PerformanceEventTiming : AbstractPerformanceEntry {
static constexpr PerformanceEntryType entryType = PerformanceEntryType::EVENT;
HighResTimeStamp processingStart;
HighResTimeStamp processingEnd;
// Custom RN extension not exposed to JS for now.
// It's the same "taskEndTime" defined in the spec for the Event Loop:
// https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
HighResTimeStamp taskEndTime;
PerformanceEntryInteractionId interactionId;
};
struct PerformanceLongTaskTiming : AbstractPerformanceEntry {
static constexpr PerformanceEntryType entryType = PerformanceEntryType::LONGTASK;
};
struct PerformanceResourceTiming : AbstractPerformanceEntry {
static constexpr PerformanceEntryType entryType = PerformanceEntryType::RESOURCE;
/** Aligns with `startTime`. */
HighResTimeStamp fetchStart;
HighResTimeStamp requestStart;
std::optional<HighResTimeStamp> connectStart;
std::optional<HighResTimeStamp> connectEnd;
std::optional<HighResTimeStamp> responseStart;
/** Aligns with `duration`. */
std::optional<HighResTimeStamp> responseEnd;
int responseStatus;
std::string contentType;
int encodedBodySize;
int decodedBodySize;
};
using PerformanceEntry = std::variant<
PerformanceMark,
PerformanceMeasure,
PerformanceEventTiming,
PerformanceLongTaskTiming,
PerformanceResourceTiming>;
struct PerformanceEntrySorter {
bool operator()(const PerformanceEntry &lhs, const PerformanceEntry &rhs)
{
return std::visit(
[](const auto &left, const auto &right) {
if (left.startTime != right.startTime) {
return left.startTime < right.startTime;
}
return left.duration < right.duration;
},
lhs,
rhs);
}
};
} // namespace facebook::react

View File

@@ -0,0 +1,38 @@
/*
* 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.
*/
#pragma once
#include <vector>
#include "PerformanceEntry.h"
namespace facebook::react {
// Default duration threshold for reporting performance entries (0 means "report
// all")
constexpr HighResDuration DEFAULT_DURATION_THRESHOLD = HighResDuration::zero();
/**
* Abstract performance entry buffer with reporting flags.
* Subtypes differ on how entries are stored.
*/
class PerformanceEntryBuffer {
public:
HighResDuration durationThreshold = DEFAULT_DURATION_THRESHOLD;
size_t droppedEntriesCount{0};
explicit PerformanceEntryBuffer() = default;
virtual ~PerformanceEntryBuffer() = default;
virtual void add(const PerformanceEntry &entry) = 0;
virtual void getEntries(std::vector<PerformanceEntry> &target) const = 0;
virtual void getEntries(std::vector<PerformanceEntry> &target, const std::string &name) const = 0;
virtual void clear() = 0;
virtual void clear(const std::string &name) = 0;
};
} // namespace facebook::react

View File

@@ -0,0 +1,47 @@
/*
* 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 "PerformanceEntryCircularBuffer.h"
#include <variant>
namespace facebook::react {
void PerformanceEntryCircularBuffer::add(const PerformanceEntry& entry) {
if (buffer_.add(entry)) {
droppedEntriesCount += 1;
}
}
void PerformanceEntryCircularBuffer::getEntries(
std::vector<PerformanceEntry>& target) const {
buffer_.getEntries(target);
}
void PerformanceEntryCircularBuffer::getEntries(
std::vector<PerformanceEntry>& target,
const std::string& name) const {
buffer_.getEntries(target, [&](const PerformanceEntry& entry) {
return std::visit(
[&name](const auto& entryData) { return entryData.name == name; },
entry);
});
}
void PerformanceEntryCircularBuffer::clear() {
buffer_.clear();
}
void PerformanceEntryCircularBuffer::clear(const std::string& name) {
buffer_.clear([&](const PerformanceEntry& entry) {
return std::visit(
[&name](const auto& entryData) { return entryData.name == name; },
entry);
});
}
} // namespace facebook::react

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
#pragma once
#include "CircularBuffer.h"
#include "PerformanceEntryBuffer.h"
namespace facebook::react {
class PerformanceEntryCircularBuffer : public PerformanceEntryBuffer {
public:
explicit PerformanceEntryCircularBuffer(size_t size) : buffer_(size) {}
~PerformanceEntryCircularBuffer() override = default;
void add(const PerformanceEntry &entry) override;
void getEntries(std::vector<PerformanceEntry> &target) const override;
void getEntries(std::vector<PerformanceEntry> &target, const std::string &name) const override;
void clear() override;
void clear(const std::string &name) override;
private:
CircularBuffer<PerformanceEntry> buffer_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,60 @@
/*
* 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 "PerformanceEntryKeyedBuffer.h"
#include <string>
namespace facebook::react {
void PerformanceEntryKeyedBuffer::add(const PerformanceEntry& entry) {
auto name =
std::visit([](const auto& entryData) { return entryData.name; }, entry);
auto node = entryMap_.find(name);
if (node != entryMap_.end()) {
node->second.push_back(entry);
} else {
entryMap_.emplace(name, std::vector<PerformanceEntry>{entry});
}
}
void PerformanceEntryKeyedBuffer::getEntries(
std::vector<PerformanceEntry>& target) const {
for (const auto& [_, entries] : entryMap_) {
target.insert(target.end(), entries.begin(), entries.end());
}
}
void PerformanceEntryKeyedBuffer::getEntries(
std::vector<PerformanceEntry>& target,
const std::string& name) const {
if (auto node = entryMap_.find(name); node != entryMap_.end()) {
target.insert(target.end(), node->second.begin(), node->second.end());
}
}
void PerformanceEntryKeyedBuffer::clear() {
entryMap_.clear();
}
void PerformanceEntryKeyedBuffer::clear(const std::string& name) {
entryMap_.erase(name);
}
std::optional<PerformanceEntry> PerformanceEntryKeyedBuffer::find(
const std::string& name) const {
if (auto node = entryMap_.find(name); node != entryMap_.end()) {
if (!node->second.empty()) {
return std::make_optional<PerformanceEntry>(node->second.back());
}
}
return std::nullopt;
}
} // namespace facebook::react

View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
#pragma once
#include <optional>
#include <unordered_map>
#include <vector>
#include "PerformanceEntryBuffer.h"
namespace facebook::react {
class PerformanceEntryKeyedBuffer : public PerformanceEntryBuffer {
public:
PerformanceEntryKeyedBuffer() = default;
void add(const PerformanceEntry &entry) override;
void getEntries(std::vector<PerformanceEntry> &target) const override;
void getEntries(std::vector<PerformanceEntry> &target, const std::string &name) const override;
void clear() override;
void clear(const std::string &name) override;
std::optional<PerformanceEntry> find(const std::string &name) const;
private:
std::unordered_map<std::string, std::vector<PerformanceEntry>> entryMap_{};
};
} // namespace facebook::react

View File

@@ -0,0 +1,390 @@
/*
* 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 <jsinspector-modern/tracing/PerformanceTracer.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/timing/primitives.h>
#include <reactperflogger/ReactPerfettoLogger.h>
#ifdef WITH_PERFETTO
#include <reactperflogger/ReactPerfetto.h>
#endif
#include <variant>
namespace facebook::react {
namespace {
std::vector<PerformanceEntryType> getSupportedEntryTypesInternal() {
std::vector<PerformanceEntryType> supportedEntryTypes{
PerformanceEntryType::MARK,
PerformanceEntryType::MEASURE,
PerformanceEntryType::EVENT,
PerformanceEntryType::LONGTASK,
};
if (ReactNativeFeatureFlags::enableResourceTimingAPI()) {
supportedEntryTypes.emplace_back(PerformanceEntryType::RESOURCE);
}
return supportedEntryTypes;
}
std::optional<std::string> getTrackFromDetail(folly::dynamic& detail) {
if (!detail.isObject()) {
return std::nullopt;
}
auto maybeDevtools = detail["devtools"];
if (!maybeDevtools.isObject()) {
return std::nullopt;
}
auto maybeTrack = maybeDevtools["track"];
if (!maybeTrack.isString()) {
return std::nullopt;
}
return maybeTrack.asString();
}
std::optional<std::string> getTrackGroupFromDetail(folly::dynamic& detail) {
if (!detail.isObject()) {
return std::nullopt;
}
auto maybeDevtools = detail["devtools"];
if (!maybeDevtools.isObject()) {
return std::nullopt;
}
auto maybeTrackGroup = maybeDevtools["trackGroup"];
if (!maybeTrackGroup.isString()) {
return std::nullopt;
}
return maybeTrackGroup.asString();
}
} // namespace
std::shared_ptr<PerformanceEntryReporter>&
PerformanceEntryReporter::getInstance() {
static auto instance = std::make_shared<PerformanceEntryReporter>();
return instance;
}
PerformanceEntryReporter::PerformanceEntryReporter()
: observerRegistry_(std::make_unique<PerformanceObserverRegistry>()) {
#ifdef WITH_PERFETTO
initializePerfetto();
#endif
}
void PerformanceEntryReporter::addEventListener(
PerformanceEntryReporterEventListener* listener) {
std::unique_lock lock(listenersMutex_);
eventListeners_.push_back(listener);
}
void PerformanceEntryReporter::removeEventListener(
PerformanceEntryReporterEventListener* listener) {
std::unique_lock lock(listenersMutex_);
auto it = std::find(eventListeners_.begin(), eventListeners_.end(), listener);
if (it != eventListeners_.end()) {
eventListeners_.erase(it);
}
}
std::vector<PerformanceEntryType>
PerformanceEntryReporter::getSupportedEntryTypes() {
static std::vector<PerformanceEntryType> supportedEntries =
getSupportedEntryTypesInternal();
return supportedEntries;
}
uint32_t PerformanceEntryReporter::getDroppedEntriesCount(
PerformanceEntryType entryType) const noexcept {
std::shared_lock lock(buffersMutex_);
return (uint32_t)getBuffer(entryType).droppedEntriesCount;
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntries() const {
std::vector<PerformanceEntry> entries;
getEntries(entries);
return entries;
}
void PerformanceEntryReporter::getEntries(
std::vector<PerformanceEntry>& dest) const {
std::shared_lock lock(buffersMutex_);
for (auto entryType : getSupportedEntryTypes()) {
getBuffer(entryType).getEntries(dest);
}
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntries(
PerformanceEntryType entryType) const {
std::vector<PerformanceEntry> dest;
getEntries(dest, entryType);
return dest;
}
void PerformanceEntryReporter::getEntries(
std::vector<PerformanceEntry>& dest,
PerformanceEntryType entryType) const {
std::shared_lock lock(buffersMutex_);
getBuffer(entryType).getEntries(dest);
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntries(
PerformanceEntryType entryType,
const std::string& entryName) const {
std::vector<PerformanceEntry> entries;
getEntries(entries, entryType, entryName);
return entries;
}
void PerformanceEntryReporter::getEntries(
std::vector<PerformanceEntry>& dest,
PerformanceEntryType entryType,
const std::string& entryName) const {
std::shared_lock lock(buffersMutex_);
getBuffer(entryType).getEntries(dest, entryName);
}
void PerformanceEntryReporter::clearEntries() {
std::unique_lock lock(buffersMutex_);
for (auto entryType : getSupportedEntryTypes()) {
getBufferRef(entryType).clear();
}
}
void PerformanceEntryReporter::clearEntries(PerformanceEntryType entryType) {
std::unique_lock lock(buffersMutex_);
getBufferRef(entryType).clear();
}
void PerformanceEntryReporter::clearEntries(
PerformanceEntryType entryType,
const std::string& entryName) {
std::unique_lock lock(buffersMutex_);
getBufferRef(entryType).clear(entryName);
}
void PerformanceEntryReporter::reportMark(
const std::string& name,
const HighResTimeStamp startTime,
UserTimingDetailProvider&& detailProvider) {
const auto entry = PerformanceMark{{.name = name, .startTime = startTime}};
traceMark(entry, std::move(detailProvider));
// Add to buffers & notify observers
{
std::unique_lock lock(buffersMutex_);
markBuffer_.add(entry);
}
observerRegistry_->queuePerformanceEntry(entry);
}
void PerformanceEntryReporter::reportMeasure(
const std::string& name,
HighResTimeStamp startTime,
HighResDuration duration,
const std::optional<UserTimingDetailProvider>& detailProvider) {
const auto entry = PerformanceMeasure{
{.name = std::string(name),
.startTime = startTime,
.duration = duration}};
traceMeasure(entry, detailProvider);
// Add to buffers & notify observers
{
std::unique_lock lock(buffersMutex_);
measureBuffer_.add(entry);
}
observerRegistry_->queuePerformanceEntry(entry);
std::vector<PerformanceEntryReporterEventListener*> listenersCopy;
{
std::shared_lock lock(listenersMutex_);
listenersCopy = eventListeners_;
}
for (auto* listener : listenersCopy) {
listener->onMeasureEntry(entry, detailProvider);
}
}
void PerformanceEntryReporter::clearEventCounts() {
eventCounts_.clear();
}
std::optional<HighResTimeStamp> PerformanceEntryReporter::getMarkTime(
const std::string& markName) const {
std::shared_lock lock(buffersMutex_);
if (auto it = markBuffer_.find(markName); it) {
return std::visit(
[](const auto& entryData) { return entryData.startTime; }, *it);
}
return std::nullopt;
}
void PerformanceEntryReporter::reportEvent(
const std::string& name,
HighResTimeStamp startTime,
HighResDuration duration,
HighResTimeStamp processingStart,
HighResTimeStamp processingEnd,
HighResTimeStamp taskEndTime,
uint32_t interactionId) {
eventCounts_[name]++;
if (duration < eventBuffer_.durationThreshold) {
// The entries duration is lower than the desired reporting threshold,
// skip
return;
}
const auto entry = PerformanceEventTiming{
{.name = name, .startTime = startTime, .duration = duration},
processingStart,
processingEnd,
taskEndTime,
interactionId};
{
std::unique_lock lock(buffersMutex_);
eventBuffer_.add(entry);
}
// TODO(T198982346): Log interaction events to jsinspector_modern
observerRegistry_->queuePerformanceEntry(entry);
std::vector<PerformanceEntryReporterEventListener*> listenersCopy;
{
std::shared_lock lock(listenersMutex_);
listenersCopy = eventListeners_;
}
for (auto* listener : listenersCopy) {
listener->onEventTimingEntry(entry);
}
}
void PerformanceEntryReporter::reportLongTask(
HighResTimeStamp startTime,
HighResDuration duration) {
const auto entry = PerformanceLongTaskTiming{
{.name = std::string{"self"},
.startTime = startTime,
.duration = duration}};
{
std::unique_lock lock(buffersMutex_);
longTaskBuffer_.add(entry);
}
observerRegistry_->queuePerformanceEntry(entry);
}
void PerformanceEntryReporter::reportResourceTiming(
const std::string& url,
HighResTimeStamp fetchStart,
HighResTimeStamp requestStart,
std::optional<HighResTimeStamp> connectStart,
std::optional<HighResTimeStamp> connectEnd,
HighResTimeStamp responseStart,
HighResTimeStamp responseEnd,
int responseStatus,
const std::string& contentType,
int encodedBodySize,
int decodedBodySize) {
const auto entry = PerformanceResourceTiming{
{.name = url, .startTime = fetchStart},
fetchStart,
requestStart,
connectStart,
connectEnd,
responseStart,
responseEnd,
responseStatus,
contentType,
encodedBodySize,
decodedBodySize,
};
// Add to buffers & notify observers
{
std::unique_lock lock(buffersMutex_);
resourceTimingBuffer_.add(entry);
}
observerRegistry_->queuePerformanceEntry(entry);
}
void PerformanceEntryReporter::traceMark(
const PerformanceMark& entry,
UserTimingDetailProvider&& detailProvider) const {
auto& performanceTracer =
jsinspector_modern::tracing::PerformanceTracer::getInstance();
if (ReactPerfettoLogger::isTracing()) {
ReactPerfettoLogger::mark(entry.name, entry.startTime);
}
if (performanceTracer.isTracing()) {
performanceTracer.reportMark(
entry.name,
entry.startTime,
detailProvider != nullptr ? detailProvider() : nullptr);
}
}
void PerformanceEntryReporter::traceMeasure(
const PerformanceMeasure& entry,
const std::optional<UserTimingDetailProvider>& detailProvider) const {
auto& performanceTracer =
jsinspector_modern::tracing::PerformanceTracer::getInstance();
if (performanceTracer.isTracing() || ReactPerfettoLogger::isTracing()) {
auto detail = detailProvider && detailProvider.has_value()
? (*detailProvider)()
: nullptr;
if (ReactPerfettoLogger::isTracing()) {
ReactPerfettoLogger::measure(
entry.name,
entry.startTime,
entry.startTime + entry.duration,
detail != nullptr ? getTrackFromDetail(detail) : std::nullopt,
detail != nullptr ? getTrackGroupFromDetail(detail) : std::nullopt);
}
if (performanceTracer.isTracing()) {
performanceTracer.reportMeasure(
entry.name, entry.startTime, entry.duration, std::move(detail));
}
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,178 @@
/*
* 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.
*/
#pragma once
#include "PerformanceEntryCircularBuffer.h"
#include "PerformanceEntryKeyedBuffer.h"
#include "PerformanceEntryReporterListeners.h"
#include "PerformanceObserverRegistry.h"
#include <folly/dynamic.h>
#include <react/timing/primitives.h>
#include <memory>
#include <optional>
#include <shared_mutex>
#include <vector>
namespace facebook::react {
// Aligned with maxBufferSize implemented by browsers
// https://w3c.github.io/timing-entrytypes-registry/#registry
constexpr size_t EVENT_BUFFER_SIZE = 150;
constexpr size_t LONG_TASK_BUFFER_SIZE = 200;
constexpr size_t RESOURCE_TIMING_BUFFER_SIZE = 250;
constexpr HighResDuration LONG_TASK_DURATION_THRESHOLD = HighResDuration::fromMilliseconds(50);
class PerformanceEntryReporter {
public:
PerformanceEntryReporter();
// NOTE: This class is not thread safe, make sure that the calls are made from
// the same thread.
// TODO: Consider passing it as a parameter to the corresponding modules at
// creation time instead of having the singleton.
static std::shared_ptr<PerformanceEntryReporter> &getInstance();
PerformanceObserverRegistry &getObserverRegistry()
{
return *observerRegistry_;
}
std::vector<PerformanceEntry> getEntries() const;
void getEntries(std::vector<PerformanceEntry> &dest) const;
std::vector<PerformanceEntry> getEntries(PerformanceEntryType entryType) const;
void getEntries(std::vector<PerformanceEntry> &dest, PerformanceEntryType entryType) const;
std::vector<PerformanceEntry> getEntries(PerformanceEntryType entryType, const std::string &entryName) const;
void getEntries(std::vector<PerformanceEntry> &dest, PerformanceEntryType entryType, const std::string &entryName)
const;
void clearEntries();
void clearEntries(PerformanceEntryType entryType);
void clearEntries(PerformanceEntryType entryType, const std::string &entryName);
void addEventListener(PerformanceEntryReporterEventListener *listener);
void removeEventListener(PerformanceEntryReporterEventListener *listener);
static std::vector<PerformanceEntryType> getSupportedEntryTypes();
uint32_t getDroppedEntriesCount(PerformanceEntryType type) const noexcept;
const std::unordered_map<std::string, uint32_t> &getEventCounts() const
{
return eventCounts_;
}
void clearEventCounts();
std::optional<HighResTimeStamp> getMarkTime(const std::string &markName) const;
using UserTimingDetailProvider = std::function<folly::dynamic()>;
void
reportMark(const std::string &name, HighResTimeStamp startTime, UserTimingDetailProvider &&detailProvider = nullptr);
void reportMeasure(
const std::string &name,
HighResTimeStamp startTime,
HighResDuration duration,
const std::optional<UserTimingDetailProvider> &detailProvider = std::nullopt);
void reportEvent(
const std::string &name,
HighResTimeStamp startTime,
HighResDuration duration,
HighResTimeStamp processingStart,
HighResTimeStamp processingEnd,
HighResTimeStamp taskEndTime,
uint32_t interactionId);
void reportLongTask(HighResTimeStamp startTime, HighResDuration duration);
void reportResourceTiming(
const std::string &url,
HighResTimeStamp fetchStart,
HighResTimeStamp requestStart,
std::optional<HighResTimeStamp> connectStart,
std::optional<HighResTimeStamp> connectEnd,
HighResTimeStamp responseStart,
HighResTimeStamp responseEnd,
int responseStatus,
const std::string &contentType,
int encodedBodySize,
int decodedBodySize);
private:
std::unique_ptr<PerformanceObserverRegistry> observerRegistry_;
mutable std::shared_mutex buffersMutex_;
PerformanceEntryCircularBuffer eventBuffer_{EVENT_BUFFER_SIZE};
PerformanceEntryCircularBuffer longTaskBuffer_{LONG_TASK_BUFFER_SIZE};
PerformanceEntryCircularBuffer resourceTimingBuffer_{RESOURCE_TIMING_BUFFER_SIZE};
PerformanceEntryKeyedBuffer markBuffer_;
PerformanceEntryKeyedBuffer measureBuffer_;
std::unordered_map<std::string, uint32_t> eventCounts_;
mutable std::shared_mutex listenersMutex_;
std::vector<PerformanceEntryReporterEventListener *> eventListeners_{};
const inline PerformanceEntryBuffer &getBuffer(PerformanceEntryType entryType) const
{
switch (entryType) {
case PerformanceEntryType::EVENT:
return eventBuffer_;
case PerformanceEntryType::MARK:
return markBuffer_;
case PerformanceEntryType::MEASURE:
return measureBuffer_;
case PerformanceEntryType::LONGTASK:
return longTaskBuffer_;
case PerformanceEntryType::RESOURCE:
return resourceTimingBuffer_;
case PerformanceEntryType::_NEXT:
throw std::logic_error("Cannot get buffer for _NEXT entry type");
default:
throw std::logic_error("Unhandled PerformanceEntryType");
}
}
inline PerformanceEntryBuffer &getBufferRef(PerformanceEntryType entryType)
{
switch (entryType) {
case PerformanceEntryType::EVENT:
return eventBuffer_;
case PerformanceEntryType::MARK:
return markBuffer_;
case PerformanceEntryType::MEASURE:
return measureBuffer_;
case PerformanceEntryType::LONGTASK:
return longTaskBuffer_;
case PerformanceEntryType::RESOURCE:
return resourceTimingBuffer_;
case PerformanceEntryType::_NEXT:
throw std::logic_error("Cannot get buffer for _NEXT entry type");
default:
throw std::logic_error("Unhandled PerformanceEntryType");
}
}
void traceMark(const PerformanceMark &entry, UserTimingDetailProvider &&detailProvider) const;
void traceMeasure(const PerformanceMeasure &entry, const std::optional<UserTimingDetailProvider> &detailProvider)
const;
void traceResourceTiming(
const PerformanceResourceTiming &entry,
const std::optional<std::string> &devtoolsRequestId,
const std::optional<std::string> &requestMethod,
const std::optional<std::string> &resourceType) const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
#pragma once
#include "PerformanceEntry.h"
#include <folly/dynamic.h>
#include <react/timing/primitives.h>
namespace facebook::react {
using UserTimingDetailProvider = std::function<folly::dynamic()>;
class PerformanceEntryReporterEventListener {
public:
virtual ~PerformanceEntryReporterEventListener() = default;
virtual void onMeasureEntry(
const PerformanceMeasure & /*entry*/,
const std::optional<UserTimingDetailProvider> & /*detailProvider*/)
{
}
virtual void onEventTimingEntry(const PerformanceEventTiming & /*entry*/) {}
};
} // namespace facebook::react

View File

@@ -0,0 +1,97 @@
/*
* 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 "PerformanceObserver.h"
#include "PerformanceEntryReporter.h"
#include <variant>
namespace facebook::react {
void PerformanceObserver::handleEntry(const PerformanceEntry& entry) {
auto entryType = std::visit(
[](const auto& entryData) { return entryData.entryType; }, entry);
if (observedTypes_.contains(entryType)) {
// https://www.w3.org/TR/event-timing/#should-add-performanceeventtiming
if (std::holds_alternative<PerformanceEventTiming>(entry) &&
std::get<PerformanceEventTiming>(entry).duration < durationThreshold_) {
// The entries duration is lower than the desired reporting threshold,
// skip
return;
}
buffer_.push_back(entry);
scheduleFlushBuffer();
}
}
std::vector<PerformanceEntry> PerformanceObserver::takeRecords() {
std::vector<PerformanceEntry> result;
buffer_.swap(result);
didScheduleFlushBuffer_ = false;
return result;
}
void PerformanceObserver::observe(
PerformanceEntryType type,
PerformanceObserverObserveSingleOptions options) {
observedTypes_.insert(type);
durationThreshold_ = options.durationThreshold;
requiresDroppedEntries_ = true;
if (options.buffered) {
auto& reporter = PerformanceEntryReporter::getInstance();
auto bufferedEntries = reporter->getEntries(type);
for (auto& bufferedEntry : bufferedEntries) {
handleEntry(bufferedEntry);
}
}
registry_.addObserver(shared_from_this());
}
void PerformanceObserver::observe(
std::unordered_set<PerformanceEntryType> types) {
observedTypes_ = std::move(types);
requiresDroppedEntries_ = false;
registry_.addObserver(shared_from_this());
}
uint32_t PerformanceObserver::getDroppedEntriesCount() noexcept {
uint32_t droppedEntriesCount = 0;
if (requiresDroppedEntries_) {
auto reporter = PerformanceEntryReporter::getInstance();
for (auto& entryType : observedTypes_) {
droppedEntriesCount += reporter->getDroppedEntriesCount(entryType);
}
requiresDroppedEntries_ = false;
}
return droppedEntriesCount;
}
void PerformanceObserver::disconnect() noexcept {
registry_.removeObserver(shared_from_this());
}
void PerformanceObserver::scheduleFlushBuffer() {
if (!didScheduleFlushBuffer_) {
didScheduleFlushBuffer_ = true;
callback_();
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,139 @@
/*
* 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.
*/
#pragma once
#include "PerformanceEntryBuffer.h"
#include "PerformanceObserverRegistry.h"
#include <react/timing/primitives.h>
#include <functional>
#include <memory>
#include <unordered_set>
#include <vector>
namespace facebook::react {
using PerformanceObserverEntryTypeFilter = std::unordered_set<PerformanceEntryType>;
using PerformanceObserverCallback = std::function<void()>;
/**
* Represents subset of spec's `PerformanceObserverInit` that is allowed for
* multiple types.
*
* https://w3c.github.io/performance-timeline/#performanceobserverinit-dictionary
*/
struct PerformanceObserverObserveMultipleOptions {
HighResDuration durationThreshold = DEFAULT_DURATION_THRESHOLD;
};
/**
* Represents subset of spec's `PerformanceObserverInit` that is allowed for
* single type.
*
* https://w3c.github.io/performance-timeline/#performanceobserverinit-dictionary
*/
struct PerformanceObserverObserveSingleOptions {
bool buffered = false;
HighResDuration durationThreshold = DEFAULT_DURATION_THRESHOLD;
};
/**
* Represents native counterpart of performance timeline PerformanceObserver
* class. Each instance has its own entry buffer and can listen for different
* performance entry types.
*
* Entries are pushed to the observer by the `PerformanceEntryReporter` class,
* through the `PerformanceObserverRegistry` class which acts as a central hub.
*/
class PerformanceObserver : public std::enable_shared_from_this<PerformanceObserver> {
private:
struct PrivateUseCreateMethod {
explicit PrivateUseCreateMethod() = default;
};
public:
explicit PerformanceObserver(
PrivateUseCreateMethod /*unused*/,
PerformanceObserverRegistry &registry,
PerformanceObserverCallback &&callback)
: registry_(registry), callback_(std::move(callback))
{
}
static std::shared_ptr<PerformanceObserver> create(
PerformanceObserverRegistry &registry,
PerformanceObserverCallback &&callback)
{
return std::make_shared<PerformanceObserver>(PrivateUseCreateMethod(), registry, std::move(callback));
}
~PerformanceObserver() = default;
/**
* Append entry to the buffer if this observer should handle this entry.
*/
void handleEntry(const PerformanceEntry &entry);
/**
* Returns current observer buffer and clears it.
*
* Spec:
* https://w3c.github.io/performance-timeline/#takerecords-method
*/
[[nodiscard]] std::vector<PerformanceEntry> takeRecords();
/**
* Configures the observer to watch for specified entry type.
*
* This operation resets and overrides previous configurations. So consecutive
* calls to this methods remove any previous watch configuration (as per
* spec).
*/
void observe(PerformanceEntryType type, PerformanceObserverObserveSingleOptions options = {});
/**
* Configures the observer to watch for specified entry type.
*
* This operation resets and overrides previous configurations. So consecutive
* calls to this methods remove any previous watch configuration (as per
* spec).
*/
void observe(std::unordered_set<PerformanceEntryType> types);
/**
* Disconnects observer from the registry
*/
void disconnect() noexcept;
/**
* Internal function called by JS bridge to get number of dropped entries
* count counted at call time.
*/
uint32_t getDroppedEntriesCount() noexcept;
private:
void scheduleFlushBuffer();
PerformanceObserverRegistry &registry_;
PerformanceObserverCallback callback_;
PerformanceObserverEntryTypeFilter observedTypes_;
/// https://www.w3.org/TR/event-timing/#sec-modifications-perf-timeline
HighResDuration durationThreshold_ = DEFAULT_DURATION_THRESHOLD;
std::vector<PerformanceEntry> buffer_;
bool didScheduleFlushBuffer_ = false;
bool requiresDroppedEntries_ = false;
};
inline bool operator==(const PerformanceObserver &lhs, const PerformanceObserver &rhs)
{
return &lhs == &rhs;
}
} // namespace facebook::react

View File

@@ -0,0 +1,34 @@
/*
* 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 "PerformanceObserverRegistry.h"
#include "PerformanceObserver.h"
namespace facebook::react {
void PerformanceObserverRegistry::addObserver(
std::shared_ptr<PerformanceObserver> observer) {
std::lock_guard guard(observersMutex_);
observers_.insert(observer);
}
void PerformanceObserverRegistry::removeObserver(
std::shared_ptr<PerformanceObserver> observer) {
std::lock_guard guard(observersMutex_);
observers_.erase(observer);
}
void PerformanceObserverRegistry::queuePerformanceEntry(
const PerformanceEntry& entry) {
std::lock_guard lock(observersMutex_);
for (auto& observer : observers_) {
observer->handleEntry(entry);
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,51 @@
/*
* 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.
*/
#pragma once
#include <memory>
#include <mutex>
#include <set>
#include "PerformanceEntry.h"
namespace facebook::react {
class PerformanceObserver;
/**
* PerformanceObserverRegistry acts as a container for known performance
* observer instances.
*
* You can queue performance entries through this registry, which then delegates
* the entry to all registered observers.
*/
class PerformanceObserverRegistry {
public:
PerformanceObserverRegistry() = default;
/**
* Adds observer to the registry.
*/
void addObserver(std::shared_ptr<PerformanceObserver> observer);
/**
* Removes observer from the registry.
*/
void removeObserver(std::shared_ptr<PerformanceObserver> observer);
/**
* Delegates specified performance `entry` to all registered observers
* in this registry.
*/
void queuePerformanceEntry(const PerformanceEntry &entry);
private:
mutable std::mutex observersMutex_;
std::set<std::shared_ptr<PerformanceObserver>, std::owner_less<std::shared_ptr<PerformanceObserver>>> observers_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,50 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
header_search_paths = []
if ENV['USE_FRAMEWORKS']
header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the performancetimeline access its own files
end
Pod::Spec.new do |s|
s.name = "React-performancetimeline"
s.version = version
s.summary = "React performance timeline utilities"
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = podspec_sources("**/*.{cpp,h}", "**/*.h")
s.header_dir = "react/performance/timeline"
s.exclude_files = "tests"
s.pod_target_xcconfig = {
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"HEADER_SEARCH_PATHS" => header_search_paths.join(' ')}
resolve_use_frameworks(s, header_mappings_dir: "../../..", module_name: "React_performancetimeline")
s.dependency "React-featureflags"
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
s.dependency "React-timing"
s.dependency "React-perflogger"
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
end

View File

@@ -0,0 +1,127 @@
/*
* 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 <ostream>
#include <gtest/gtest.h>
#include "../CircularBuffer.h"
namespace facebook::react {
using namespace facebook::react;
constexpr auto OK = false;
constexpr auto OVERWRITE = true;
TEST(CircularBuffer, CanAddAndRetrieveElements) {
CircularBuffer<int> buffer{5};
ASSERT_EQ(OK, buffer.add(1));
ASSERT_EQ(OK, buffer.add(2));
ASSERT_EQ(1, buffer[0]);
ASSERT_EQ(2, buffer[1]);
ASSERT_EQ(2, buffer.size());
ASSERT_EQ(std::vector<int>({1, 2}), buffer.getEntries());
ASSERT_EQ(OK, buffer.add(3));
ASSERT_EQ(3, buffer.size());
ASSERT_EQ(std::vector<int>({1, 2, 3}), buffer.getEntries());
ASSERT_EQ(1, buffer[0]);
ASSERT_EQ(2, buffer[1]);
ASSERT_EQ(3, buffer[2]);
}
TEST(BoundedConsumableBuffer, WrapsAroundCorrectly) {
CircularBuffer<int> buffer(3);
ASSERT_EQ(OK, buffer.add(1));
ASSERT_EQ(OK, buffer.add(2));
ASSERT_EQ(std::vector<int>({1, 2}), buffer.getEntries());
ASSERT_EQ(OK, buffer.add(3));
ASSERT_EQ(std::vector<int>({1, 2, 3}), buffer.getEntries());
ASSERT_EQ(OVERWRITE, buffer.add(4));
ASSERT_EQ(OVERWRITE, buffer.add(5));
ASSERT_EQ(std::vector<int>({3, 4, 5}), buffer.getEntries());
ASSERT_EQ(OVERWRITE, buffer.add(6));
ASSERT_EQ(OVERWRITE, buffer.add(7));
ASSERT_EQ(std::vector<int>({5, 6, 7}), buffer.getEntries());
ASSERT_EQ(OVERWRITE, buffer.add(8));
ASSERT_EQ(OVERWRITE, buffer.add(9));
ASSERT_EQ(OVERWRITE, buffer.add(10));
ASSERT_EQ(std::vector<int>({8, 9, 10}), buffer.getEntries());
ASSERT_EQ(8, buffer[0]);
ASSERT_EQ(9, buffer[1]);
ASSERT_EQ(10, buffer[2]);
}
TEST(BoundedConsumableBuffer, CanClearByPredicate) {
CircularBuffer<int> buffer(5);
buffer.add(1);
buffer.add(0);
buffer.add(2);
buffer.add(0);
buffer.add(3);
buffer.clear([](const int& el) { return el == 0; });
ASSERT_EQ(std::vector<int>({1, 2, 3}), buffer.getEntries());
buffer.add(0);
buffer.add(4);
buffer.clear([](const int& el) { return el == 0; });
ASSERT_EQ(std::vector<int>({1, 2, 3, 4}), buffer.getEntries());
}
TEST(BoundedConsumableBuffer, CanClearBeforeReachingMaxSize) {
CircularBuffer<int> buffer(5);
buffer.add(1);
buffer.add(2);
buffer.add(3);
buffer.clear([](const int&) { return false; }); // no-op clear
ASSERT_EQ(std::vector<int>({1, 2, 3}), buffer.getEntries());
buffer.add(4);
buffer.add(5);
ASSERT_EQ(std::vector<int>({1, 2, 3, 4, 5}), buffer.getEntries());
}
TEST(BoundedConsumableBuffer, CanGetByPredicate) {
CircularBuffer<int> buffer(5);
buffer.add(1);
buffer.add(0);
buffer.add(2);
buffer.add(0);
buffer.add(3);
ASSERT_EQ(std::vector<int>({1, 2, 3}), buffer.getEntries([](const int& el) {
return el != 0;
}));
buffer.add(0);
buffer.add(4);
ASSERT_EQ(std::vector<int>({2, 3, 4}), buffer.getEntries([](const int& el) {
return el != 0;
}));
}
} // namespace facebook::react

View File

@@ -0,0 +1,373 @@
/*
* 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 <ostream>
#include <gtest/gtest.h>
#include "../PerformanceEntryReporter.h"
#include <array>
#include <variant>
using namespace facebook::react;
namespace facebook::react {
[[maybe_unused]] static bool operator==(
const PerformanceEntry& lhs,
const PerformanceEntry& rhs) {
return std::visit(
[&](const auto& left, const auto& right) {
bool baseMatch = left.name == right.name &&
left.entryType == right.entryType &&
left.startTime == right.startTime &&
left.duration == right.duration;
if (baseMatch && left.entryType == PerformanceEntryType::EVENT) {
auto leftEventTiming = std::get<PerformanceEventTiming>(lhs);
auto rightEventTiming = std::get<PerformanceEventTiming>(rhs);
return leftEventTiming.processingStart ==
rightEventTiming.processingStart &&
leftEventTiming.processingEnd == rightEventTiming.processingEnd &&
leftEventTiming.interactionId == rightEventTiming.interactionId;
}
return baseMatch;
},
lhs,
rhs);
}
[[maybe_unused]] static std::ostream& operator<<(
std::ostream& os,
const PerformanceEntry& entry) {
static constexpr auto entryTypeNames = std::to_array<const char*>({
"PerformanceEntryType::UNDEFINED",
"PerformanceEntryType::MARK",
"PerformanceEntryType::MEASURE",
"PerformanceEntryType::EVENT",
"PerformanceEntryType::RESOURCE",
});
return std::visit(
[&](const auto& entryDetails) -> std::ostream& {
os << "{ .name = \"" << entryDetails.name << "\"" << ", .entryType = "
<< entryTypeNames[static_cast<int>(entryDetails.entryType) - 1]
<< ", .startTime = "
<< entryDetails.startTime.toDOMHighResTimeStamp()
<< ", .duration = " << entryDetails.duration.toDOMHighResTimeStamp()
<< " }";
return os;
},
entry);
}
} // namespace facebook::react
namespace {
std::vector<PerformanceEntry> toSorted(
std::vector<PerformanceEntry>&& entries) {
std::stable_sort(entries.begin(), entries.end(), PerformanceEntrySorter{});
return entries;
}
} // namespace
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) {
auto reporter = PerformanceEntryReporter::getInstance();
auto timeOrigin = HighResTimeStamp::now();
reporter->clearEntries();
reporter->reportMark("mark0", timeOrigin);
reporter->reportMark(
"mark1", timeOrigin + HighResDuration::fromMilliseconds(1));
reporter->reportMark(
"mark2", timeOrigin + HighResDuration::fromMilliseconds(2));
// Report mark0 again
reporter->reportMark(
"mark0", timeOrigin + HighResDuration::fromMilliseconds(3));
const auto entries = toSorted(reporter->getEntries());
ASSERT_EQ(4, entries.size());
const std::vector<PerformanceEntry> expected = {
PerformanceMark{
{.name = "mark0",
.startTime = timeOrigin,
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark1",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(2),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark0",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(3),
.duration = HighResDuration::zero()}}};
ASSERT_EQ(expected, entries);
}
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) {
auto reporter = PerformanceEntryReporter::getInstance();
auto timeOrigin = HighResTimeStamp::now();
reporter->clearEntries();
reporter->reportMark("mark0", timeOrigin);
reporter->reportMark(
"mark1", timeOrigin + HighResDuration::fromMilliseconds(1));
reporter->reportMark(
"mark2", timeOrigin + HighResDuration::fromMilliseconds(2));
reporter->reportMeasure(
"measure0", timeOrigin, HighResDuration::fromMilliseconds(2));
reporter->reportMeasure(
"measure1", timeOrigin, HighResDuration::fromMilliseconds(3));
reporter->reportMark(
"mark3", timeOrigin + HighResDuration::fromNanoseconds(2.5 * 1e6));
reporter->reportMeasure(
"measure2",
timeOrigin + HighResDuration::fromMilliseconds(2),
HighResDuration::fromMilliseconds(2));
reporter->reportMark(
"mark4", timeOrigin + HighResDuration::fromMilliseconds(3));
const auto entries = toSorted(reporter->getEntries());
const std::vector<PerformanceEntry> expected = {
PerformanceMark{
{.name = "mark0",
.startTime = timeOrigin,
.duration = HighResDuration::zero()}},
PerformanceMeasure{
{.name = "measure0",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(2)}},
PerformanceMeasure{
{.name = "measure1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(3)}},
PerformanceMark{
{.name = "mark1",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(2),
.duration = HighResDuration::zero()}},
PerformanceMeasure{
{.name = "measure2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(2),
.duration = HighResDuration::fromMilliseconds(2)}},
PerformanceMark{
{.name = "mark3",
.startTime =
timeOrigin + HighResDuration::fromNanoseconds(2.5 * 1e6),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark4",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(3),
.duration = HighResDuration::zero()}}};
ASSERT_EQ(expected, entries);
}
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) {
auto reporter = PerformanceEntryReporter::getInstance();
auto timeOrigin = HighResTimeStamp::now();
reporter->clearEntries();
{
const auto entries = reporter->getEntries();
ASSERT_EQ(0, entries.size());
}
reporter->reportMark("common_name", timeOrigin);
reporter->reportMark(
"mark1", timeOrigin + HighResDuration::fromMilliseconds(1));
reporter->reportMark(
"mark2", timeOrigin + HighResDuration::fromMilliseconds(2));
reporter->reportMeasure(
"common_name", timeOrigin, HighResDuration::fromMilliseconds(2));
reporter->reportMeasure(
"measure1", timeOrigin, HighResDuration::fromMilliseconds(3));
reporter->reportMeasure(
"measure2",
timeOrigin + HighResDuration::fromMilliseconds(1),
HighResDuration::fromMilliseconds(6));
reporter->reportMeasure(
"measure3",
timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6),
HighResDuration::fromMilliseconds(2));
{
const auto allEntries = toSorted(reporter->getEntries());
const std::vector<PerformanceEntry> expected = {
PerformanceMark{
{.name = "common_name",
.startTime = timeOrigin,
.duration = HighResDuration::zero()}},
PerformanceMeasure{
{.name = "common_name",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(2)}},
PerformanceMeasure{
{.name = "measure1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(3)}},
PerformanceMark{
{.name = "mark1",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::zero()}},
PerformanceMeasure{
{.name = "measure2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::fromMilliseconds(6)}},
PerformanceMeasure{
{.name = "measure3",
.startTime =
timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6),
.duration = HighResDuration::fromMilliseconds(2)}},
PerformanceMark{
{.name = "mark2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(2),
.duration = HighResDuration::zero()}}};
ASSERT_EQ(expected, allEntries);
}
{
const auto marks =
toSorted(reporter->getEntries(PerformanceEntryType::MARK));
const std::vector<PerformanceEntry> expected = {
PerformanceMark{
{.name = "common_name",
.startTime = timeOrigin,
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark1",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(2),
.duration = HighResDuration::zero()}}};
ASSERT_EQ(expected, marks);
}
{
const auto measures =
toSorted(reporter->getEntries(PerformanceEntryType::MEASURE));
const std::vector<PerformanceEntry> expected = {
PerformanceMeasure{
{.name = "common_name",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(2)}},
PerformanceMeasure{
{.name = "measure1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(3)}},
PerformanceMeasure{
{.name = "measure2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::fromMilliseconds(6)}},
PerformanceMeasure{
{.name = "measure3",
.startTime =
timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6),
.duration = HighResDuration::fromMilliseconds(2)}}};
ASSERT_EQ(expected, measures);
}
{
const std::vector<PerformanceEntry> expected = {
PerformanceMark{{.name = "common_name", .startTime = timeOrigin}}};
const auto commonName =
reporter->getEntries(PerformanceEntryType::MARK, "common_name");
ASSERT_EQ(expected, commonName);
}
{
const std::vector<PerformanceEntry> expected = {PerformanceMeasure{
{.name = "common_name",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(2)}}};
const auto commonName =
reporter->getEntries(PerformanceEntryType::MEASURE, "common_name");
ASSERT_EQ(expected, commonName);
}
}
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearMarks) {
auto reporter = PerformanceEntryReporter::getInstance();
auto timeOrigin = HighResTimeStamp::now();
reporter->clearEntries();
reporter->reportMark("common_name", timeOrigin);
reporter->reportMark(
"mark1", timeOrigin + HighResDuration::fromMilliseconds(1));
reporter->reportMark(
"mark1", timeOrigin + HighResDuration::fromNanoseconds(2.1 * 1e6));
reporter->reportMark(
"mark2", timeOrigin + HighResDuration::fromMilliseconds(2));
reporter->reportMeasure(
"common_name", timeOrigin, HighResDuration::fromMilliseconds(2));
reporter->reportMeasure(
"measure1", timeOrigin, HighResDuration::fromMilliseconds(3));
reporter->reportMeasure(
"measure2",
timeOrigin + HighResDuration::fromMilliseconds(1),
HighResDuration::fromMilliseconds(6));
reporter->reportMeasure(
"measure3",
timeOrigin + HighResDuration::fromNanoseconds(1.5 * 1e6),
HighResDuration::fromMilliseconds(2));
reporter->clearEntries(PerformanceEntryType::MARK, "common_name");
{
auto entries = toSorted(reporter->getEntries(PerformanceEntryType::MARK));
std::vector<PerformanceEntry> expected = {
PerformanceMark{
{.name = "mark1",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(1),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(2),
.duration = HighResDuration::zero()}},
PerformanceMark{
{.name = "mark1",
.startTime =
timeOrigin + HighResDuration::fromNanoseconds(2.1 * 1e6),
.duration = HighResDuration::zero()}},
};
ASSERT_EQ(expected, entries);
}
reporter->clearEntries(PerformanceEntryType::MARK);
{
auto entries = reporter->getEntries(PerformanceEntryType::MARK);
ASSERT_EQ(entries.size(), 0);
}
reporter->clearEntries();
{
auto entries = reporter->getEntries();
ASSERT_EQ(entries.size(), 0);
}
}

View File

@@ -0,0 +1,461 @@
/*
* 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 <gtest/gtest.h>
#include <ostream>
#include "../PerformanceEntryReporter.h"
#include "../PerformanceObserver.h"
namespace facebook::react {
[[maybe_unused]] static bool operator==(
const PerformanceEntry& lhs,
const PerformanceEntry& rhs) {
return std::visit(
[&](const auto& left, const auto& right) {
bool baseMatch = left.name == right.name &&
left.entryType == right.entryType &&
left.startTime == right.startTime &&
left.duration == right.duration;
if (baseMatch && left.entryType == PerformanceEntryType::EVENT) {
auto leftEventTiming = std::get<PerformanceEventTiming>(lhs);
auto rightEventTiming = std::get<PerformanceEventTiming>(rhs);
return leftEventTiming.processingStart ==
rightEventTiming.processingStart &&
leftEventTiming.processingEnd == rightEventTiming.processingEnd &&
leftEventTiming.interactionId == rightEventTiming.interactionId;
}
return baseMatch;
},
lhs,
rhs);
}
} // namespace facebook::react
using namespace facebook::react;
TEST(PerformanceObserver, PerformanceObserverTestObserveFlushes) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
bool callbackCalled = false;
auto observer = PerformanceObserver::create(
reporter->getObserverRegistry(), [&]() { callbackCalled = true; });
auto timeOrigin = HighResTimeStamp::now();
observer->observe(PerformanceEntryType::MARK);
// buffer is empty
ASSERT_FALSE(callbackCalled);
reporter->reportMark(
"test", timeOrigin + HighResDuration::fromMilliseconds(10));
ASSERT_TRUE(callbackCalled);
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestFilteredSingle) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto observer =
PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {});
observer->observe(PerformanceEntryType::MEASURE);
reporter->reportMark(
"test", timeOrigin + HighResDuration::fromMilliseconds(10));
// wrong type
ASSERT_EQ(observer->takeRecords().size(), 0);
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestFilterMulti) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto callbackCalled = false;
auto observer = PerformanceObserver::create(
reporter->getObserverRegistry(), [&]() { callbackCalled = true; });
observer->observe(
{PerformanceEntryType::MEASURE, PerformanceEntryType::MARK});
reporter->reportEvent(
"test1",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(10),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test2",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(10),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test3",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(10),
timeOrigin,
timeOrigin,
timeOrigin,
0);
ASSERT_EQ(observer->takeRecords().size(), 0);
ASSERT_FALSE(callbackCalled);
observer->disconnect();
}
TEST(
PerformanceObserver,
PerformanceObserverTestFilterSingleCallbackNotCalled) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto callbackCalled = false;
auto observer = PerformanceObserver::create(
reporter->getObserverRegistry(), [&]() { callbackCalled = true; });
observer->observe(PerformanceEntryType::MEASURE);
reporter->reportMark(
"test", timeOrigin + HighResDuration::fromMilliseconds(10));
ASSERT_FALSE(callbackCalled);
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestFilterMultiCallbackNotCalled) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto callbackCalled = false;
auto observer = PerformanceObserver::create(
reporter->getObserverRegistry(), [&]() { callbackCalled = true; });
observer->observe(
{PerformanceEntryType::MEASURE, PerformanceEntryType::MARK});
reporter->reportEvent(
"test1",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(10),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test2",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(10),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"off3",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(10),
timeOrigin,
timeOrigin,
timeOrigin,
0);
ASSERT_FALSE(callbackCalled);
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestObserveTakeRecords) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto observer =
PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {});
observer->observe(PerformanceEntryType::MARK);
reporter->reportMark(
"test1", timeOrigin + HighResDuration::fromMilliseconds(10));
reporter->reportMeasure(
"off",
timeOrigin + HighResDuration::fromMilliseconds(10),
HighResDuration::fromMilliseconds(20),
std::nullopt);
reporter->reportMark(
"test2", timeOrigin + HighResDuration::fromMilliseconds(20));
reporter->reportMark(
"test3", timeOrigin + HighResDuration::fromMilliseconds(30));
const std::vector<PerformanceEntry> expected = {
PerformanceMark{
{.name = "test1",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(10)}},
PerformanceMark{
{.name = "test2",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(20)}},
PerformanceMark{
{.name = "test3",
.startTime = timeOrigin + HighResDuration::fromMilliseconds(30)}},
};
ASSERT_EQ(expected, observer->takeRecords());
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestObserveDurationThreshold) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto observer =
PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {});
observer->observe(
PerformanceEntryType::EVENT,
{.durationThreshold = HighResDuration::fromMilliseconds(50)});
reporter->reportEvent(
"test1",
timeOrigin,
HighResDuration::fromMilliseconds(50),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test2",
timeOrigin,
HighResDuration::fromMilliseconds(100),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"off1",
timeOrigin,
HighResDuration::fromMilliseconds(40),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportMark(
"off2", timeOrigin + HighResDuration::fromMilliseconds(100));
reporter->reportEvent(
"test3",
timeOrigin,
HighResDuration::fromMilliseconds(60),
timeOrigin,
timeOrigin,
timeOrigin,
0);
const std::vector<PerformanceEntry> expected = {
PerformanceEventTiming{
{.name = "test1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(50)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
PerformanceEventTiming{
{.name = "test2",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(100)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
PerformanceEventTiming{
{.name = "test3",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(60)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
};
ASSERT_EQ(expected, observer->takeRecords());
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestObserveBuffered) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
reporter->reportEvent(
"test1",
timeOrigin,
HighResDuration::fromMilliseconds(50),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test2",
timeOrigin,
HighResDuration::fromMilliseconds(100),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test3",
timeOrigin,
HighResDuration::fromMilliseconds(40),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"test4",
timeOrigin,
HighResDuration::fromMilliseconds(100),
timeOrigin,
timeOrigin,
timeOrigin,
0);
auto observer =
PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {});
observer->observe(
PerformanceEntryType::EVENT,
{.buffered = true,
.durationThreshold = HighResDuration::fromMilliseconds(50)});
const std::vector<PerformanceEntry> expected = {
PerformanceEventTiming{
{.name = "test1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(50)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
PerformanceEventTiming{
{.name = "test2",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(100)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
PerformanceEventTiming{
{.name = "test4",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(100)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
};
ASSERT_EQ(expected, observer->takeRecords());
observer->disconnect();
}
TEST(PerformanceObserver, PerformanceObserverTestMultiple) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
auto timeOrigin = HighResTimeStamp::now();
auto observer1 =
PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {});
auto observer2 =
PerformanceObserver::create(reporter->getObserverRegistry(), [&]() {});
observer1->observe(
PerformanceEntryType::EVENT,
{.durationThreshold = HighResDuration::fromMilliseconds(50)});
observer2->observe(
PerformanceEntryType::EVENT,
{.durationThreshold = HighResDuration::fromMilliseconds(80)});
reporter->reportMeasure(
"measure",
timeOrigin,
HighResDuration::fromMilliseconds(50),
std::nullopt);
reporter->reportEvent(
"event1",
timeOrigin,
HighResDuration::fromMilliseconds(100),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportEvent(
"event2",
timeOrigin,
HighResDuration::fromMilliseconds(40),
timeOrigin,
timeOrigin,
timeOrigin,
0);
reporter->reportMark(
"mark1", timeOrigin + HighResDuration::fromMilliseconds(100));
reporter->reportEvent(
"event3",
timeOrigin,
HighResDuration::fromMilliseconds(60),
timeOrigin,
timeOrigin,
timeOrigin,
0);
const std::vector<PerformanceEntry> expected1 = {
PerformanceEventTiming{
{.name = "event1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(100)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
PerformanceEventTiming{
{.name = "event3",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(60)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
};
const std::vector<PerformanceEntry> expected2 = {
PerformanceEventTiming{
{.name = "event1",
.startTime = timeOrigin,
.duration = HighResDuration::fromMilliseconds(100)},
timeOrigin,
timeOrigin,
timeOrigin,
0},
};
ASSERT_EQ(expected1, observer1->takeRecords());
ASSERT_EQ(expected2, observer2->takeRecords());
observer1->disconnect();
observer2->disconnect();
}