Skip to content

Commit

Permalink
New implementation of PerformanceObserver and related APIs (fixes #45122
Browse files Browse the repository at this point in the history
) (#45206)

Summary:
Changelog: [internal]

(this functionality hasn't been enabled in OSS yet, so no changelog necessary).

Fix #45122.

performance.mark is currently O(1) but performance.clearMark is O(n) (being n the number of entries in the buffer), which makes this operation very slow.

### Changes overview

 - Created new `PerformanceEntryBuffer` abstraction with the following subtypes, that differ on how entries are stored:
   - `PerformanceEntryCircularBuffer` - stores them in a ring buffer that was already implemented, removed key lookup cache (`BoundedConsumableBuffer`)
   - `PerformanceEntryKeyedBuffer` - stores them in a `unordered_map`, allowing for faster retrieval by type
   - `PerformanceEntryLinearBuffer` - a simple infinite buffer based on `std::vector`, currently used in a `PerformanceObserver`
 - Created `PerformanceObserver` abstraction on native side.
 - Created `PerformanceObserverRegistry` that collects active observers and forwards entries to observers that should retrieve them
 - Moved some method implementations to `.cpp` files to reduce potential compilation time slowdown. As the `PerformanceEntryReporter` can be included from anywhere in the code, it will be beneficial to make header files as light as possible.
 - Add comments to methods that note which standard is the method from/for.
 - Added some `[[nodiscard]]` attributes
 - Since the logic of routing entries to observers is moved to native side, JS side of the code got much simplified
 - If ever needed, `PerformanceObserver` can be created from native-side

Standards covered:

 - https://www.w3.org/TR/performance-timeline
 - https://www.w3.org/TR/event-timing/
 - https://w3c.github.io/timing-entrytypes-registry
 - https://w3c.github.io/user-timing/

Pull Request resolved: #45206

Test Plan:
I've tested this e2e on IGVR and in the RNTester playground for the performance APIs. Everything works as expected.
There are also new unit tests for this.

C++ test results: https://www.internalfb.com/intern/testinfra/testconsole/testrun/8725724513169247/

Reviewed By: rshest

Differential Revision: D63101520

Pulled By: rubennorte

fbshipit-source-id: 5970b5c14692ff33ffda44a9f09067f6a758bdbe
  • Loading branch information
robik authored and facebook-github-bot committed Sep 26, 2024
1 parent 94b7793 commit 38b3b21
Show file tree
Hide file tree
Showing 35 changed files with 2,042 additions and 1,732 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@
#include <cxxreact/ReactMarker.h>
#include <jsi/instrumentation.h>
#include <react/performance/timeline/PerformanceEntryReporter.h>
#if __has_include(<reactperflogger/fusebox/FuseboxTracer.h>)
#include <reactperflogger/fusebox/FuseboxTracer.h>
#define HAS_FUSEBOX
#endif
#include "NativePerformance.h"

#ifdef RN_DISABLE_OSS_PLUGIN_HEADER
#include "Plugins.h"
#endif

#ifdef WITH_PERFETTO
#include <reactperflogger/ReactPerfetto.h>
Expand Down Expand Up @@ -106,8 +112,11 @@ void NativePerformance::measure(
std::optional<std::string> endMark) {
auto [trackName, eventName] = parseTrackName(name);

#ifdef HAS_FUSEBOX
FuseboxTracer::getFuseboxTracer().addEvent(
eventName, (uint64_t)startTime, (uint64_t)endTime, trackName);
#endif

PerformanceEntryReporter::getInstance()->measure(
eventName, startTime, endTime, duration, startMark, endMark);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

#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 <memory>
#include <string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
* LICENSE file in the root directory of this source tree.
*/

#include <memory>

#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(
Expand All @@ -25,90 +27,152 @@ NativePerformanceObserverModuleProvider(

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)) {}

void NativePerformanceObserver::startReporting(
jsi::Runtime& /*rt*/,
PerformanceEntryType entryType) {
auto reporter = PerformanceEntryReporter::getInstance();
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);
};

reporter->startReporting(entryType);
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;
}

void NativePerformanceObserver::stopReporting(
jsi::Runtime& /*rt*/,
PerformanceEntryType entryType) {
auto reporter = PerformanceEntryReporter::getInstance();
double NativePerformanceObserver::getDroppedEntriesCount(
jsi::Runtime& rt,
jsi::Object observerObj) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));

reporter->stopReporting(entryType);
if (!observerWrapper) {
return 0;
}

auto observer = observerWrapper->observer;
return observer->getDroppedEntriesCount();
}

void NativePerformanceObserver::setIsBuffered(
jsi::Runtime& /*rt*/,
const std::vector<PerformanceEntryType> entryTypes,
bool isBuffered) {
for (const PerformanceEntryType entryType : entryTypes) {
PerformanceEntryReporter::getInstance()->setAlwaysLogged(
entryType, isBuffered);
void NativePerformanceObserver::observe(
jsi::Runtime& rt,
jsi::Object observerObj,
NativePerformanceObserverObserveOptions options) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));

if (!observerWrapper) {
return;
}
}

PerformanceEntryReporter::PopPendingEntriesResult
NativePerformanceObserver::popPendingEntries(jsi::Runtime& /*rt*/) {
return PerformanceEntryReporter::getInstance()->popPendingEntries();
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::setOnPerformanceEntryCallback(
jsi::Runtime& /*rt*/,
std::optional<AsyncCallback<>> callback) {
if (callback) {
PerformanceEntryReporter::getInstance()->setReportingCallback(
[callback = std::move(callback)]() {
callback->callWithPriority(SchedulerPriority::IdlePriority);
});
} else {
PerformanceEntryReporter::getInstance()->setReportingCallback(nullptr);
void NativePerformanceObserver::disconnect(
jsi::Runtime& rt,
jsi::Object observerObj) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));

if (!observerWrapper) {
return;
}
}

void NativePerformanceObserver::logRawEntry(
jsi::Runtime& /*rt*/,
const PerformanceEntry entry) {
PerformanceEntryReporter::getInstance()->logEntry(entry);
auto observer = observerWrapper->observer;
observer->disconnect();
}

std::vector<std::pair<std::string, uint32_t>>
NativePerformanceObserver::getEventCounts(jsi::Runtime& /*rt*/) {
const auto& eventCounts =
PerformanceEntryReporter::getInstance()->getEventCounts();
return std::vector<std::pair<std::string, uint32_t>>(
eventCounts.begin(), eventCounts.end());
}
std::vector<PerformanceEntry> NativePerformanceObserver::takeRecords(
jsi::Runtime& rt,
jsi::Object observerObj,
bool sort) {
auto observerWrapper = std::dynamic_pointer_cast<PerformanceObserverWrapper>(
observerObj.getNativeState(rt));

void NativePerformanceObserver::setDurationThreshold(
jsi::Runtime& /*rt*/,
PerformanceEntryType entryType,
double durationThreshold) {
PerformanceEntryReporter::getInstance()->setDurationThreshold(
entryType, durationThreshold);
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 ? entryName->c_str() : std::string_view{});
PerformanceEntryReporter::getInstance()->clearEntries(entryType, entryName);
}

std::vector<PerformanceEntry> NativePerformanceObserver::getEntries(
jsi::Runtime& /*rt*/,
std::optional<PerformanceEntryType> entryType,
std::optional<std::string> entryName) {
return PerformanceEntryReporter::getInstance()->getEntries(
entryType, entryName ? entryName->c_str() : std::string_view{});
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>
Expand All @@ -127,4 +191,10 @@ NativePerformanceObserver::getSupportedPerformanceEntryTypes(
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,33 @@

#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 <>
Expand All @@ -33,47 +52,42 @@ struct Bridging<PerformanceEntryType> {
};

template <>
struct Bridging<PerformanceEntry>
: NativePerformanceObserverRawPerformanceEntryBridging<PerformanceEntry> {};
struct Bridging<NativePerformanceObserverObserveOptions>
: NativePerformanceObserverPerformanceObserverInitBridging<
NativePerformanceObserverObserveOptions> {};

template <>
struct Bridging<PerformanceEntryReporter::PopPendingEntriesResult>
: NativePerformanceObserverGetPendingEntriesResultBridging<
PerformanceEntryReporter::PopPendingEntriesResult> {};
struct Bridging<PerformanceEntry>
: NativePerformanceObserverRawPerformanceEntryBridging<PerformanceEntry> {};

#pragma mark - implementation

class NativePerformanceObserver
: public NativePerformanceObserverCxxSpec<NativePerformanceObserver> {
public:
NativePerformanceObserver(std::shared_ptr<CallInvoker> jsInvoker);

void startReporting(jsi::Runtime& rt, PerformanceEntryType entryType);

void stopReporting(jsi::Runtime& rt, PerformanceEntryType entryType);
explicit NativePerformanceObserver(std::shared_ptr<CallInvoker> jsInvoker);

void setIsBuffered(
jsi::Object createObserver(
jsi::Runtime& rt,
const std::vector<PerformanceEntryType> entryTypes,
bool isBuffered);
NativePerformanceObserverCallback callback);
double getDroppedEntriesCount(jsi::Runtime& rt, jsi::Object observerObj);

PerformanceEntryReporter::PopPendingEntriesResult popPendingEntries(
jsi::Runtime& rt);

void setOnPerformanceEntryCallback(
void observe(
jsi::Runtime& rt,
std::optional<AsyncCallback<>> callback);

void logRawEntry(jsi::Runtime& rt, const PerformanceEntry entry);
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 setDurationThreshold(
jsi::Runtime& rt,
PerformanceEntryType entryType,
DOMHighResTimeStamp durationThreshold);

void clearEntries(
jsi::Runtime& rt,
PerformanceEntryType entryType,
Expand Down
Loading

0 comments on commit 38b3b21

Please sign in to comment.