Skip to content

Commit

Permalink
Implement Performance.getEntries* W3C extension for Performance Timel…
Browse files Browse the repository at this point in the history
…ine API (#36314)

Summary:
Pull Request resolved: #36314

## Changelog:

[Internal] - Added support for W3C Performance API extension (Performance,getEntries*)

See [here for the details](https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface).

This only supports `mark` and `measure` performance entry types (not `events`, as those are not supported by browsers either, they recommend using the `PerformanceObserver` API instead).

From the implementation perspective, we already maintained persistent buffer for `mark` entry types, which was required in order to look up measures.

The buffer is circular, limited to 1K entries by default.

I basically mimic the same behavior for `measure` entry types as well, since it's the simplest way of doing it at this point,

If we happen to want adding some other entry types that would be available via `Performance.getEntries*` API in the future, we may want to iterate on the internal data structures representation inside `PerformanceEntryReporter`, but for now I believe this approach should work just fine, and whatever changes we may want to do will be purely internal implementation detail.

Reviewed By: rubennorte

Differential Revision: D43625488

fbshipit-source-id: dd315b3f8488e910749a8e2a4158246e94d76f99
  • Loading branch information
rshest authored and facebook-github-bot committed Mar 1, 2023
1 parent 3997fc1 commit bc56f66
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 14 deletions.
10 changes: 10 additions & 0 deletions Libraries/WebPerformance/NativePerformanceObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,14 @@ void NativePerformanceObserver::clearEntries(
entryName ? entryName->c_str() : nullptr);
}

std::vector<RawPerformanceEntry> NativePerformanceObserver::getEntries(
jsi::Runtime &rt,
std::optional<int32_t> entryType,
std::optional<std::string> entryName) {
return PerformanceEntryReporter::getInstance().getEntries(
entryType ? static_cast<PerformanceEntryType>(*entryType)
: PerformanceEntryType::UNDEFINED,
entryName ? entryName->c_str() : nullptr);
}

} // namespace facebook::react
5 changes: 5 additions & 0 deletions Libraries/WebPerformance/NativePerformanceObserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ class NativePerformanceObserver
int32_t entryType,
std::optional<std::string> entryName);

std::vector<RawPerformanceEntry> getEntries(
jsi::Runtime &rt,
std::optional<int32_t> entryType,
std::optional<std::string> entryName);

private:
};

Expand Down
4 changes: 4 additions & 0 deletions Libraries/WebPerformance/NativePerformanceObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export interface Spec extends TurboModule {
entryType: RawPerformanceEntryType,
entryName?: string,
) => void;
+getEntries: (
entryType?: RawPerformanceEntryType,
entryName?: string,
) => $ReadOnlyArray<RawPerformanceEntry>;
}

export default (TurboModuleRegistry.get<Spec>(
Expand Down
62 changes: 61 additions & 1 deletion Libraries/WebPerformance/Performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

// flowlint unsafe-getters-setters:off

import type {HighResTimeStamp} from './PerformanceEntry';
import type {HighResTimeStamp, PerformanceEntryType} from './PerformanceEntry';
import type {PerformanceEntryList} from './PerformanceObserver';

import warnOnce from '../Utilities/warnOnce';
import EventCounts from './EventCounts';
Expand All @@ -19,6 +20,10 @@ import NativePerformance from './NativePerformance';
import NativePerformanceObserver from './NativePerformanceObserver';
import {PerformanceEntry} from './PerformanceEntry';
import {warnNoNativePerformanceObserver} from './PerformanceObserver';
import {
performanceEntryTypeToRaw,
rawToPerformanceEntry,
} from './RawPerformanceEntry';
import {RawPerformanceEntryTypeValues} from './RawPerformanceEntry';

type DetailType = mixed;
Expand Down Expand Up @@ -237,4 +242,59 @@ export default class Performance {
now(): HighResTimeStamp {
return getCurrentTimeStamp();
}

/**
* An extension that allows to get back to JS all currently logged marks/measures
* (in our case, be it from JS or native), see
* https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface
*/
getEntries(): PerformanceEntryList {
if (!NativePerformanceObserver?.clearEntries) {
warnNoNativePerformanceObserver();
return [];
}
return NativePerformanceObserver.getEntries().map(rawToPerformanceEntry);
}

getEntriesByType(entryType: PerformanceEntryType): PerformanceEntryList {
if (entryType !== 'mark' && entryType !== 'measure') {
console.log(
`Performance.getEntriesByType: Only valid for 'mark' and 'measure' entry types, got ${entryType}`,
);
return [];
}

if (!NativePerformanceObserver?.clearEntries) {
warnNoNativePerformanceObserver();
return [];
}
return NativePerformanceObserver.getEntries(
performanceEntryTypeToRaw(entryType),
).map(rawToPerformanceEntry);
}

getEntriesByName(
entryName: string,
entryType?: PerformanceEntryType,
): PerformanceEntryList {
if (
entryType !== undefined &&
entryType !== 'mark' &&
entryType !== 'measure'
) {
console.log(
`Performance.getEntriesByName: Only valid for 'mark' and 'measure' entry types, got ${entryType}`,
);
return [];
}

if (!NativePerformanceObserver?.clearEntries) {
warnNoNativePerformanceObserver();
return [];
}
return NativePerformanceObserver.getEntries(
entryType != null ? performanceEntryTypeToRaw(entryType) : undefined,
entryName,
).map(rawToPerformanceEntry);
}
}
72 changes: 68 additions & 4 deletions Libraries/WebPerformance/PerformanceEntryReporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ static constexpr size_t MAX_ENTRY_BUFFER_SIZE = 1024;
namespace facebook::react {
EventTag PerformanceEntryReporter::sCurrentEventTag_{0};

RawPerformanceEntry PerformanceMark::toRawPerformanceEntry() const {
return {
name,
static_cast<int>(PerformanceEntryType::MARK),
timeStamp,
0.0,
std::nullopt,
std::nullopt,
std::nullopt};
}

RawPerformanceEntry PerformanceMeasure::toRawPerformanceEntry() const {
return {
name,
static_cast<int>(PerformanceEntryType::MEASURE),
timeStamp,
duration,
std::nullopt,
std::nullopt,
std::nullopt};
}

PerformanceEntryReporter &PerformanceEntryReporter::getInstance() {
static PerformanceEntryReporter instance;
return instance;
Expand Down Expand Up @@ -97,6 +119,7 @@ void PerformanceEntryReporter::mark(
// it to a circular buffer:
PerformanceMark &mark = marksBuffer_[marksBufferPosition_];
marksBufferPosition_ = (marksBufferPosition_ + 1) % marksBuffer_.size();
marksCount_ = std::min(marksBuffer_.size(), marksCount_ + 1);

if (!mark.name.empty()) {
// Drop off the oldest mark out of the queue, but only if that's indeed the
Expand Down Expand Up @@ -124,10 +147,24 @@ void PerformanceEntryReporter::mark(
void PerformanceEntryReporter::clearEntries(
PerformanceEntryType entryType,
const char *entryName) {
if (entryName != nullptr && entryType == PerformanceEntryType::MARK) {
// remove a named mark from the mark/measure registry
PerformanceMark mark{{entryName, 0}};
marksRegistry_.erase(&mark);
if (entryType == PerformanceEntryType::MARK) {
if (entryName != nullptr) {
// remove a named mark from the mark/measure registry
PerformanceMark mark{{entryName, 0}};
marksRegistry_.erase(&mark);

clearCircularBuffer(
marksBuffer_, marksCount_, marksBufferPosition_, entryName);
} else {
marksCount_ = 0;
}
} else if (entryType == PerformanceEntryType::MEASURE) {
if (entryName != nullptr) {
clearCircularBuffer(
measuresBuffer_, measuresCount_, measuresBufferPosition_, entryName);
} else {
measuresCount_ = 0;
}
}

int lastPos = entries_.size() - 1;
Expand All @@ -144,6 +181,26 @@ void PerformanceEntryReporter::clearEntries(
entries_.resize(lastPos + 1);
}

std::vector<RawPerformanceEntry> PerformanceEntryReporter::getEntries(
PerformanceEntryType entryType,
const char *entryName) const {
if (entryType == PerformanceEntryType::MARK) {
return getCircularBufferContents(
marksBuffer_, marksCount_, marksBufferPosition_, entryName);
} else if (entryType == PerformanceEntryType::MEASURE) {
return getCircularBufferContents(
measuresBuffer_, measuresCount_, measuresBufferPosition_, entryName);
} else if (entryType == PerformanceEntryType::UNDEFINED) {
auto marks = getCircularBufferContents(
marksBuffer_, marksCount_, marksBufferPosition_, entryName);
auto measures = getCircularBufferContents(
measuresBuffer_, measuresCount_, measuresBufferPosition_, entryName);
marks.insert(marks.end(), measures.begin(), measures.end());
return marks;
}
return {};
}

void PerformanceEntryReporter::measure(
const std::string &name,
double startTime,
Expand All @@ -154,6 +211,13 @@ void PerformanceEntryReporter::measure(
double startTimeVal = startMark ? getMarkTime(*startMark) : startTime;
double endTimeVal = endMark ? getMarkTime(*endMark) : endTime;
double durationVal = duration ? *duration : endTimeVal - startTimeVal;

measuresBuffer_[measuresBufferPosition_] =
PerformanceMeasure{name, startTime, endTime};
measuresBufferPosition_ =
(measuresBufferPosition_ + 1) % measuresBuffer_.size();
measuresCount_ = std::min(measuresBuffer_.size(), measuresCount_ + 1);

logEntry(
{name,
static_cast<int>(PerformanceEntryType::MEASURE),
Expand Down
80 changes: 71 additions & 9 deletions Libraries/WebPerformance/PerformanceEntryReporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace facebook::react {
struct PerformanceMark {
std::string name;
double timeStamp;

RawPerformanceEntry toRawPerformanceEntry() const;
};

struct PerformanceMarkHash {
Expand All @@ -37,13 +39,24 @@ struct PerformanceMarkEqual {
}
};

struct PerformanceMeasure {
std::string name;
double timeStamp;
double duration;

RawPerformanceEntry toRawPerformanceEntry() const;
};

using PerformanceMarkRegistryType = std::
unordered_set<PerformanceMark *, PerformanceMarkHash, PerformanceMarkEqual>;

// Only the MARKS_BUFFER_SIZE amount of the latest marks will be kept in
// memory for the sake of the "Performance.measure" mark name lookup
constexpr size_t MARKS_BUFFER_SIZE = 1024;

// Limit buffer size for the measures kept in memory (only keep the latest ones)
constexpr size_t MEASURES_BUFFER_SIZE = 1024;

constexpr double DEFAULT_DURATION_THRESHOLD = 0.0;

enum class PerformanceEntryType {
Expand Down Expand Up @@ -97,6 +110,10 @@ class PerformanceEntryReporter : public EventLogger {
PerformanceEntryType entryType,
const char *entryName = nullptr);

std::vector<RawPerformanceEntry> getEntries(
PerformanceEntryType entryType,
const char *entryName = nullptr) const;

void event(
std::string name,
double startTime,
Expand All @@ -114,15 +131,6 @@ class PerformanceEntryReporter : public EventLogger {
}

private:
PerformanceEntryReporter() {}

double getMarkTime(const std::string &markName) const;
void scheduleFlushBuffer();

bool isReportingEvents() const {
return isReportingType(PerformanceEntryType::EVENT);
}

std::optional<AsyncCallback<>> callback_;
std::vector<RawPerformanceEntry> entries_;
std::mutex entriesMutex_;
Expand All @@ -135,6 +143,12 @@ class PerformanceEntryReporter : public EventLogger {
PerformanceMarkRegistryType marksRegistry_;
std::array<PerformanceMark, MARKS_BUFFER_SIZE> marksBuffer_;
size_t marksBufferPosition_{0};
size_t marksCount_{0};

std::array<PerformanceMeasure, MEASURES_BUFFER_SIZE> measuresBuffer_;
size_t measuresBufferPosition_{0};
size_t measuresCount_{0};

uint32_t droppedEntryCount_{0};

struct EventEntry {
Expand All @@ -151,6 +165,54 @@ class PerformanceEntryReporter : public EventLogger {
std::mutex eventsInFlightMutex_;

static EventTag sCurrentEventTag_;

PerformanceEntryReporter() {}

double getMarkTime(const std::string &markName) const;
void scheduleFlushBuffer();

bool isReportingEvents() const {
return isReportingType(PerformanceEntryType::EVENT);
}

template <class T, size_t N>
std::vector<RawPerformanceEntry> getCircularBufferContents(
const std::array<T, N> &buffer,
size_t entryCount,
size_t bufferPosition,
const char *entryName = nullptr) const {
std::vector<RawPerformanceEntry> res;
size_t pos = bufferPosition;
for (size_t i = 0; i < entryCount; i++) {
if (entryName == nullptr || buffer[pos].name == entryName) {
res.push_back(buffer[pos].toRawPerformanceEntry());
}
pos = (pos + 1) % buffer.size();
}
return res;
}

template <class T, size_t N>
void clearCircularBuffer(
std::array<T, N> &buffer,
size_t &entryCount,
size_t &bufferPosition,
const char *entryName) const {
std::array<T, N> newBuffer;
size_t newEntryCount = 0;

size_t pos = bufferPosition;
for (size_t i = 0; i < entryCount; i++) {
if (buffer[pos].name != entryName) {
newBuffer[newEntryCount++] = buffer[pos];
}
pos = (pos + 1) % buffer.size();
}

buffer = newBuffer;
bufferPosition = 0;
entryCount = newEntryCount;
}
};

} // namespace facebook::react
11 changes: 11 additions & 0 deletions Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ const NativePerformanceObserverMock: NativePerformanceObserver = {
(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),
);
},
};

export default NativePerformanceObserverMock;

0 comments on commit bc56f66

Please sign in to comment.