Refactor Performance and PerformanceObserver internals (#46693)

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

Changelog: [internal]

This unifies the native modules for `Performance` and `PerformanceObserver` in the same module. Keeping them separate is artificial and introduces unnecessary duplication. These APIs are very closely related so it makes sense to unify

Reviewed By: javache

Differential Revision: D63471855

fbshipit-source-id: fa8c5dc7b7c68954fc11867f68909d2c6c2ee85c
This commit is contained in:
Rubén Norte 2024-10-02 03:13:24 -07:00 committed by Facebook GitHub Bot
parent b62ae0d02b
commit 559c6029fd
32 changed files with 1095 additions and 1244 deletions

View File

@ -63,7 +63,6 @@ module.exports = class ReactNativeEnvironment extends NodeEnv {
Networking: {},
ImageLoader: {},
NativePerformanceCxx: {},
NativePerformanceObserverCxx: {},
LogBox: {},
SettingsManager: {
getConstants: () => ({settings: {}}),

View File

@ -15,6 +15,8 @@
#include <cxxreact/ReactMarker.h>
#include <jsi/instrumentation.h>
#include <react/performance/timeline/PerformanceEntryReporter.h>
#include <react/performance/timeline/PerformanceObserver.h>
#if __has_include(<reactperflogger/fusebox/FuseboxTracer.h>)
#include <reactperflogger/fusebox/FuseboxTracer.h>
#define HAS_FUSEBOX
@ -71,6 +73,40 @@ std::tuple<std::string, std::string_view> parseTrackName(
return std::make_tuple(trackNameRef, eventName);
}
class PerformanceObserverWrapper : public jsi::NativeState {
public:
explicit PerformanceObserverWrapper(
const std::shared_ptr<PerformanceObserver> observer)
: observer(observer) {}
const std::shared_ptr<PerformanceObserver> observer;
};
void sortEntries(std::vector<PerformanceEntry>& entries) {
return std::stable_sort(
entries.begin(), entries.end(), PerformanceEntrySorter{});
}
const std::array<PerformanceEntryType, 2> ENTRY_TYPES_AVAILABLE_FROM_TIMELINE{
{PerformanceEntryType::MARK, PerformanceEntryType::MEASURE}};
bool isAvailableFromTimeline(PerformanceEntryType entryType) {
return entryType == PerformanceEntryType::MARK ||
entryType == PerformanceEntryType::MEASURE;
}
std::shared_ptr<PerformanceObserver> tryGetObserver(
jsi::Runtime& rt,
jsi::Object& observerObj) {
if (!observerObj.hasNativeState(rt)) {
return nullptr;
}
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
return observerWrapper ? observerWrapper->observer : nullptr;
}
} // namespace
NativePerformance::NativePerformance(std::shared_ptr<CallInvoker> jsInvoker)
@ -137,6 +173,87 @@ void NativePerformance::measure(
#endif
}
void NativePerformance::clearMarks(
jsi::Runtime& /*rt*/,
std::optional<std::string> entryName) {
if (entryName) {
PerformanceEntryReporter::getInstance()->clearEntries(
PerformanceEntryType::MARK, *entryName);
} else {
PerformanceEntryReporter::getInstance()->clearEntries(
PerformanceEntryType::MARK);
}
}
void NativePerformance::clearMeasures(
jsi::Runtime& /*rt*/,
std::optional<std::string> entryName) {
if (entryName) {
PerformanceEntryReporter::getInstance()->clearEntries(
PerformanceEntryType::MEASURE, *entryName);
} else {
PerformanceEntryReporter::getInstance()->clearEntries(
PerformanceEntryType::MEASURE);
}
}
std::vector<PerformanceEntry> NativePerformance::getEntries(
jsi::Runtime& /*rt*/) {
std::vector<PerformanceEntry> entries;
for (auto entryType : ENTRY_TYPES_AVAILABLE_FROM_TIMELINE) {
PerformanceEntryReporter::getInstance()->getEntries(entries, entryType);
}
sortEntries(entries);
return entries;
}
std::vector<PerformanceEntry> NativePerformance::getEntriesByName(
jsi::Runtime& /*rt*/,
std::string entryName,
std::optional<PerformanceEntryType> entryType) {
std::vector<PerformanceEntry> entries;
if (entryType) {
if (isAvailableFromTimeline(*entryType)) {
PerformanceEntryReporter::getInstance()->getEntries(
entries, *entryType, entryName);
}
} else {
for (auto type : ENTRY_TYPES_AVAILABLE_FROM_TIMELINE) {
PerformanceEntryReporter::getInstance()->getEntries(
entries, type, entryName);
}
}
sortEntries(entries);
return entries;
}
std::vector<PerformanceEntry> NativePerformance::getEntriesByType(
jsi::Runtime& /*rt*/,
PerformanceEntryType entryType) {
std::vector<PerformanceEntry> entries;
if (isAvailableFromTimeline(entryType)) {
PerformanceEntryReporter::getInstance()->getEntries(entries, entryType);
}
sortEntries(entries);
return entries;
}
std::vector<std::pair<std::string, uint32_t>> NativePerformance::getEventCounts(
jsi::Runtime& /*rt*/) {
const auto& eventCounts =
PerformanceEntryReporter::getInstance()->getEventCounts();
return {eventCounts.begin(), eventCounts.end()};
}
std::unordered_map<std::string, double> NativePerformance::getSimpleMemoryInfo(
jsi::Runtime& rt) {
auto heapInfo = rt.instrumentation().getHeapInfo(false);
@ -185,4 +302,106 @@ NativePerformance::getReactNativeStartupTiming(jsi::Runtime& rt) {
return result;
}
jsi::Object NativePerformance::createObserver(
jsi::Runtime& rt,
NativePerformancePerformanceObserverCallback callback) {
// The way we dispatch performance observer callbacks is a bit different from
// the spec. The specification requires us to queue a single task that
// dispatches observer callbacks. Instead, we are queuing all callbacks as
// separate tasks in the scheduler.
PerformanceObserverCallback cb = [callback = std::move(callback)]() {
callback.callWithPriority(SchedulerPriority::IdlePriority);
};
auto& registry =
PerformanceEntryReporter::getInstance()->getObserverRegistry();
auto observer = PerformanceObserver::create(registry, std::move(cb));
auto observerWrapper = std::make_shared<PerformanceObserverWrapper>(observer);
jsi::Object observerObj{rt};
observerObj.setNativeState(rt, observerWrapper);
return observerObj;
}
double NativePerformance::getDroppedEntriesCount(
jsi::Runtime& rt,
jsi::Object observerObj) {
auto observer = tryGetObserver(rt, observerObj);
if (!observer) {
return 0;
}
return observer->getDroppedEntriesCount();
}
void NativePerformance::observe(
jsi::Runtime& rt,
jsi::Object observerObj,
NativePerformancePerformanceObserverObserveOptions options) {
auto observer = tryGetObserver(rt, observerObj);
if (!observer) {
return;
}
auto durationThreshold = options.durationThreshold.value_or(0.0);
// observer of type multiple
if (options.entryTypes.has_value()) {
std::unordered_set<PerformanceEntryType> entryTypes;
auto rawTypes = options.entryTypes.value();
for (auto rawType : rawTypes) {
entryTypes.insert(Bridging<PerformanceEntryType>::fromJs(rt, rawType));
}
observer->observe(entryTypes);
} else { // single
auto buffered = options.buffered.value_or(false);
if (options.type.has_value()) {
observer->observe(
static_cast<PerformanceEntryType>(options.type.value()),
{.buffered = buffered, .durationThreshold = durationThreshold});
}
}
}
void NativePerformance::disconnect(jsi::Runtime& rt, jsi::Object observerObj) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
if (!observerWrapper) {
return;
}
auto observer = observerWrapper->observer;
observer->disconnect();
}
std::vector<PerformanceEntry> NativePerformance::takeRecords(
jsi::Runtime& rt,
jsi::Object observerObj,
bool sort) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
if (!observerWrapper) {
return {};
}
auto observer = observerWrapper->observer;
auto records = observer->takeRecords();
if (sort) {
sortEntries(records);
}
return records;
}
std::vector<PerformanceEntryType>
NativePerformance::getSupportedPerformanceEntryTypes(jsi::Runtime& /*rt*/) {
auto supportedEntryTypes = PerformanceEntryReporter::getSupportedEntryTypes();
return {supportedEntryTypes.begin(), supportedEntryTypes.end()};
}
} // namespace facebook::react

View File

@ -14,19 +14,65 @@
#else
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
#endif
#include <react/performance/timeline/PerformanceEntry.h>
#include <memory>
#include <optional>
#include <string>
namespace facebook::react {
using NativePerformancePerformanceObserverCallback = AsyncCallback<>;
using NativePerformancePerformanceObserverObserveOptions =
NativePerformancePerformanceObserverInit<
// entryTypes
std::optional<std::vector<int>>,
// type
std::optional<int>,
// buffered
std::optional<bool>,
// durationThreshold
std::optional<double>>;
template <>
struct Bridging<PerformanceEntryType> {
static PerformanceEntryType fromJs(
jsi::Runtime& /*rt*/,
const jsi::Value& value) {
return static_cast<PerformanceEntryType>(value.asNumber());
}
static jsi::Value toJs(
jsi::Runtime& /*rt*/,
const PerformanceEntryType& value) {
return {static_cast<int>(value)};
}
};
template <>
struct Bridging<PerformanceEntry>
: NativePerformanceRawPerformanceEntryBridging<PerformanceEntry> {};
template <>
struct Bridging<NativePerformancePerformanceObserverObserveOptions>
: NativePerformancePerformanceObserverInitBridging<
NativePerformancePerformanceObserverObserveOptions> {};
class NativePerformance : public NativePerformanceCxxSpec<NativePerformance> {
public:
NativePerformance(std::shared_ptr<CallInvoker> jsInvoker);
#pragma mark - DOM Performance (High Resolution Time) (https://www.w3.org/TR/hr-time-3/#dom-performance)
// https://www.w3.org/TR/hr-time-3/#now-method
double now(jsi::Runtime& rt);
#pragma mark - User Timing Level 3 functions (https://w3c.github.io/user-timing/)
// https://w3c.github.io/user-timing/#mark-method
void mark(jsi::Runtime& rt, std::string name, double startTime);
// https://w3c.github.io/user-timing/#measure-method
void measure(
jsi::Runtime& rt,
std::string name,
@ -36,6 +82,65 @@ class NativePerformance : public NativePerformanceCxxSpec<NativePerformance> {
std::optional<std::string> startMark,
std::optional<std::string> endMark);
// https://w3c.github.io/user-timing/#clearmarks-method
void clearMarks(
jsi::Runtime& rt,
std::optional<std::string> entryName = std::nullopt);
// https://w3c.github.io/user-timing/#clearmeasures-method
void clearMeasures(
jsi::Runtime& rt,
std::optional<std::string> entryName = std::nullopt);
#pragma mark - Performance Timeline (https://w3c.github.io/performance-timeline/#performance-timeline)
// https://www.w3.org/TR/performance-timeline/#getentries-method
std::vector<PerformanceEntry> getEntries(jsi::Runtime& rt);
// https://www.w3.org/TR/performance-timeline/#getentriesbytype-method
std::vector<PerformanceEntry> getEntriesByType(
jsi::Runtime& rt,
PerformanceEntryType entryType);
// https://www.w3.org/TR/performance-timeline/#getentriesbyname-method
std::vector<PerformanceEntry> getEntriesByName(
jsi::Runtime& rt,
std::string entryName,
std::optional<PerformanceEntryType> entryType = std::nullopt);
#pragma mark - Performance Observer (https://w3c.github.io/performance-timeline/#the-performanceobserver-interface)
jsi::Object createObserver(
jsi::Runtime& rt,
NativePerformancePerformanceObserverCallback callback);
// https://www.w3.org/TR/performance-timeline/#dom-performanceobservercallbackoptions-droppedentriescount
double getDroppedEntriesCount(jsi::Runtime& rt, jsi::Object observerObj);
void observe(
jsi::Runtime& rt,
jsi::Object observer,
NativePerformancePerformanceObserverObserveOptions options);
void disconnect(jsi::Runtime& rt, jsi::Object observer);
std::vector<PerformanceEntry> takeRecords(
jsi::Runtime& rt,
jsi::Object observerObj,
// When called via `observer.takeRecords` it should be in insertion order.
// When called via the observer callback, it should be in chronological
// order with respect to `startTime`.
bool sort);
std::vector<PerformanceEntryType> getSupportedPerformanceEntryTypes(
jsi::Runtime& rt);
#pragma mark - Event Timing API functions (https://www.w3.org/TR/event-timing/)
// https://www.w3.org/TR/event-timing/#dom-performance-eventcounts
std::vector<std::pair<std::string, uint32_t>> getEventCounts(
jsi::Runtime& rt);
#pragma mark - Non-standard memory functions
// To align with web API, we will make sure to return three properties
// (jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize) + anything needed from
// the VM side.
@ -49,6 +154,8 @@ class NativePerformance : public NativePerformanceCxxSpec<NativePerformance> {
// for heap size information, as double's 2^53 sig bytes is large enough.
std::unordered_map<std::string, double> getSimpleMemoryInfo(jsi::Runtime& rt);
#pragma mark - RN-specific startup timing
// Collect and return the RN app startup timing information for performance
// tracking.
std::unordered_map<std::string, double> getReactNativeStartupTiming(

View File

@ -1,200 +0,0 @@
/*
* 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 "NativePerformanceObserver.h"
#include <jsi/jsi.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/performance/timeline/PerformanceEntryReporter.h>
#include <react/performance/timeline/PerformanceObserver.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/utils/CoreFeatures.h>
#include <memory>
#ifdef RN_DISABLE_OSS_PLUGIN_HEADER
#include "Plugins.h"
#endif
std::shared_ptr<facebook::react::TurboModule>
NativePerformanceObserverModuleProvider(
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
return std::make_shared<facebook::react::NativePerformanceObserver>(
std::move(jsInvoker));
}
namespace facebook::react {
class PerformanceObserverWrapper : public jsi::NativeState {
public:
explicit PerformanceObserverWrapper(
const std::shared_ptr<PerformanceObserver> observer)
: observer(observer) {}
std::shared_ptr<PerformanceObserver> observer;
};
NativePerformanceObserver::NativePerformanceObserver(
std::shared_ptr<CallInvoker> jsInvoker)
: NativePerformanceObserverCxxSpec(std::move(jsInvoker)) {}
jsi::Object NativePerformanceObserver::createObserver(
jsi::Runtime& rt,
NativePerformanceObserverCallback callback) {
// The way we dispatch performance observer callbacks is a bit different from
// the spec. The specification requires us to queue a single task that
// dispatches observer callbacks. Instead, we are queuing all callbacks as
// separate tasks in the scheduler.
PerformanceObserverCallback cb = [callback = std::move(callback)]() {
callback.callWithPriority(SchedulerPriority::IdlePriority);
};
auto& registry =
PerformanceEntryReporter::getInstance()->getObserverRegistry();
auto observer = PerformanceObserver::create(registry, std::move(cb));
auto observerWrapper = std::make_shared<PerformanceObserverWrapper>(observer);
jsi::Object observerObj{rt};
observerObj.setNativeState(rt, observerWrapper);
return observerObj;
}
double NativePerformanceObserver::getDroppedEntriesCount(
jsi::Runtime& rt,
jsi::Object observerObj) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
if (!observerWrapper) {
return 0;
}
auto observer = observerWrapper->observer;
return observer->getDroppedEntriesCount();
}
void NativePerformanceObserver::observe(
jsi::Runtime& rt,
jsi::Object observerObj,
NativePerformanceObserverObserveOptions options) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
if (!observerWrapper) {
return;
}
auto observer = observerWrapper->observer;
auto durationThreshold = options.durationThreshold.value_or(0.0);
// observer of type multiple
if (options.entryTypes.has_value()) {
std::unordered_set<PerformanceEntryType> entryTypes;
auto rawTypes = options.entryTypes.value();
for (auto rawType : rawTypes) {
entryTypes.insert(Bridging<PerformanceEntryType>::fromJs(rt, rawType));
}
observer->observe(entryTypes);
} else { // single
auto buffered = options.buffered.value_or(false);
if (options.type.has_value()) {
observer->observe(
static_cast<PerformanceEntryType>(options.type.value()),
{.buffered = buffered, .durationThreshold = durationThreshold});
}
}
}
void NativePerformanceObserver::disconnect(
jsi::Runtime& rt,
jsi::Object observerObj) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
if (!observerWrapper) {
return;
}
auto observer = observerWrapper->observer;
observer->disconnect();
}
std::vector<PerformanceEntry> NativePerformanceObserver::takeRecords(
jsi::Runtime& rt,
jsi::Object observerObj,
bool sort) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));
if (!observerWrapper) {
return {};
}
auto observer = observerWrapper->observer;
auto records = observer->takeRecords();
if (sort) {
std::stable_sort(records.begin(), records.end(), PerformanceEntrySorter{});
}
return records;
}
void NativePerformanceObserver::clearEntries(
jsi::Runtime& /*rt*/,
PerformanceEntryType entryType,
std::optional<std::string> entryName) {
PerformanceEntryReporter::getInstance()->clearEntries(entryType, entryName);
}
std::vector<PerformanceEntry> NativePerformanceObserver::getEntries(
jsi::Runtime& /*rt*/,
std::optional<PerformanceEntryType> entryType,
std::optional<std::string> entryName) {
const auto reporter = PerformanceEntryReporter::getInstance();
std::vector<PerformanceEntry> entries;
if (entryType.has_value()) {
if (entryName.has_value()) {
entries =
reporter->getEntriesByName(entryName.value(), entryType.value());
} else {
entries = reporter->getEntriesByType(entryType.value());
}
} else if (entryName.has_value()) {
entries = reporter->getEntriesByName(entryName.value());
} else {
entries = reporter->getEntries();
}
std::stable_sort(entries.begin(), entries.end(), PerformanceEntrySorter{});
return entries;
}
std::vector<PerformanceEntryType>
NativePerformanceObserver::getSupportedPerformanceEntryTypes(
jsi::Runtime& /*rt*/) {
std::vector supportedEntries = {
PerformanceEntryType::MARK,
PerformanceEntryType::MEASURE,
PerformanceEntryType::EVENT,
};
if (ReactNativeFeatureFlags::enableLongTaskAPI()) {
supportedEntries.push_back(PerformanceEntryType::LONGTASK);
}
return supportedEntries;
}
std::vector<std::pair<std::string, uint32_t>>
NativePerformanceObserver::getEventCounts(jsi::Runtime& /*rt*/) {
const auto& eventCounts =
PerformanceEntryReporter::getInstance()->getEventCounts();
return {eventCounts.begin(), eventCounts.end()};
}
} // namespace facebook::react

View File

@ -1,105 +0,0 @@
/*
* 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
#if __has_include("rncoreJSI.h") // Cmake headers on Android
#include "rncoreJSI.h"
#elif __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple
#include "FBReactNativeSpecJSI.h"
#else
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
#endif
#include <react/performance/timeline/PerformanceEntryReporter.h>
#include <optional>
#include <string>
#include <vector>
namespace facebook::react {
using NativePerformanceObserverCallback = AsyncCallback<>;
using NativePerformanceObserverObserveOptions =
NativePerformanceObserverPerformanceObserverInit<
// entryTypes
std::optional<std::vector<int>>,
// type
std::optional<int>,
// buffered
std::optional<bool>,
// durationThreshold
std::optional<double>>;
#pragma mark - Structs
template <>
struct Bridging<PerformanceEntryType> {
static PerformanceEntryType fromJs(
jsi::Runtime& /*rt*/,
const jsi::Value& value) {
return static_cast<PerformanceEntryType>(value.asNumber());
}
static jsi::Value toJs(
jsi::Runtime& /*rt*/,
const PerformanceEntryType& value) {
return {static_cast<int>(value)};
}
};
template <>
struct Bridging<NativePerformanceObserverObserveOptions>
: NativePerformanceObserverPerformanceObserverInitBridging<
NativePerformanceObserverObserveOptions> {};
template <>
struct Bridging<PerformanceEntry>
: NativePerformanceObserverRawPerformanceEntryBridging<PerformanceEntry> {};
#pragma mark - implementation
class NativePerformanceObserver
: public NativePerformanceObserverCxxSpec<NativePerformanceObserver> {
public:
explicit NativePerformanceObserver(std::shared_ptr<CallInvoker> jsInvoker);
jsi::Object createObserver(
jsi::Runtime& rt,
NativePerformanceObserverCallback callback);
double getDroppedEntriesCount(jsi::Runtime& rt, jsi::Object observerObj);
void observe(
jsi::Runtime& rt,
jsi::Object observer,
NativePerformanceObserverObserveOptions options);
void disconnect(jsi::Runtime& rt, jsi::Object observer);
std::vector<PerformanceEntry> takeRecords(
jsi::Runtime& rt,
jsi::Object observerObj,
// When called via `observer.takeRecords` it should be in insertion order.
// When called via the observer callback, it should be in chronological
// order with respect to `startTime`.
bool sort);
std::vector<std::pair<std::string, uint32_t>> getEventCounts(
jsi::Runtime& rt);
void clearEntries(
jsi::Runtime& rt,
PerformanceEntryType entryType,
std::optional<std::string> entryName);
std::vector<PerformanceEntry> getEntries(
jsi::Runtime& rt,
std::optional<PerformanceEntryType> entryType,
std::optional<std::string> entryName);
std::vector<PerformanceEntryType> getSupportedPerformanceEntryTypes(
jsi::Runtime& rt);
};
} // namespace facebook::react

View File

@ -10,7 +10,6 @@
#include <react/timing/primitives.h>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_set>
namespace facebook::react {

View File

@ -29,12 +29,12 @@ class PerformanceEntryBuffer {
virtual ~PerformanceEntryBuffer() = default;
virtual void add(const PerformanceEntry& entry) = 0;
virtual void getEntries(
std::string_view name,
std::vector<PerformanceEntry>& target) const = 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(std::string_view name) = 0;
virtual void clear(const std::string& name) = 0;
};
} // namespace facebook::react

View File

@ -21,8 +21,8 @@ void PerformanceEntryCircularBuffer::getEntries(
}
void PerformanceEntryCircularBuffer::getEntries(
std::string_view name,
std::vector<PerformanceEntry>& target) const {
std::vector<PerformanceEntry>& target,
const std::string& name) const {
buffer_.getEntries(
target, [&](const PerformanceEntry& e) { return e.name == name; });
}
@ -31,7 +31,7 @@ void PerformanceEntryCircularBuffer::clear() {
buffer_.clear();
}
void PerformanceEntryCircularBuffer::clear(std::string_view name) {
void PerformanceEntryCircularBuffer::clear(const std::string& name) {
buffer_.clear([&](const PerformanceEntry& e) { return e.name == name; });
}

View File

@ -20,11 +20,12 @@ class PerformanceEntryCircularBuffer : public PerformanceEntryBuffer {
void add(const PerformanceEntry& entry) override;
void getEntries(std::vector<PerformanceEntry>& target) const override;
void getEntries(std::string_view name, std::vector<PerformanceEntry>& target)
const override;
void getEntries(
std::vector<PerformanceEntry>& target,
const std::string& name) const override;
void clear() override;
void clear(std::string_view name) override;
void clear(const std::string& name) override;
private:
CircularBuffer<PerformanceEntry> buffer_;

View File

@ -28,8 +28,8 @@ void PerformanceEntryKeyedBuffer::getEntries(
}
void PerformanceEntryKeyedBuffer::getEntries(
std::string_view name,
std::vector<PerformanceEntry>& target) const {
std::vector<PerformanceEntry>& target,
const std::string& name) const {
std::string nameStr{name};
if (auto node = entryMap_.find(nameStr); node != entryMap_.end()) {
@ -41,8 +41,8 @@ void PerformanceEntryKeyedBuffer::clear() {
entryMap_.clear();
}
void PerformanceEntryKeyedBuffer::clear(std::string_view nameView) {
entryMap_.erase(std::string(nameView));
void PerformanceEntryKeyedBuffer::clear(const std::string& name) {
entryMap_.erase(name);
}
std::optional<PerformanceEntry> PerformanceEntryKeyedBuffer::find(

View File

@ -23,11 +23,12 @@ class PerformanceEntryKeyedBuffer : public PerformanceEntryBuffer {
void getEntries(std::vector<PerformanceEntry>& target) const override;
void getEntries(std::string_view name, std::vector<PerformanceEntry>& target)
const override;
void getEntries(
std::vector<PerformanceEntry>& target,
const std::string& name) const override;
void clear() override;
void clear(std::string_view name) override;
void clear(const std::string& name) override;
std::optional<PerformanceEntry> find(const std::string& name) const;

View File

@ -8,9 +8,26 @@
#include "PerformanceEntryReporter.h"
#include <cxxreact/JSExecutor.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
namespace facebook::react {
namespace {
std::vector<PerformanceEntryType> getSupportedEntryTypesInternal() {
std::vector<PerformanceEntryType> supportedEntryTypes{
PerformanceEntryType::MARK,
PerformanceEntryType::MEASURE,
PerformanceEntryType::EVENT,
};
if (ReactNativeFeatureFlags::enableLongTaskAPI()) {
supportedEntryTypes.emplace_back(PerformanceEntryType::LONGTASK);
}
return supportedEntryTypes;
}
} // namespace
std::shared_ptr<PerformanceEntryReporter>&
PerformanceEntryReporter::getInstance() {
static auto instance = std::make_shared<PerformanceEntryReporter>();
@ -20,90 +37,93 @@ PerformanceEntryReporter::getInstance() {
PerformanceEntryReporter::PerformanceEntryReporter()
: observerRegistry_(std::make_unique<PerformanceObserverRegistry>()) {}
#pragma mark - DOM Performance (High Resolution Time) (https://www.w3.org/TR/hr-time-3/#dom-performance)
DOMHighResTimeStamp PerformanceEntryReporter::getCurrentTimeStamp() const {
return timeStampProvider_ != nullptr ? timeStampProvider_()
: JSExecutor::performanceNow();
}
#pragma mark - Performance Timeline (https://w3c.github.io/performance-timeline/)
uint32_t PerformanceEntryReporter::getDroppedEntriesCount(
PerformanceEntryType type) const noexcept {
return getBuffer(type).droppedEntriesCount;
std::vector<PerformanceEntryType>
PerformanceEntryReporter::getSupportedEntryTypes() {
static std::vector<PerformanceEntryType> supportedEntries =
getSupportedEntryTypesInternal();
return supportedEntries;
}
void PerformanceEntryReporter::clearEntries(
std::optional<PerformanceEntryType> entryType,
std::optional<std::string_view> entryName) {
uint32_t PerformanceEntryReporter::getDroppedEntriesCount(
PerformanceEntryType entryType) const noexcept {
std::lock_guard lock(buffersMutex_);
// Clear all entry types
if (!entryType) {
if (entryName.has_value()) {
markBuffer_.clear(*entryName);
measureBuffer_.clear(*entryName);
eventBuffer_.clear(*entryName);
longTaskBuffer_.clear(*entryName);
} else {
markBuffer_.clear();
measureBuffer_.clear();
eventBuffer_.clear();
longTaskBuffer_.clear();
}
return;
}
auto& buffer = getBufferRef(*entryType);
if (entryName.has_value()) {
buffer.clear(*entryName);
} else {
buffer.clear();
}
return getBuffer(entryType).droppedEntriesCount;
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntries() const {
std::vector<PerformanceEntry> res;
// Collect all entry types
for (int i = 1; i <= NUM_PERFORMANCE_ENTRY_TYPES; i++) {
getBuffer(static_cast<PerformanceEntryType>(i)).getEntries(res);
std::vector<PerformanceEntry> entries;
getEntries(entries);
return entries;
}
void PerformanceEntryReporter::getEntries(
std::vector<PerformanceEntry>& dest) const {
std::lock_guard lock(buffersMutex_);
for (auto entryType : getSupportedEntryTypes()) {
getBuffer(entryType).getEntries(dest);
}
return res;
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntriesByType(
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntries(
PerformanceEntryType entryType) const {
std::vector<PerformanceEntry> res;
getEntriesByType(entryType, res);
return res;
std::vector<PerformanceEntry> dest;
getEntries(dest, entryType);
return dest;
}
void PerformanceEntryReporter::getEntriesByType(
void PerformanceEntryReporter::getEntries(
std::vector<PerformanceEntry>& dest,
PerformanceEntryType entryType) const {
std::lock_guard lock(buffersMutex_);
getBuffer(entryType).getEntries(dest);
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntries(
PerformanceEntryType entryType,
std::vector<PerformanceEntry>& target) const {
getBuffer(entryType).getEntries(target);
const std::string& entryName) const {
std::vector<PerformanceEntry> entries;
getEntries(entries, entryType, entryName);
return entries;
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntriesByName(
std::string_view entryName) const {
std::vector<PerformanceEntry> res;
// Collect all entry types
for (int i = 1; i <= NUM_PERFORMANCE_ENTRY_TYPES; i++) {
getBuffer(static_cast<PerformanceEntryType>(i)).getEntries(entryName, res);
void PerformanceEntryReporter::getEntries(
std::vector<PerformanceEntry>& dest,
PerformanceEntryType entryType,
const std::string& entryName) const {
std::lock_guard lock(buffersMutex_);
getBuffer(entryType).getEntries(dest, entryName);
}
void PerformanceEntryReporter::clearEntries() {
std::lock_guard lock(buffersMutex_);
for (auto entryType : getSupportedEntryTypes()) {
getBufferRef(entryType).clear();
}
return res;
}
std::vector<PerformanceEntry> PerformanceEntryReporter::getEntriesByName(
std::string_view entryName,
PerformanceEntryType entryType) const {
std::vector<PerformanceEntry> res;
getBuffer(entryType).getEntries(entryName, res);
return res;
void PerformanceEntryReporter::clearEntries(PerformanceEntryType entryType) {
std::lock_guard lock(buffersMutex_);
getBufferRef(entryType).clear();
}
#pragma mark - User Timing Level 3 functions (https://w3c.github.io/user-timing/)
void PerformanceEntryReporter::clearEntries(
PerformanceEntryType entryType,
const std::string& entryName) {
std::lock_guard lock(buffersMutex_);
getBufferRef(entryType).clear(entryName);
}
void PerformanceEntryReporter::reportMark(
const std::string& name,
@ -166,8 +186,6 @@ DOMHighResTimeStamp PerformanceEntryReporter::getMarkTime(
}
}
#pragma mark - Event Timing API functions (https://www.w3.org/TR/event-timing/)
void PerformanceEntryReporter::reportEvent(
std::string name,
DOMHighResTimeStamp startTime,
@ -201,8 +219,6 @@ void PerformanceEntryReporter::reportEvent(
observerRegistry_->queuePerformanceEntry(entry);
}
#pragma mark - Long Tasks API functions (https://w3c.github.io/longtasks/)
void PerformanceEntryReporter::reportLongTask(
DOMHighResTimeStamp startTime,
DOMHighResTimeStamp duration) {

View File

@ -11,6 +11,7 @@
#include <memory>
#include <mutex>
#include <optional>
#include <vector>
#include "PerformanceEntryCircularBuffer.h"
#include "PerformanceEntryKeyedBuffer.h"
#include "PerformanceObserverRegistry.h"
@ -36,43 +37,47 @@ class PerformanceEntryReporter {
return *observerRegistry_;
}
#pragma mark - DOM Performance (High Resolution Time) (https://www.w3.org/TR/hr-time-3/#dom-performance)
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);
// https://www.w3.org/TR/hr-time-3/#now-method
DOMHighResTimeStamp getCurrentTimeStamp() const;
void setTimeStampProvider(std::function<DOMHighResTimeStamp()> provider) {
timeStampProvider_ = std::move(provider);
}
#pragma mark - Performance Timeline (https://w3c.github.io/performance-timeline/)
static std::vector<PerformanceEntryType> getSupportedEntryTypes();
// https://www.w3.org/TR/performance-timeline/#dom-performanceobservercallbackoptions-droppedentriescount
uint32_t getDroppedEntriesCount(PerformanceEntryType type) const noexcept;
// https://www.w3.org/TR/performance-timeline/#getentries-method
// https://www.w3.org/TR/performance-timeline/#getentriesbytype-method
// https://www.w3.org/TR/performance-timeline/#getentriesbyname-method
std::vector<PerformanceEntry> getEntries() const;
std::vector<PerformanceEntry> getEntriesByType(
PerformanceEntryType entryType) const;
void getEntriesByType(
PerformanceEntryType entryType,
std::vector<PerformanceEntry>& target) const;
std::vector<PerformanceEntry> getEntriesByName(
std::string_view entryName) const;
std::vector<PerformanceEntry> getEntriesByName(
std::string_view entryName,
PerformanceEntryType entryType) const;
const std::unordered_map<std::string, uint32_t>& getEventCounts() const {
return eventCounts_;
}
#pragma mark - User Timing Level 3 functions (https://w3c.github.io/user-timing/)
// https://w3c.github.io/user-timing/#mark-method
void reportMark(
const std::string& name,
const std::optional<DOMHighResTimeStamp>& startTime = std::nullopt);
// https://w3c.github.io/user-timing/#measure-method
void reportMeasure(
const std::string_view& name,
double startTime,
@ -81,14 +86,6 @@ class PerformanceEntryReporter {
const std::optional<std::string>& startMark = std::nullopt,
const std::optional<std::string>& endMark = std::nullopt);
// https://w3c.github.io/user-timing/#clearmarks-method
// https://w3c.github.io/user-timing/#clearmeasures-method
void clearEntries(
std::optional<PerformanceEntryType> entryType = std::nullopt,
std::optional<std::string_view> entryName = std::nullopt);
#pragma mark - Event Timing API functions (https://www.w3.org/TR/event-timing/)
void reportEvent(
std::string name,
double startTime,
@ -97,13 +94,6 @@ class PerformanceEntryReporter {
double processingEnd,
uint32_t interactionId);
// https://www.w3.org/TR/event-timing/#dom-performance-eventcounts
const std::unordered_map<std::string, uint32_t>& getEventCounts() const {
return eventCounts_;
}
#pragma mark - Long Tasks API functions (https://w3c.github.io/longtasks/)
void reportLongTask(double startTime, double duration);
private:
@ -121,7 +111,8 @@ class PerformanceEntryReporter {
double getMarkTime(const std::string& markName) const;
inline PerformanceEntryBuffer& getBufferRef(PerformanceEntryType entryType) {
const inline PerformanceEntryBuffer& getBuffer(
PerformanceEntryType entryType) const {
switch (entryType) {
case PerformanceEntryType::EVENT:
return eventBuffer_;
@ -136,8 +127,7 @@ class PerformanceEntryReporter {
}
}
const inline PerformanceEntryBuffer& getBuffer(
PerformanceEntryType entryType) const {
inline PerformanceEntryBuffer& getBufferRef(PerformanceEntryType entryType) {
switch (entryType) {
case PerformanceEntryType::EVENT:
return eventBuffer_;

View File

@ -45,7 +45,7 @@ void PerformanceObserver::observe(
if (options.buffered) {
auto& reporter = PerformanceEntryReporter::getInstance();
auto bufferedEntries = reporter->getEntriesByType(type);
auto bufferedEntries = reporter->getEntries(type);
for (auto& bufferedEntry : bufferedEntries) {
handleEntry(bufferedEntry);
}

View File

@ -51,6 +51,7 @@ Pod::Spec.new do |s|
s.header_mappings_dir = "../../.."
end
s.dependency "React-featureflags"
s.dependency "React-timing"
s.dependency "React-cxxreact"
s.dependency "RCT-Folly", folly_version

View File

@ -11,6 +11,8 @@
#include "../PerformanceEntryReporter.h"
using namespace facebook::react;
namespace facebook::react {
[[maybe_unused]] static bool operator==(
@ -37,27 +39,26 @@ namespace facebook::react {
<< ", .startTime = " << entry.startTime
<< ", .duration = " << entry.duration << " }";
}
} // namespace facebook::react
static std::vector<PerformanceEntry> toSorted(
const std::vector<PerformanceEntry>& originalEntries) {
std::vector<PerformanceEntry> entries = originalEntries;
namespace {
std::vector<PerformanceEntry> toSorted(
std::vector<PerformanceEntry>&& entries) {
std::stable_sort(entries.begin(), entries.end(), PerformanceEntrySorter{});
return entries;
}
} // namespace facebook::react
using namespace facebook::react;
} // namespace
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
reporter->reportMark("mark0", 0.0);
reporter->reportMark("mark1", 1.0);
reporter->reportMark("mark2", 2.0);
reporter->reportMark("mark0", 0);
reporter->reportMark("mark1", 1);
reporter->reportMark("mark2", 2);
// Report mark0 again
reporter->reportMark("mark0", 3.0);
reporter->reportMark("mark0", 3);
const auto entries = toSorted(reporter->getEntries());
@ -66,17 +67,20 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMarks) {
const std::vector<PerformanceEntry> expected = {
{.name = "mark0",
.entryType = PerformanceEntryType::MARK,
.startTime = 0.0},
.startTime = 0,
.duration = 0},
{.name = "mark1",
.entryType = PerformanceEntryType::MARK,
.startTime = 1.0},
.startTime = 1,
.duration = 0},
{.name = "mark2",
.entryType = PerformanceEntryType::MARK,
.startTime = 2.0},
.startTime = 2,
.duration = 0},
{.name = "mark0",
.entryType = PerformanceEntryType::MARK,
.startTime = 3.0},
};
.startTime = 3,
.duration = 0}};
ASSERT_EQ(expected, entries);
}
@ -85,26 +89,26 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
reporter->reportMark("mark0", 0.0);
reporter->reportMark("mark1", 1.0);
reporter->reportMark("mark2", 2.0);
reporter->reportMark("mark0", 0);
reporter->reportMark("mark1", 1);
reporter->reportMark("mark2", 2);
reporter->reportMeasure("measure0", 0.0, 2.0);
reporter->reportMeasure("measure1", 0.0, 2.0, 4.0);
reporter->reportMeasure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2");
reporter->reportMeasure("measure3", 0.0, 0.0, 5.0, "mark1");
reporter->reportMeasure("measure0", 0, 2);
reporter->reportMeasure("measure1", 0, 2, 4);
reporter->reportMeasure("measure2", 0, 0, std::nullopt, "mark1", "mark2");
reporter->reportMeasure("measure3", 0, 0, 5, "mark1");
reporter->reportMeasure(
"measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2");
"measure4", 1.5, 0, std::nullopt, std::nullopt, "mark2");
reporter->setTimeStampProvider([]() { return 3.5; });
reporter->reportMeasure("measure5", 0.0, 0.0, std::nullopt, "mark2");
reporter->reportMeasure("measure5", 0, 0, std::nullopt, "mark2");
reporter->reportMark("mark3", 2.5);
reporter->reportMeasure("measure6", 2.0, 2.0);
reporter->reportMark("mark4", 2.1);
reporter->reportMark("mark4", 3.0);
// Uses the last reported time for mark4
reporter->reportMeasure("measure7", 0.0, 0.0, std::nullopt, "mark1", "mark4");
reporter->reportMeasure("measure7", 0, 0, std::nullopt, "mark1", "mark4");
const auto entries = toSorted(reporter->getEntries());
@ -178,16 +182,16 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) {
ASSERT_EQ(0, entries.size());
}
reporter->reportMark("common_name", 0.0);
reporter->reportMark("mark1", 1.0);
reporter->reportMark("mark2", 2.0);
reporter->reportMark("common_name", 0);
reporter->reportMark("mark1", 1);
reporter->reportMark("mark2", 2);
reporter->reportMeasure("common_name", 0.0, 2.0);
reporter->reportMeasure("measure1", 0.0, 2.0, 4.0);
reporter->reportMeasure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2");
reporter->reportMeasure("measure3", 0.0, 0.0, 5.0, "mark1");
reporter->reportMeasure("common_name", 0, 2);
reporter->reportMeasure("measure1", 0, 2, 4);
reporter->reportMeasure("measure2", 0, 0, std::nullopt, "mark1", "mark2");
reporter->reportMeasure("measure3", 0, 0, 5, "mark1");
reporter->reportMeasure(
"measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2");
"measure4", 1.5, 0, std::nullopt, std::nullopt, "mark2");
{
const auto allEntries = toSorted(reporter->getEntries());
@ -229,7 +233,7 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) {
{
const auto marks =
toSorted(reporter->getEntriesByType(PerformanceEntryType::MARK));
toSorted(reporter->getEntries(PerformanceEntryType::MARK));
const std::vector<PerformanceEntry> expected = {
{.name = "common_name",
.entryType = PerformanceEntryType::MARK,
@ -248,7 +252,7 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) {
{
const auto measures =
toSorted(reporter->getEntriesByType(PerformanceEntryType::MEASURE));
toSorted(reporter->getEntries(PerformanceEntryType::MEASURE));
const std::vector<PerformanceEntry> expected = {
{.name = "common_name",
.entryType = PerformanceEntryType::MEASURE,
@ -269,8 +273,7 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) {
{.name = "measure4",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1.5,
.duration = 0.5},
};
.duration = 0.5}};
ASSERT_EQ(expected, measures);
}
@ -278,113 +281,72 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestGetEntries) {
const std::vector<PerformanceEntry> expected = {
{.name = "common_name",
.entryType = PerformanceEntryType::MARK,
.startTime = 0,
.duration = 0},
.startTime = 0}};
const auto commonName =
reporter->getEntries(PerformanceEntryType::MARK, "common_name");
ASSERT_EQ(expected, commonName);
}
{
const std::vector<PerformanceEntry> expected = {
{.name = "common_name",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 0,
.duration = 2}};
const auto commonName = toSorted(reporter->getEntriesByName("common_name"));
const auto commonName =
reporter->getEntries(PerformanceEntryType::MEASURE, "common_name");
ASSERT_EQ(expected, commonName);
}
}
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearEntries) {
TEST(PerformanceEntryReporter, PerformanceEntryReporterTestClearMarks) {
auto reporter = PerformanceEntryReporter::getInstance();
reporter->clearEntries();
reporter->reportMark("common_name", 0.0);
reporter->reportMark("mark1", 1.0);
reporter->reportMark("mark2", 2.0);
reporter->reportMark("common_name", 0);
reporter->reportMark("mark1", 1);
reporter->reportMark("mark1", 2.1);
reporter->reportMark("mark2", 2);
reporter->reportMeasure("common_name", 0.0, 2.0);
reporter->reportMeasure("measure1", 0.0, 2.0, 4.0);
reporter->reportMeasure("measure2", 0.0, 0.0, std::nullopt, "mark1", "mark2");
reporter->reportMeasure("measure3", 0.0, 0.0, 5.0, "mark1");
reporter->reportMeasure("common_name", 0, 2);
reporter->reportMeasure("measure1", 0, 2, 4);
reporter->reportMeasure("measure2", 0, 0, std::nullopt, "mark1", "mark2");
reporter->reportMeasure("measure3", 0, 0, 5, "mark1");
reporter->reportMeasure(
"measure4", 1.5, 0.0, std::nullopt, std::nullopt, "mark2");
"measure4", 1.5, 0, std::nullopt, std::nullopt, "mark2");
reporter->clearEntries(PerformanceEntryType::MARK, "common_name");
{
reporter->clearEntries(std::nullopt, "common_name");
auto entriesWithoutCommonName = toSorted(reporter->getEntries());
auto entries = toSorted(reporter->getEntries(PerformanceEntryType::MARK));
std::vector<PerformanceEntry> expected = {
{.name = "measure1",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 0,
.duration = 4},
{.name = "mark1",
.entryType = PerformanceEntryType::MARK,
.startTime = 1,
.duration = 0},
{.name = "measure2",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1,
.duration = 1},
{.name = "measure3",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1,
.duration = 5},
{.name = "measure4",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1.5,
.duration = 0.5},
{.name = "mark2",
.entryType = PerformanceEntryType::MARK,
.startTime = 2,
.duration = 0}};
ASSERT_EQ(6, entriesWithoutCommonName.size());
ASSERT_EQ(expected, entriesWithoutCommonName);
.duration = 0},
{.name = "mark1",
.entryType = PerformanceEntryType::MARK,
.startTime = 2.1,
.duration = 0},
};
ASSERT_EQ(expected, entries);
}
{
reporter->clearEntries(PerformanceEntryType::MARK, "mark1");
auto entriesWithoutMark1 = toSorted(reporter->getEntries());
reporter->clearEntries(PerformanceEntryType::MARK);
ASSERT_EQ(5, entriesWithoutMark1.size());
ASSERT_EQ(
std::vector<PerformanceEntry>(
{{.name = "measure1",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 0,
.duration = 4},
{.name = "measure2",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1,
.duration = 1},
{.name = "measure3",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1,
.duration = 5},
{.name = "measure4",
.entryType = PerformanceEntryType::MEASURE,
.startTime = 1.5,
.duration = 0.5},
{.name = "mark2",
.entryType = PerformanceEntryType::MARK,
.startTime = 2,
.duration = 0}}),
entriesWithoutMark1);
{
auto entries = reporter->getEntries(PerformanceEntryType::MARK);
ASSERT_EQ(entries.size(), 0);
}
{
reporter->clearEntries(PerformanceEntryType::MEASURE);
auto entriesWithoutMeasures = toSorted(reporter->getEntries());
ASSERT_EQ(1, entriesWithoutMeasures.size());
ASSERT_EQ(
std::vector<PerformanceEntry>(
{{.name = "mark2",
.entryType = PerformanceEntryType::MARK,
.startTime = 2,
.duration = 0}}),
entriesWithoutMeasures);
}
reporter->clearEntries();
{
reporter->clearEntries();
auto emptyEntries = reporter->getEntries();
ASSERT_EQ(0, emptyEntries.size());
auto entries = reporter->getEntries();
ASSERT_EQ(entries.size(), 0);
}
}

View File

@ -16,8 +16,8 @@ import type {
} from './PerformanceEntry';
import {PerformanceEntry} from './PerformanceEntry';
import {warnNoNativePerformanceObserver} from './Utilities';
import NativePerformanceObserver from './specs/NativePerformanceObserver';
import {warnNoNativePerformance} from './Utilities';
import NativePerformance from './specs/NativePerformance';
export type PerformanceEventTimingJSON = {
...PerformanceEntryJSON,
@ -85,14 +85,18 @@ function getCachedEventCounts(): Map<string, number> {
if (cachedEventCounts) {
return cachedEventCounts;
}
if (!NativePerformanceObserver) {
warnNoNativePerformanceObserver();
return new Map();
if (!NativePerformance || !NativePerformance?.getEventCounts) {
warnNoNativePerformance();
cachedEventCounts = new Map();
return cachedEventCounts;
}
cachedEventCounts = new Map<string, number>(
NativePerformanceObserver.getEventCounts(),
const eventCounts = new Map<string, number>(
NativePerformance.getEventCounts?.() ?? [],
);
cachedEventCounts = eventCounts;
// $FlowFixMe[incompatible-call]
global.queueMicrotask(() => {
// To be consistent with the calls to the API from the same task,
@ -101,7 +105,8 @@ function getCachedEventCounts(): Map<string, number> {
// after the current task is guaranteed to have finished.
cachedEventCounts = null;
});
return cachedEventCounts ?? new Map();
return eventCounts;
}
/**

View File

@ -13,22 +13,18 @@
import type {
DOMHighResTimeStamp,
PerformanceEntryType,
PerformanceEntryList,
} from './PerformanceEntry';
import type {PerformanceEntryList} from './PerformanceObserver';
import type {DetailType, PerformanceMarkOptions} from './UserTiming';
import {EventCounts} from './EventTiming';
import MemoryInfo from './MemoryInfo';
import {ALWAYS_LOGGED_ENTRY_TYPES} from './PerformanceEntry';
import {warnNoNativePerformanceObserver} from './Utilities';
import {
performanceEntryTypeToRaw,
rawToPerformanceEntry,
} from './RawPerformanceEntry';
import {RawPerformanceEntryTypeValues} from './RawPerformanceEntry';
import ReactNativeStartupTiming from './ReactNativeStartupTiming';
import NativePerformance from './specs/NativePerformance';
import NativePerformanceObserver from './specs/NativePerformanceObserver';
import {PerformanceMark, PerformanceMeasure} from './UserTiming';
import {warnNoNativePerformance} from './Utilities';
@ -47,6 +43,9 @@ export type PerformanceMeasureOptions = {
end?: DOMHighResTimeStamp,
};
const ENTRY_TYPES_AVAILABLE_FROM_TIMELINE: $ReadOnlyArray<PerformanceEntryType> =
['mark', 'measure'];
/**
* Partial implementation of the Performance interface for RN,
* corresponding to the standard in
@ -122,15 +121,12 @@ export default class Performance {
}
clearMarks(markName?: string): void {
if (!NativePerformanceObserver?.clearEntries) {
warnNoNativePerformanceObserver();
if (!NativePerformance?.clearMarks) {
warnNoNativePerformance();
return;
}
NativePerformanceObserver.clearEntries(
RawPerformanceEntryTypeValues.MARK,
markName,
);
NativePerformance.clearMarks(markName);
}
measure(
@ -209,15 +205,12 @@ export default class Performance {
}
clearMeasures(measureName?: string): void {
if (!NativePerformanceObserver?.clearEntries) {
warnNoNativePerformanceObserver();
if (!NativePerformance?.clearMeasures) {
warnNoNativePerformance();
return;
}
NativePerformanceObserver?.clearEntries(
RawPerformanceEntryTypeValues.MEASURE,
measureName,
);
NativePerformance?.clearMeasures(measureName);
}
/**
@ -234,28 +227,28 @@ export default class Performance {
* https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface
*/
getEntries(): PerformanceEntryList {
if (!NativePerformanceObserver?.getEntries) {
warnNoNativePerformanceObserver();
if (!NativePerformance?.getEntries) {
warnNoNativePerformance();
return [];
}
return NativePerformanceObserver.getEntries().map(rawToPerformanceEntry);
return NativePerformance.getEntries().map(rawToPerformanceEntry);
}
getEntriesByType(entryType: PerformanceEntryType): PerformanceEntryList {
if (!ALWAYS_LOGGED_ENTRY_TYPES.includes(entryType)) {
console.warn(
`Performance.getEntriesByType: Only valid for ${JSON.stringify(
ALWAYS_LOGGED_ENTRY_TYPES,
)} entry types, got ${entryType}`,
);
if (
entryType != null &&
!ENTRY_TYPES_AVAILABLE_FROM_TIMELINE.includes(entryType)
) {
console.warn('Deprecated API for given entry type.');
return [];
}
if (!NativePerformanceObserver?.getEntries) {
warnNoNativePerformanceObserver();
if (!NativePerformance?.getEntriesByType) {
warnNoNativePerformance();
return [];
}
return NativePerformanceObserver.getEntries(
return NativePerformance.getEntriesByType(
performanceEntryTypeToRaw(entryType),
).map(rawToPerformanceEntry);
}
@ -265,24 +258,21 @@ export default class Performance {
entryType?: PerformanceEntryType,
): PerformanceEntryList {
if (
entryType !== undefined &&
!ALWAYS_LOGGED_ENTRY_TYPES.includes(entryType)
entryType != null &&
!ENTRY_TYPES_AVAILABLE_FROM_TIMELINE.includes(entryType)
) {
console.warn(
`Performance.getEntriesByName: Only valid for ${JSON.stringify(
ALWAYS_LOGGED_ENTRY_TYPES,
)} entry types, got ${entryType}`,
);
console.warn('Deprecated API for given entry type.');
return [];
}
if (!NativePerformanceObserver?.getEntries) {
warnNoNativePerformanceObserver();
if (!NativePerformance?.getEntriesByName) {
warnNoNativePerformance();
return [];
}
return NativePerformanceObserver.getEntries(
entryType != null ? performanceEntryTypeToRaw(entryType) : undefined,
return NativePerformance.getEntriesByName(
entryName,
entryType != null ? performanceEntryTypeToRaw(entryType) : undefined,
).map(rawToPerformanceEntry);
}
}

View File

@ -21,11 +21,6 @@ export type PerformanceEntryJSON = {
...
};
export const ALWAYS_LOGGED_ENTRY_TYPES: $ReadOnlyArray<PerformanceEntryType> = [
'mark',
'measure',
];
export class PerformanceEntry {
#name: string;
#entryType: PerformanceEntryType;
@ -69,3 +64,5 @@ export class PerformanceEntry {
};
}
}
export type PerformanceEntryList = $ReadOnlyArray<PerformanceEntry>;

View File

@ -11,20 +11,18 @@
import type {
DOMHighResTimeStamp,
PerformanceEntryType,
PerformanceEntryList,
} from './PerformanceEntry';
import {PerformanceEventTiming} from './EventTiming';
import {PerformanceEntry} from './PerformanceEntry';
import {
performanceEntryTypeToRaw,
rawToPerformanceEntry,
rawToPerformanceEntryType,
} from './RawPerformanceEntry';
import NativePerformanceObserver from './specs/NativePerformanceObserver';
import type {OpaqueNativeObserverHandle} from './specs/NativePerformanceObserver';
import {warnNoNativePerformanceObserver} from './Utilities';
export type PerformanceEntryList = $ReadOnlyArray<PerformanceEntry>;
import NativePerformance from './specs/NativePerformance';
import type {OpaqueNativeObserverHandle} from './specs/NativePerformance';
import {warnNoNativePerformance} from './Utilities';
export {PerformanceEntry} from './PerformanceEntry';
@ -76,15 +74,15 @@ export type PerformanceObserverInit = {
};
function getSupportedPerformanceEntryTypes(): $ReadOnlyArray<PerformanceEntryType> {
if (!NativePerformanceObserver) {
if (!NativePerformance) {
return Object.freeze([]);
}
if (!NativePerformanceObserver.getSupportedPerformanceEntryTypes) {
if (!NativePerformance.getSupportedPerformanceEntryTypes) {
// fallback if getSupportedPerformanceEntryTypes is not defined on native side
return Object.freeze(['mark', 'measure', 'event']);
}
return Object.freeze(
NativePerformanceObserver.getSupportedPerformanceEntryTypes().map(
NativePerformance.getSupportedPerformanceEntryTypes().map(
rawToPerformanceEntryType,
),
);
@ -121,11 +119,8 @@ export class PerformanceObserver {
}
observe(options: PerformanceObserverInit): void {
if (
!NativePerformanceObserver ||
NativePerformanceObserver.observe == null
) {
warnNoNativePerformanceObserver();
if (!NativePerformance || NativePerformance.observe == null) {
warnNoNativePerformance();
return;
}
@ -137,12 +132,12 @@ export class PerformanceObserver {
if (options.entryTypes) {
this.#type = 'multiple';
NativePerformanceObserver.observe?.(this.#nativeObserverHandle, {
NativePerformance.observe?.(this.#nativeObserverHandle, {
entryTypes: options.entryTypes.map(performanceEntryTypeToRaw),
});
} else if (options.type) {
this.#type = 'single';
NativePerformanceObserver.observe?.(this.#nativeObserverHandle, {
NativePerformance.observe?.(this.#nativeObserverHandle, {
type: performanceEntryTypeToRaw(options.type),
buffered: options.buffered,
durationThreshold: options.durationThreshold,
@ -151,35 +146,28 @@ export class PerformanceObserver {
}
disconnect(): void {
if (!NativePerformanceObserver) {
warnNoNativePerformanceObserver();
if (!NativePerformance) {
warnNoNativePerformance();
return;
}
if (
this.#nativeObserverHandle == null ||
!NativePerformanceObserver.disconnect
) {
if (this.#nativeObserverHandle == null || !NativePerformance.disconnect) {
return;
}
NativePerformanceObserver.disconnect(this.#nativeObserverHandle);
NativePerformance.disconnect(this.#nativeObserverHandle);
}
#createNativeObserver(): OpaqueNativeObserverHandle {
if (
!NativePerformanceObserver ||
!NativePerformanceObserver.createObserver
) {
warnNoNativePerformanceObserver();
if (!NativePerformance || !NativePerformance.createObserver) {
warnNoNativePerformance();
return;
}
this.#calledAtLeastOnce = false;
return NativePerformanceObserver.createObserver(() => {
// $FlowNotNull
const rawEntries = NativePerformanceObserver.takeRecords?.(
return NativePerformance.createObserver(() => {
const rawEntries = NativePerformance.takeRecords?.(
this.#nativeObserverHandle,
true, // sort records
);
@ -193,7 +181,7 @@ export class PerformanceObserver {
let droppedEntriesCount = 0;
if (!this.#calledAtLeastOnce) {
droppedEntriesCount =
NativePerformanceObserver.getDroppedEntriesCount?.(
NativePerformance.getDroppedEntriesCount?.(
this.#nativeObserverHandle,
) ?? 0;
this.#calledAtLeastOnce = true;

View File

@ -12,7 +12,7 @@ import type {PerformanceEntryType} from './PerformanceEntry';
import type {
RawPerformanceEntry,
RawPerformanceEntryType,
} from './specs/NativePerformanceObserver';
} from './specs/NativePerformance';
import {PerformanceEventTiming} from './EventTiming';
import {PerformanceLongTaskTiming} from './LongTasks';

View File

@ -16,10 +16,3 @@ export function warnNoNativePerformance() {
'Missing native implementation of Performance',
);
}
export function warnNoNativePerformanceObserver() {
warnOnce(
'missing-native-performance-observer',
'Missing native implementation of PerformanceObserver',
);
}

View File

@ -1,135 +0,0 @@
/**
* 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.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';
jest.mock(
'../specs/NativePerformance',
() => require('../specs/__mocks__/NativePerformance').default,
);
jest.mock(
'../specs/NativePerformanceObserver',
() => require('../specs/__mocks__/NativePerformanceObserver').default,
);
// NOTE: Jest mocks of transitive dependencies don't appear to work with
// ES6 module imports, therefore forced to use commonjs style imports here.
const Performance = require('../Performance').default;
const logMockEntry =
require('../specs/__mocks__/NativePerformanceObserver').logMockEntry;
describe('EventCounts', () => {
it('defines EventCounts for Performance', () => {
const eventCounts = new Performance().eventCounts;
expect(eventCounts).not.toBeUndefined();
});
it('consistently implements the API for EventCounts', async () => {
let interactionId = 0;
const eventDefaultValues = {
entryType: RawPerformanceEntryTypeValues.EVENT,
startTime: 0,
duration: 100,
processingStart: 0,
processingEnd: 100,
};
logMockEntry({
name: 'click',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'input',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'input',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
const eventCounts = new Performance().eventCounts;
expect(eventCounts.size).toBe(3);
expect(Array.from(eventCounts.entries())).toStrictEqual([
['click', 1],
['input', 2],
['keyup', 3],
]);
expect(eventCounts.get('click')).toEqual(1);
expect(eventCounts.get('input')).toEqual(2);
expect(eventCounts.get('keyup')).toEqual(3);
expect(eventCounts.has('click')).toEqual(true);
expect(eventCounts.has('input')).toEqual(true);
expect(eventCounts.has('keyup')).toEqual(true);
expect(Array.from(eventCounts.keys())).toStrictEqual([
'click',
'input',
'keyup',
]);
expect(Array.from(eventCounts.values())).toStrictEqual([1, 2, 3]);
await jest.runAllTicks();
logMockEntry({
name: 'input',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
logMockEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
expect(Array.from(eventCounts.values())).toStrictEqual([1, 3, 5]);
await jest.runAllTicks();
logMockEntry({
name: 'click',
...eventDefaultValues,
interactionId: interactionId++,
});
await jest.runAllTicks();
logMockEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
expect(Array.from(eventCounts.values())).toStrictEqual([2, 3, 6]);
});
});

View File

@ -9,21 +9,89 @@
* @oncall react_native
*/
const Performance = require('../Performance').default;
import {performanceEntryTypeToRaw} from '../RawPerformanceEntry';
import {reportEntry} from '../specs/__mocks__/NativePerformanceMock';
jest.mock(
'../specs/NativePerformance',
() => require('../specs/__mocks__/NativePerformance').default,
jest.mock('../specs/NativePerformance', () =>
require('../specs/__mocks__/NativePerformanceMock'),
);
jest.mock(
'../specs/NativePerformanceObserver',
() => require('../specs/__mocks__/NativePerformanceObserver').default,
);
const NativePerformanceMock =
require('../specs/__mocks__/NativePerformanceMock').default;
describe('Performance', () => {
it('clearEntries removes correct entry types', async () => {
const performance = new Performance();
beforeEach(() => {
jest.resetModules();
const Performance = require('../Performance').default;
// $FlowExpectedError[cannot-write]
global.performance = new Performance();
});
it('reports marks and measures', () => {
NativePerformanceMock.setCurrentTime(25);
performance.mark('mark-now');
performance.mark('mark-in-the-past', {
startTime: 10,
});
performance.mark('mark-in-the-future', {
startTime: 50,
});
performance.measure('measure-with-specific-time', {
start: 30,
duration: 4,
});
performance.measure('measure-now-with-start-mark', 'mark-in-the-past');
performance.measure(
'measure-with-start-and-end-mark',
'mark-in-the-past',
'mark-in-the-future',
);
const entries = performance.getEntries();
expect(entries.length).toBe(6);
expect(entries.map(entry => entry.toJSON())).toEqual([
{
duration: 0,
entryType: 'mark',
name: 'mark-in-the-past',
startTime: 10,
},
{
duration: -10,
entryType: 'measure',
name: 'measure-now-with-start-mark',
startTime: 10,
},
{
duration: 40,
entryType: 'measure',
name: 'measure-with-start-and-end-mark',
startTime: 10,
},
{
duration: 0,
entryType: 'mark',
name: 'mark-now',
startTime: 25,
},
{
duration: 4,
entryType: 'measure',
name: 'measure-with-specific-time',
startTime: 30,
},
{
duration: 0,
entryType: 'mark',
name: 'mark-in-the-future',
startTime: 50,
},
]);
});
it('clearMarks and clearMeasures remove correct entry types', async () => {
performance.mark('entry1', {startTime: 0});
performance.mark('mark2', {startTime: 0});
@ -53,7 +121,6 @@ describe('Performance', () => {
});
it('getEntries only works with allowed entry types', async () => {
const performance = new Performance();
performance.clearMarks();
performance.clearMeasures();
@ -82,7 +149,6 @@ describe('Performance', () => {
});
it('getEntries works with marks and measures', async () => {
const performance = new Performance();
performance.clearMarks();
performance.clearMeasures();
@ -115,4 +181,108 @@ describe('Performance', () => {
performance.getEntriesByName('entry1', 'measure').map(e => e.entryType),
).toStrictEqual(['measure']);
});
it('defines EventCounts for Performance', () => {
expect(performance.eventCounts).not.toBeUndefined();
});
it('consistently implements the API for EventCounts', async () => {
let interactionId = 0;
const eventDefaultValues = {
entryType: performanceEntryTypeToRaw('event'),
startTime: 0, // startTime
duration: 100, // duration
processingStart: 0, // processing start
processingEnd: 100, // processingEnd
};
reportEntry({
name: 'click',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'input',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'input',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
const eventCounts = performance.eventCounts;
expect(eventCounts.size).toBe(3);
expect(Array.from(eventCounts.entries())).toStrictEqual([
['click', 1],
['input', 2],
['keyup', 3],
]);
expect(eventCounts.get('click')).toEqual(1);
expect(eventCounts.get('input')).toEqual(2);
expect(eventCounts.get('keyup')).toEqual(3);
expect(eventCounts.has('click')).toEqual(true);
expect(eventCounts.has('input')).toEqual(true);
expect(eventCounts.has('keyup')).toEqual(true);
expect(Array.from(eventCounts.keys())).toStrictEqual([
'click',
'input',
'keyup',
]);
expect(Array.from(eventCounts.values())).toStrictEqual([1, 2, 3]);
await jest.runAllTicks();
reportEntry({
name: 'input',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
reportEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
expect(Array.from(eventCounts.values())).toStrictEqual([1, 3, 5]);
await jest.runAllTicks();
reportEntry({
name: 'click',
...eventDefaultValues,
interactionId: interactionId++,
});
await jest.runAllTicks();
reportEntry({
name: 'keyup',
...eventDefaultValues,
interactionId: interactionId++,
});
expect(Array.from(eventCounts.values())).toStrictEqual([2, 3, 6]);
});
});

View File

@ -9,30 +9,26 @@
* @oncall react_native
*/
import type {PerformanceEntryList} from '../PerformanceObserver';
jest.mock(
'../specs/NativePerformance',
() => require('../specs/__mocks__/NativePerformance').default,
() => require('../specs/__mocks__/NativePerformanceMock').default,
);
jest.mock(
'../specs/NativePerformanceObserver',
() => require('../specs/__mocks__/NativePerformanceObserver').default,
);
// // NOTE: Jest mocks of transitive dependencies don't appear to work with
// // ES6 module imports, therefore forced to use commonjs style imports here.
const {PerformanceObserver} = require('../PerformanceObserver');
const NativePerformanceMock =
require('../specs/__mocks__/NativePerformance').default;
describe('PerformanceObserver', () => {
beforeEach(() => {
jest.resetModules();
const Performance = require('../Performance').default;
// $FlowExpectedError[cannot-write]
global.performance = new Performance();
global.PerformanceObserver =
require('../PerformanceObserver').PerformanceObserver;
});
it('prevents durationThreshold to be used together with entryTypes', async () => {
const observer = new PerformanceObserver((list, _observer) => {});
expect(() =>
// $FlowExpectedError[incompatible-call]
observer.observe({entryTypes: ['event', 'mark'], durationThreshold: 100}),
).toThrow();
});
@ -46,7 +42,10 @@ describe('PerformanceObserver', () => {
observer.observe({type: 'measure', durationThreshold: 100});
NativePerformanceMock?.measure('measure1', 0, 200);
performance.measure('measure1', {
start: 0,
duration: 10,
});
await jest.runAllTicks();
expect(entries).toHaveLength(1);

View File

@ -16,6 +16,31 @@ export type NativeMemoryInfo = {[key: string]: ?number};
export type ReactNativeStartupTiming = {[key: string]: ?number};
export type RawPerformanceEntryType = number;
export type RawPerformanceEntry = {
name: string,
entryType: RawPerformanceEntryType,
startTime: number,
duration: number,
// For "event" entries only:
processingStart?: number,
processingEnd?: number,
interactionId?: number,
};
export type OpaqueNativeObserverHandle = mixed;
export type NativeBatchedObserverCallback = () => void;
export type PerformanceObserverInit = {
entryTypes?: $ReadOnlyArray<number>,
type?: number,
buffered?: boolean,
durationThreshold?: number,
};
export interface Spec extends TurboModule {
+now?: () => number;
+mark: (name: string, startTime: number) => void;
@ -27,8 +52,36 @@ export interface Spec extends TurboModule {
startMark?: string,
endMark?: string,
) => void;
+clearMarks?: (entryName?: string) => void;
+clearMeasures?: (entryName?: string) => void;
+getEntries?: () => $ReadOnlyArray<RawPerformanceEntry>;
+getEntriesByName?: (
entryName: string,
entryType?: ?RawPerformanceEntryType,
) => $ReadOnlyArray<RawPerformanceEntry>;
+getEntriesByType?: (
entryType: RawPerformanceEntryType,
) => $ReadOnlyArray<RawPerformanceEntry>;
+getEventCounts?: () => $ReadOnlyArray<[string, number]>;
+getSimpleMemoryInfo: () => NativeMemoryInfo;
+getReactNativeStartupTiming: () => ReactNativeStartupTiming;
+createObserver?: (
callback: NativeBatchedObserverCallback,
) => OpaqueNativeObserverHandle;
+getDroppedEntriesCount?: (observer: OpaqueNativeObserverHandle) => number;
+observe?: (
observer: OpaqueNativeObserverHandle,
options: PerformanceObserverInit,
) => void;
+disconnect?: (observer: OpaqueNativeObserverHandle) => void;
+takeRecords?: (
observer: OpaqueNativeObserverHandle,
sort: boolean,
) => $ReadOnlyArray<RawPerformanceEntry>;
+getSupportedPerformanceEntryTypes?: () => $ReadOnlyArray<RawPerformanceEntryType>;
}
export default (TurboModuleRegistry.get<Spec>('NativePerformanceCxx'): ?Spec);

View File

@ -1,69 +0,0 @@
/**
* 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.
*
* @flow strict
* @format
*/
import type {TurboModule} from '../../../../../Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from '../../../../../Libraries/TurboModule/TurboModuleRegistry';
export type RawPerformanceEntryType = number;
export type OpaqueNativeObserverHandle = mixed;
export type NativeBatchedObserverCallback = () => void;
export type RawPerformanceEntry = {|
name: string,
entryType: RawPerformanceEntryType,
startTime: number,
duration: number,
// For "event" entries only:
processingStart?: number,
processingEnd?: number,
interactionId?: number,
|};
export type PerformanceObserverInit = {
entryTypes?: $ReadOnlyArray<number>,
type?: number,
buffered?: boolean,
durationThreshold?: number,
};
export interface Spec extends TurboModule {
+getEventCounts: () => $ReadOnlyArray<[string, number]>;
+createObserver?: (
callback: NativeBatchedObserverCallback,
) => OpaqueNativeObserverHandle;
+getDroppedEntriesCount?: (observer: OpaqueNativeObserverHandle) => number;
+observe?: (
observer: OpaqueNativeObserverHandle,
options: PerformanceObserverInit,
) => void;
+disconnect?: (observer: OpaqueNativeObserverHandle) => void;
+takeRecords?: (
observer: OpaqueNativeObserverHandle,
sort: boolean,
) => $ReadOnlyArray<RawPerformanceEntry>;
+clearEntries: (
entryType?: RawPerformanceEntryType,
entryName?: string,
) => void;
+getEntries: (
entryType?: RawPerformanceEntryType,
entryName?: string,
) => $ReadOnlyArray<RawPerformanceEntry>;
+getSupportedPerformanceEntryTypes: () => $ReadOnlyArray<RawPerformanceEntryType>;
}
export default (TurboModuleRegistry.get<Spec>(
'NativePerformanceObserverCxx',
): ?Spec);

View File

@ -1,69 +0,0 @@
/**
* 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.
*
* @flow strict
* @format
*/
import type {
NativeMemoryInfo,
ReactNativeStartupTiming,
} from '../NativePerformance';
import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry';
import NativePerformance from '../NativePerformance';
import {logMockEntry} from './NativePerformanceObserver';
const marks: Map<string, number> = new Map();
const NativePerformanceMock: typeof NativePerformance = {
mark: (name: string, startTime: number): void => {
NativePerformance?.mark(name, startTime);
marks.set(name, startTime);
logMockEntry({
entryType: RawPerformanceEntryTypeValues.MARK,
name,
startTime,
duration: 0,
});
},
measure: (
name: string,
startTime: number,
endTime: number,
duration?: number,
startMark?: string,
endMark?: string,
): void => {
const start = startMark != null ? marks.get(startMark) ?? 0 : startTime;
const end = endMark != null ? marks.get(endMark) ?? 0 : endTime;
NativePerformance?.measure(name, start, end);
logMockEntry({
entryType: RawPerformanceEntryTypeValues.MEASURE,
name,
startTime: start,
duration: duration ?? end - start,
});
},
getSimpleMemoryInfo: (): NativeMemoryInfo => {
return {};
},
getReactNativeStartupTiming: (): ReactNativeStartupTiming => {
return {
startTime: 0,
endTime: 0,
executeJavaScriptBundleEntryPointStart: 0,
executeJavaScriptBundleEntryPointEnd: 0,
initializeRuntimeStart: 0,
initializeRuntimeEnd: 0,
};
},
};
export default NativePerformanceMock;

View File

@ -0,0 +1,247 @@
/**
* 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.
*
* @flow strict
* @format
*/
import type {
NativeBatchedObserverCallback,
NativeMemoryInfo,
RawPerformanceEntry,
ReactNativeStartupTiming,
PerformanceObserverInit,
OpaqueNativeObserverHandle,
RawPerformanceEntryType,
} from '../NativePerformance';
import typeof NativePerformance from '../NativePerformance';
import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry';
type MockObserver = {
handleEntry: (entry: RawPerformanceEntry) => void,
callback: NativeBatchedObserverCallback,
didScheduleFlushBuffer: boolean,
entries: Array<RawPerformanceEntry>,
options: PerformanceObserverInit,
droppedEntriesCount: number,
};
const eventCounts: Map<string, number> = new Map();
const observers: Set<MockObserver> = new Set();
const marks: Map<string, number> = new Map();
let entries: Array<RawPerformanceEntry> = [];
function getMockObserver(
opaqueNativeObserverHandle: OpaqueNativeObserverHandle,
): MockObserver {
return opaqueNativeObserverHandle as $FlowFixMe as MockObserver;
}
function createMockObserver(callback: NativeBatchedObserverCallback) {
const observer: MockObserver = {
callback,
didScheduleFlushBuffer: false,
entries: [],
options: {},
droppedEntriesCount: 0,
handleEntry: (entry: RawPerformanceEntry) => {
if (
observer.options.type !== entry.entryType &&
!observer.options.entryTypes?.includes(entry.entryType)
) {
return;
}
if (
entry.entryType === RawPerformanceEntryTypeValues.EVENT &&
entry.duration < (observer.options?.durationThreshold ?? 0)
) {
return;
}
observer.entries.push(entry);
if (!observer.didScheduleFlushBuffer) {
observer.didScheduleFlushBuffer = true;
// $FlowFixMe[incompatible-call]
global.queueMicrotask(() => {
observer.didScheduleFlushBuffer = false;
// We want to emulate the way it's done in native (i.e. async/batched)
observer.callback();
});
}
},
};
return observer;
}
export function reportEntry(entry: RawPerformanceEntry) {
entries.push(entry);
switch (entry.entryType) {
case RawPerformanceEntryTypeValues.MARK:
marks.set(entry.name, entry.startTime);
break;
case RawPerformanceEntryTypeValues.MEASURE:
break;
case RawPerformanceEntryTypeValues.EVENT:
eventCounts.set(entry.name, (eventCounts.get(entry.name) ?? 0) + 1);
break;
}
for (const observer of observers) {
observer.handleEntry(entry);
}
}
let currentTime: number = 12;
const NativePerformanceMock = {
setCurrentTime: (time: number): void => {
currentTime = time;
},
now: (): number => currentTime,
mark: (name: string, startTime: number): void => {
marks.set(name, startTime);
reportEntry({
entryType: RawPerformanceEntryTypeValues.MARK,
name,
startTime,
duration: 0,
});
},
measure: (
name: string,
startTime: number,
endTime: number,
duration?: number,
startMark?: string,
endMark?: string,
): void => {
const start = startMark != null ? marks.get(startMark) ?? 0 : startTime;
const end = endMark != null ? marks.get(endMark) ?? 0 : endTime;
reportEntry({
entryType: RawPerformanceEntryTypeValues.MEASURE,
name,
startTime: start,
duration: duration ?? end - start,
});
},
getSimpleMemoryInfo: (): NativeMemoryInfo => {
return {};
},
getReactNativeStartupTiming: (): ReactNativeStartupTiming => {
return {
startTime: 0,
endTime: 0,
executeJavaScriptBundleEntryPointStart: 0,
executeJavaScriptBundleEntryPointEnd: 0,
initializeRuntimeStart: 0,
initializeRuntimeEnd: 0,
};
},
getEventCounts: (): $ReadOnlyArray<[string, number]> => {
return Array.from(eventCounts.entries());
},
createObserver: (
callback: NativeBatchedObserverCallback,
): OpaqueNativeObserverHandle => {
return createMockObserver(callback);
},
getDroppedEntriesCount: (observer: OpaqueNativeObserverHandle): number => {
return getMockObserver(observer).droppedEntriesCount;
},
observe: (
observer: OpaqueNativeObserverHandle,
options: PerformanceObserverInit,
): void => {
const mockObserver = getMockObserver(observer);
mockObserver.options = options;
observers.add(mockObserver);
},
disconnect: (observer: OpaqueNativeObserverHandle): void => {
const mockObserver = getMockObserver(observer);
observers.delete(mockObserver);
},
takeRecords: (
observer: OpaqueNativeObserverHandle,
): $ReadOnlyArray<RawPerformanceEntry> => {
const mockObserver = getMockObserver(observer);
const observerEntries = mockObserver.entries;
mockObserver.entries = [];
return observerEntries.sort((a, b) => a.startTime - b.startTime);
},
clearMarks: (entryName?: string) => {
if (entryName != null) {
marks.delete(entryName);
} else {
marks.clear();
}
entries = entries.filter(
entry =>
entry.entryType !== RawPerformanceEntryTypeValues.MARK ||
(entryName != null && entry.name !== entryName),
);
},
clearMeasures: (entryName?: string) => {
entries = entries.filter(
entry =>
entry.entryType !== RawPerformanceEntryTypeValues.MEASURE ||
(entryName != null && entry.name !== entryName),
);
},
getEntries: (): $ReadOnlyArray<RawPerformanceEntry> => {
return [...entries].sort((a, b) => a.startTime - b.startTime);
},
getEntriesByName: (
entryName: string,
entryType?: ?RawPerformanceEntryType,
): $ReadOnlyArray<RawPerformanceEntry> => {
return NativePerformanceMock.getEntries().filter(
entry =>
(entryType == null || entry.entryType === entryType) &&
entry.name === entryName,
);
},
getEntriesByType: (
entryType: RawPerformanceEntryType,
): $ReadOnlyArray<RawPerformanceEntry> => {
return entries.filter(entry => entry.entryType === entryType);
},
getSupportedPerformanceEntryTypes:
(): $ReadOnlyArray<RawPerformanceEntryType> => {
return [
RawPerformanceEntryTypeValues.MARK,
RawPerformanceEntryTypeValues.MEASURE,
RawPerformanceEntryTypeValues.EVENT,
];
},
};
(NativePerformanceMock: NativePerformance);
export default NativePerformanceMock;

View File

@ -1,154 +0,0 @@
/**
* 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.
*
* @flow strict
* @format
*/
import type {
NativeBatchedObserverCallback,
RawPerformanceEntry,
RawPerformanceEntryType,
OpaqueNativeObserverHandle,
PerformanceObserverInit,
Spec as NativePerformanceObserver,
} from '../NativePerformanceObserver';
import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry';
jest.mock(
'../NativePerformance',
() => require('../__mocks__/NativePerformance').default,
);
jest.mock(
'../NativePerformanceObserver',
() => require('../__mocks__/NativePerformanceObserver').default,
);
const eventCounts: Map<string, number> = new Map();
let observers: MockObserver[] = [];
let entries: Array<RawPerformanceEntry> = [];
export function logMockEntry(entry: RawPerformanceEntry) {
entries.push(entry);
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
eventCounts.set(entry.name, (eventCounts.get(entry.name) ?? 0) + 1);
}
for (const observer of observers) {
if (
observer.options.type !== entry.entryType &&
!observer.options.entryTypes?.includes(entry.entryType)
) {
continue;
}
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
const {durationThreshold = 0} = observer.options;
if (durationThreshold > 0 && entry.duration < durationThreshold) {
continue;
}
}
observer.entries.push(entry);
// $FlowFixMe[incompatible-call]
global.queueMicrotask(() => {
// We want to emulate the way it's done in native (i.e. async/batched)
observer.callback();
});
}
}
type MockObserver = {
callback: NativeBatchedObserverCallback,
entries: Array<RawPerformanceEntry>,
options: PerformanceObserverInit,
droppedEntriesCount: number,
};
const NativePerformanceObserverMock: NativePerformanceObserver = {
getEventCounts: (): $ReadOnlyArray<[string, number]> => {
return Array.from(eventCounts.entries());
},
createObserver: (
callback: NativeBatchedObserverCallback,
): OpaqueNativeObserverHandle => {
const observer: MockObserver = {
callback,
entries: [],
options: {},
droppedEntriesCount: 0,
};
return observer;
},
getDroppedEntriesCount: (observer: OpaqueNativeObserverHandle): number => {
// $FlowFixMe
const mockObserver = (observer: any) as MockObserver;
return mockObserver.droppedEntriesCount;
},
observe: (
observer: OpaqueNativeObserverHandle,
options: PerformanceObserverInit,
): void => {
// $FlowFixMe
const mockObserver = (observer: any) as MockObserver;
mockObserver.options = options;
observers.push(mockObserver);
},
disconnect: (observer: OpaqueNativeObserverHandle): void => {
// $FlowFixMe
const mockObserver = (observer: any) as MockObserver;
observers = observers.filter(e => e !== mockObserver);
},
takeRecords: (
observer: OpaqueNativeObserverHandle,
): $ReadOnlyArray<RawPerformanceEntry> => {
// $FlowFixMe
const mockObserver = (observer: any) as MockObserver;
const observerEntries = mockObserver.entries;
mockObserver.entries = [];
return observerEntries;
},
clearEntries: (entryType?: RawPerformanceEntryType, entryName?: string) => {
entries = entries.filter(
e =>
(entryType != null && e.entryType !== entryType) ||
(entryName != null && e.name !== entryName),
);
},
getEntries: (
entryType?: RawPerformanceEntryType,
entryName?: string,
): $ReadOnlyArray<RawPerformanceEntry> => {
return entries.filter(
e =>
(entryType == null || e.entryType === entryType) &&
(entryName == null || e.name === entryName),
);
},
getSupportedPerformanceEntryTypes:
(): $ReadOnlyArray<RawPerformanceEntryType> => {
return [
RawPerformanceEntryTypeValues.MARK,
RawPerformanceEntryTypeValues.MEASURE,
RawPerformanceEntryTypeValues.EVENT,
];
},
};
export default NativePerformanceObserverMock;

View File

@ -1,86 +0,0 @@
/**
* 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.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import type {PerformanceEntryList} from '../../PerformanceObserver';
jest.mock(
'../NativePerformanceObserver',
() => require('../__mocks__/NativePerformanceObserver').default,
);
const NativePerformanceMock = require('../__mocks__/NativePerformance').default;
const PerformanceObserver =
require('../../PerformanceObserver').PerformanceObserver;
describe('NativePerformanceMock', () => {
it('marks get reported', async () => {
let entries: PerformanceEntryList = [];
const observer = new PerformanceObserver((list, _observer) => {
entries = [...entries, ...list.getEntries()];
});
observer.observe({type: 'mark'});
NativePerformanceMock?.mark('mark1', 0);
NativePerformanceMock?.mark('mark2', 5);
NativePerformanceMock?.mark('mark3', 10);
await jest.runAllTicks();
expect(entries).toHaveLength(3);
expect(entries.map(e => e.name)).toStrictEqual(['mark1', 'mark2', 'mark3']);
expect(entries.map(e => e.startTime)).toStrictEqual([0, 5, 10]);
});
it('measures get reported', async () => {
let entries: PerformanceEntryList = [];
const observer = new PerformanceObserver((list, _observer) => {
entries = [...entries, ...list.getEntries()];
});
observer.observe({entryTypes: ['measure']});
NativePerformanceMock?.mark('mark0', 0.0);
NativePerformanceMock?.mark('mark1', 1.0);
NativePerformanceMock?.mark('mark2', 2.0);
NativePerformanceMock?.measure('measure0', 0, 2);
NativePerformanceMock?.measure('measure1', 0, 2, 4);
NativePerformanceMock?.measure(
'measure2',
0,
0,
undefined,
'mark1',
'mark2',
);
NativePerformanceMock?.measure('measure3', 0, 0, 5, 'mark1');
NativePerformanceMock?.measure(
'measure4',
1.5,
0,
undefined,
undefined,
'mark2',
);
await jest.runAllTicks();
expect(entries).toHaveLength(5);
expect(entries.map(e => e.name)).toStrictEqual([
'measure0',
'measure1',
'measure2',
'measure3',
'measure4',
]);
expect(entries.map(e => e.startTime)).toStrictEqual([0, 0, 1, 1, 1.5]);
expect(entries.map(e => e.duration)).toStrictEqual([2, 4, 1, 5, 0.5]);
});
});

View File

@ -1,68 +0,0 @@
/**
* 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.
*
* @flow strict-local
* @format
* @oncall react_native
*/
import NativePerformanceObserverMock, {
logMockEntry,
} from '../__mocks__/NativePerformanceObserver';
import {RawPerformanceEntryTypeValues} from '../../RawPerformanceEntry';
describe('NativePerformanceObserver', () => {
it('correctly clears/gets entries', async () => {
logMockEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 0,
});
logMockEntry({
name: 'event1',
entryType: RawPerformanceEntryTypeValues.EVENT,
startTime: 0,
duration: 0,
});
expect(
NativePerformanceObserverMock.getEntries().map(e => e.name),
).toStrictEqual(['mark1', 'event1']);
NativePerformanceObserverMock.clearEntries();
expect(NativePerformanceObserverMock.getEntries()).toStrictEqual([]);
logMockEntry({
name: 'entry1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 0,
});
logMockEntry({
name: 'entry2',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 0,
});
logMockEntry({
name: 'entry1',
entryType: RawPerformanceEntryTypeValues.EVENT,
startTime: 0,
duration: 0,
});
NativePerformanceObserverMock.clearEntries(undefined, 'entry1');
expect(
NativePerformanceObserverMock.getEntries().map(e => e.name),
).toStrictEqual(['entry2']);
});
});