Skip to content

Commit 09ad0cc

Browse files
rshestfacebook-github-bot
authored andcommitted
Implement reporting of events from native side to WebPerformance API (#35768)
Summary: Pull Request resolved: #35768 Changelog: [Internal] This implements native side mechanics for reporting user events timing to JS (PerformanceObserver API). See the standard for more details: https://www.w3.org/TR/event-timing/ The events are only logged when there are any active subscriptions (via `PerformanceObserver.observe`), also we only log "discrete events" (i.e. no likes of mouse move), so the overhead is non-existing. There are two main metrics of interest for an event lifecycle: * Time the event is spent in the queue, i.e. the time between it's created and dispatched * Time that is spend in the event handler on the JS side (event dispatch), or processing time Both of these are measured, and the corresponding fields are populated. Reviewed By: sammy-SC Differential Revision: D42294947 fbshipit-source-id: 4fd7938c04b942400befa4057d4929fb2763cee1
1 parent 28a06d2 commit 09ad0cc

13 files changed

+224
-22
lines changed

BUCK

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,5 +1477,7 @@ rn_apple_xplat_cxx_library(
14771477
visibility = ["PUBLIC"],
14781478
deps = [
14791479
":FBReactNativeSpecJSI",
1480+
react_native_xplat_target("react/renderer/core:core"),
1481+
react_native_xplat_target("cxxreact:bridge"),
14801482
],
14811483
)

Libraries/WebPerformance/NativePerformanceObserver.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77

88
#include "NativePerformanceObserver.h"
9-
#include <glog/logging.h>
109
#include "PerformanceEntryReporter.h"
1110

1211
namespace facebook::react {
@@ -28,7 +27,13 @@ static PerformanceEntryType stringToPerformanceEntryType(
2827

2928
NativePerformanceObserver::NativePerformanceObserver(
3029
std::shared_ptr<CallInvoker> jsInvoker)
31-
: NativePerformanceObserverCxxSpec(std::move(jsInvoker)) {}
30+
: NativePerformanceObserverCxxSpec(std::move(jsInvoker)) {
31+
setEventLogger(&PerformanceEntryReporter::getInstance());
32+
}
33+
34+
NativePerformanceObserver::~NativePerformanceObserver() {
35+
setEventLogger(nullptr);
36+
}
3237

3338
void NativePerformanceObserver::startReporting(
3439
jsi::Runtime &rt,

Libraries/WebPerformance/NativePerformanceObserver.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class NativePerformanceObserver
5757
std::enable_shared_from_this<NativePerformanceObserver> {
5858
public:
5959
NativePerformanceObserver(std::shared_ptr<CallInvoker> jsInvoker);
60+
~NativePerformanceObserver();
6061

6162
void startReporting(jsi::Runtime &rt, std::string entryType);
6263

Libraries/WebPerformance/PerformanceEntryReporter.cpp

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@
66
*/
77

88
#include "PerformanceEntryReporter.h"
9-
#include <glog/logging.h>
10-
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
9+
#include <cxxreact/JSExecutor.h>
10+
#include <react/renderer/core/EventLogger.h>
1111
#include "NativePerformanceObserver.h"
1212

13+
#include <algorithm>
14+
1315
// All the unflushed entries beyond this amount will get discarded, with
1416
// the amount of discarded ones sent back to the observers' callbacks as
1517
// "droppedEntryCount" value
1618
static constexpr size_t MAX_ENTRY_BUFFER_SIZE = 1024;
1719

1820
namespace facebook::react {
21+
EventTag PerformanceEntryReporter::sCurrentEventTag_{0};
22+
1923
PerformanceEntryReporter &PerformanceEntryReporter::getInstance() {
2024
static PerformanceEntryReporter instance;
2125
return instance;
@@ -33,27 +37,22 @@ void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) {
3337
reportingType_[static_cast<int>(entryType)] = false;
3438
}
3539

36-
const std::vector<RawPerformanceEntry>
37-
&PerformanceEntryReporter::getPendingEntries() const {
38-
return entries_;
39-
}
40-
4140
GetPendingEntriesResult PerformanceEntryReporter::popPendingEntries() {
41+
std::lock_guard<std::mutex> lock(entriesMutex_);
42+
4243
GetPendingEntriesResult res = {std::move(entries_), droppedEntryCount_};
4344
entries_ = {};
4445
droppedEntryCount_ = 0;
4546
return res;
4647
}
4748

48-
void PerformanceEntryReporter::clearPendingEntries() {
49-
entries_.clear();
50-
}
51-
5249
void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) {
5350
if (!isReportingType(static_cast<PerformanceEntryType>(entry.entryType))) {
5451
return;
5552
}
5653

54+
std::lock_guard<std::mutex> lock(entriesMutex_);
55+
5756
if (entries_.size() == MAX_ENTRY_BUFFER_SIZE) {
5857
// Start dropping entries once reached maximum buffer size.
5958
// The number of dropped entries will be reported back to the corresponding
@@ -179,7 +178,7 @@ void PerformanceEntryReporter::event(
179178
{name,
180179
static_cast<int>(
181180
isFirstInput ? PerformanceEntryType::FIRST_INPUT
182-
: PerformanceEntryType::MEASURE),
181+
: PerformanceEntryType::EVENT),
183182
startTime,
184183
duration,
185184
processingStart,
@@ -207,4 +206,78 @@ void PerformanceEntryReporter::scheduleFlushBuffer() {
207206
}
208207
}
209208

209+
static bool isDiscreteEvent(const char *name) {
210+
return !std::strstr(name, "Move") && !std::strstr(name, "Layout");
211+
}
212+
213+
EventTag PerformanceEntryReporter::onEventStart(const char *name) {
214+
if (!isReportingEvents() || !isDiscreteEvent(name)) {
215+
return 0;
216+
}
217+
218+
sCurrentEventTag_++;
219+
if (sCurrentEventTag_ == 0) {
220+
// The tag wrapped around (which is highly unlikely, but still)
221+
sCurrentEventTag_ = 1;
222+
}
223+
224+
if (std::strstr(name, "top") == name) {
225+
// Skip the "top" prefix if present
226+
name += 3;
227+
}
228+
229+
auto timeStamp = JSExecutor::performanceNow();
230+
{
231+
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
232+
eventsInFlight_.emplace(
233+
std::make_pair(sCurrentEventTag_, EventEntry{name, timeStamp, 0.0}));
234+
}
235+
return sCurrentEventTag_;
236+
}
237+
238+
void PerformanceEntryReporter::onEventDispatch(EventTag tag) {
239+
if (!isReportingEvents() || tag == 0) {
240+
return;
241+
}
242+
auto timeStamp = JSExecutor::performanceNow();
243+
{
244+
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
245+
auto it = eventsInFlight_.find(tag);
246+
if (it != eventsInFlight_.end()) {
247+
it->second.dispatchTime = timeStamp;
248+
}
249+
}
250+
}
251+
252+
void PerformanceEntryReporter::onEventEnd(EventTag tag) {
253+
if (!isReportingEvents() || tag == 0) {
254+
return;
255+
}
256+
auto timeStamp = JSExecutor::performanceNow();
257+
{
258+
std::lock_guard<std::mutex> lock(eventsInFlightMutex_);
259+
auto it = eventsInFlight_.find(tag);
260+
if (it == eventsInFlight_.end()) {
261+
return;
262+
}
263+
auto &entry = it->second;
264+
auto &name = entry.name;
265+
std::transform(name.begin(), name.end(), name.begin(), ::tolower);
266+
267+
// TODO: Define the way to assign interaction IDs to the event chains
268+
// (T141358175)
269+
const uint32_t interactionId = 0;
270+
bool firstInput = isFirstInput(name);
271+
event(
272+
std::move(name),
273+
entry.startTime,
274+
timeStamp - entry.startTime,
275+
firstInput,
276+
entry.dispatchTime,
277+
timeStamp,
278+
interactionId);
279+
eventsInFlight_.erase(it);
280+
}
281+
}
282+
210283
} // namespace facebook::react

Libraries/WebPerformance/PerformanceEntryReporter.h

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
#pragma once
99

1010
#include <react/bridging/Function.h>
11+
#include <react/renderer/core/EventLogger.h>
1112
#include <array>
1213
#include <functional>
14+
#include <mutex>
1315
#include <optional>
16+
#include <unordered_map>
1417
#include <unordered_set>
1518
#include "NativePerformanceObserver.h"
1619

@@ -50,7 +53,7 @@ enum class PerformanceEntryType {
5053
_COUNT = 5,
5154
};
5255

53-
class PerformanceEntryReporter {
56+
class PerformanceEntryReporter : public EventLogger {
5457
public:
5558
PerformanceEntryReporter(PerformanceEntryReporter const &) = delete;
5659
void operator=(PerformanceEntryReporter const &) = delete;
@@ -65,9 +68,7 @@ class PerformanceEntryReporter {
6568
void startReporting(PerformanceEntryType entryType);
6669
void stopReporting(PerformanceEntryType entryType);
6770

68-
const std::vector<RawPerformanceEntry> &getPendingEntries() const;
6971
GetPendingEntriesResult popPendingEntries();
70-
void clearPendingEntries();
7172
void logEntry(const RawPerformanceEntry &entry);
7273

7374
bool isReportingType(PerformanceEntryType entryType) const {
@@ -99,22 +100,56 @@ class PerformanceEntryReporter {
99100
double processingEnd,
100101
uint32_t interactionId);
101102

103+
EventTag onEventStart(const char *name) override;
104+
void onEventDispatch(EventTag tag) override;
105+
void onEventEnd(EventTag tag) override;
106+
102107
private:
103108
PerformanceEntryReporter() {}
104109

105110
double getMarkTime(const std::string &markName) const;
106111
void clearEntries(std::function<bool(const RawPerformanceEntry &)> predicate);
107112
void scheduleFlushBuffer();
108113

114+
bool isReportingEvents() const {
115+
return isReportingType(PerformanceEntryType::EVENT) ||
116+
isReportingType(PerformanceEntryType::FIRST_INPUT);
117+
}
118+
119+
bool isFirstInput(std::string name) {
120+
if (firstInputs_.find(name) == firstInputs_.end()) {
121+
firstInputs_.insert(std::move(name));
122+
return true;
123+
}
124+
return false;
125+
}
126+
109127
std::optional<AsyncCallback<>> callback_;
110128
std::vector<RawPerformanceEntry> entries_;
129+
std::mutex entriesMutex_;
111130
std::array<bool, (size_t)PerformanceEntryType::_COUNT> reportingType_{false};
112131

113132
// Mark registry for "measure" lookup
114133
PerformanceMarkRegistryType marksRegistry_;
115134
std::array<PerformanceMark, MARKS_BUFFER_SIZE> marksBuffer_;
116135
size_t marksBufferPosition_{0};
117136
uint32_t droppedEntryCount_{0};
137+
138+
struct EventEntry {
139+
std::string name;
140+
double startTime{0.0};
141+
double dispatchTime{0.0};
142+
};
143+
144+
// Registry to store the events that are currently ongoing.
145+
// Note that we could probably use a more efficient container for that,
146+
// but since we only report discrete events, the volume is normally low,
147+
// so a hash map should be just fine.
148+
std::unordered_map<EventTag, EventEntry> eventsInFlight_;
149+
std::mutex eventsInFlightMutex_;
150+
std::unordered_set<std::string> firstInputs_;
151+
152+
static EventTag sCurrentEventTag_;
118153
};
119154

120155
} // namespace facebook::react

Libraries/WebPerformance/PerformanceEventTiming.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class PerformanceEventTiming extends PerformanceEntry {
2828
}) {
2929
super({
3030
name: init.name,
31-
entryType: init.isFirstInput === true ? 'first-input' : 'measure',
31+
entryType: init.isFirstInput === true ? 'first-input' : 'event',
3232
startTime: init.startTime ?? 0,
3333
duration: init.duration ?? 0,
3434
});

ReactCommon/react/renderer/core/BUCK

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ rn_xplat_cxx_library(
5959
react_native_xplat_target("react/utils:utils"),
6060
react_native_xplat_target("react/renderer/debug:debug"),
6161
react_native_xplat_target("react/renderer/graphics:graphics"),
62+
react_native_xplat_target("cxxreact:bridge"),
6263
],
6364
)
6465

@@ -84,6 +85,7 @@ fb_xplat_cxx_test(
8485
react_native_xplat_target("react/renderer/components/text:text"),
8586
react_native_xplat_target("react/renderer/element:element"),
8687
react_native_xplat_target("react/renderer/components/view:view"),
88+
react_native_xplat_target("cxxreact:bridge"),
8789
],
8890
)
8991

ReactCommon/react/renderer/core/EventDispatcher.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
*/
77

88
#include "EventDispatcher.h"
9-
9+
#include <cxxreact/JSExecutor.h>
1010
#include <react/renderer/core/StateUpdate.h>
11+
#include "EventLogger.h"
1112

1213
#include "BatchedEventQueue.h"
1314
#include "RawEvent.h"
@@ -39,6 +40,11 @@ void EventDispatcher::dispatchEvent(RawEvent &&rawEvent, EventPriority priority)
3940
if (eventListeners_.willDispatchEvent(rawEvent)) {
4041
return;
4142
}
43+
44+
auto eventLogger = getEventLogger();
45+
if (eventLogger != nullptr) {
46+
rawEvent.loggingTag = eventLogger->onEventStart(rawEvent.type.c_str());
47+
}
4248
getEventQueue(priority).enqueueEvent(std::move(rawEvent));
4349
}
4450

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "EventLogger.h"
9+
10+
namespace facebook::react {
11+
12+
EventLogger *eventLogger;
13+
14+
void setEventLogger(EventLogger *logger) {
15+
eventLogger = logger;
16+
}
17+
18+
EventLogger *getEventLogger() {
19+
return eventLogger;
20+
}
21+
22+
} // namespace facebook::react
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
namespace facebook::react {
11+
12+
using EventTag = unsigned int;
13+
14+
/*
15+
* Interface for logging discrete events (such as pointerenter/leave),
16+
* which can be used for implementing W3C Event Timing API standard,
17+
* see https://www.w3.org/TR/event-timing
18+
*/
19+
class EventLogger {
20+
public:
21+
virtual ~EventLogger() = default;
22+
23+
/*
24+
* Called when an event is first created, returns and unique tag for this
25+
* event, which can be used to log further event processing stages.
26+
*/
27+
virtual EventTag onEventStart(const char *name) = 0;
28+
29+
/*
30+
* Called when event starts getting dispatched (processed by the handlers, if
31+
* any)
32+
*/
33+
virtual void onEventDispatch(EventTag tag) = 0;
34+
35+
/*
36+
* Called when event finishes being dispatched
37+
*/
38+
virtual void onEventEnd(EventTag tag) = 0;
39+
};
40+
41+
void setEventLogger(EventLogger *eventLogger);
42+
EventLogger *getEventLogger();
43+
44+
} // namespace facebook::react

0 commit comments

Comments
 (0)