diff --git a/Libraries/WebPerformance/NativePerformanceObserver.cpp b/Libraries/WebPerformance/NativePerformanceObserver.cpp index 69b1c852879..b37255119e7 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.cpp +++ b/Libraries/WebPerformance/NativePerformanceObserver.cpp @@ -87,4 +87,14 @@ void NativePerformanceObserver::clearEntries( entryName ? entryName->c_str() : nullptr); } +std::vector NativePerformanceObserver::getEntries( + jsi::Runtime &rt, + std::optional entryType, + std::optional entryName) { + return PerformanceEntryReporter::getInstance().getEntries( + entryType ? static_cast(*entryType) + : PerformanceEntryType::UNDEFINED, + entryName ? entryName->c_str() : nullptr); +} + } // namespace facebook::react diff --git a/Libraries/WebPerformance/NativePerformanceObserver.h b/Libraries/WebPerformance/NativePerformanceObserver.h index 0c3d73f8b12..7f1e4d9161c 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.h +++ b/Libraries/WebPerformance/NativePerformanceObserver.h @@ -84,6 +84,11 @@ class NativePerformanceObserver int32_t entryType, std::optional entryName); + std::vector getEntries( + jsi::Runtime &rt, + std::optional entryType, + std::optional entryName); + private: }; diff --git a/Libraries/WebPerformance/NativePerformanceObserver.js b/Libraries/WebPerformance/NativePerformanceObserver.js index 5bca758e39a..f7a9031d06c 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/NativePerformanceObserver.js @@ -45,6 +45,10 @@ export interface Spec extends TurboModule { entryType: RawPerformanceEntryType, entryName?: string, ) => void; + +getEntries: ( + entryType?: RawPerformanceEntryType, + entryName?: string, + ) => $ReadOnlyArray; } export default (TurboModuleRegistry.get( diff --git a/Libraries/WebPerformance/Performance.js b/Libraries/WebPerformance/Performance.js index a4a0b19dcc9..2f14f9f8a1a 100644 --- a/Libraries/WebPerformance/Performance.js +++ b/Libraries/WebPerformance/Performance.js @@ -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'; @@ -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; @@ -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); + } } diff --git a/Libraries/WebPerformance/PerformanceEntryReporter.cpp b/Libraries/WebPerformance/PerformanceEntryReporter.cpp index 4864200abbb..84951be44da 100644 --- a/Libraries/WebPerformance/PerformanceEntryReporter.cpp +++ b/Libraries/WebPerformance/PerformanceEntryReporter.cpp @@ -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(PerformanceEntryType::MARK), + timeStamp, + 0.0, + std::nullopt, + std::nullopt, + std::nullopt}; +} + +RawPerformanceEntry PerformanceMeasure::toRawPerformanceEntry() const { + return { + name, + static_cast(PerformanceEntryType::MEASURE), + timeStamp, + duration, + std::nullopt, + std::nullopt, + std::nullopt}; +} + PerformanceEntryReporter &PerformanceEntryReporter::getInstance() { static PerformanceEntryReporter instance; return instance; @@ -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 @@ -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; @@ -144,6 +181,26 @@ void PerformanceEntryReporter::clearEntries( entries_.resize(lastPos + 1); } +std::vector 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, @@ -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(PerformanceEntryType::MEASURE), diff --git a/Libraries/WebPerformance/PerformanceEntryReporter.h b/Libraries/WebPerformance/PerformanceEntryReporter.h index 4202b7a9ba7..00b54c763fc 100644 --- a/Libraries/WebPerformance/PerformanceEntryReporter.h +++ b/Libraries/WebPerformance/PerformanceEntryReporter.h @@ -22,6 +22,8 @@ namespace facebook::react { struct PerformanceMark { std::string name; double timeStamp; + + RawPerformanceEntry toRawPerformanceEntry() const; }; struct PerformanceMarkHash { @@ -37,6 +39,14 @@ struct PerformanceMarkEqual { } }; +struct PerformanceMeasure { + std::string name; + double timeStamp; + double duration; + + RawPerformanceEntry toRawPerformanceEntry() const; +}; + using PerformanceMarkRegistryType = std:: unordered_set; @@ -44,6 +54,9 @@ using PerformanceMarkRegistryType = std:: // 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 { @@ -97,6 +110,10 @@ class PerformanceEntryReporter : public EventLogger { PerformanceEntryType entryType, const char *entryName = nullptr); + std::vector getEntries( + PerformanceEntryType entryType, + const char *entryName = nullptr) const; + void event( std::string name, double startTime, @@ -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> callback_; std::vector entries_; std::mutex entriesMutex_; @@ -135,6 +143,12 @@ class PerformanceEntryReporter : public EventLogger { PerformanceMarkRegistryType marksRegistry_; std::array marksBuffer_; size_t marksBufferPosition_{0}; + size_t marksCount_{0}; + + std::array measuresBuffer_; + size_t measuresBufferPosition_{0}; + size_t measuresCount_{0}; + uint32_t droppedEntryCount_{0}; struct EventEntry { @@ -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 + std::vector getCircularBufferContents( + const std::array &buffer, + size_t entryCount, + size_t bufferPosition, + const char *entryName = nullptr) const { + std::vector 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 + void clearCircularBuffer( + std::array &buffer, + size_t &entryCount, + size_t &bufferPosition, + const char *entryName) const { + std::array 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 diff --git a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js index 3c2eb47f700..1b9c3142f38 100644 --- a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js @@ -85,6 +85,17 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { (entryName == null || e.name === entryName), ); }, + + getEntries: ( + entryType?: RawPerformanceEntryType, + entryName?: string, + ): $ReadOnlyArray => { + return entries.filter( + e => + (entryType == null || e.entryType === entryType) && + (entryName == null || e.name === entryName), + ); + }, }; export default NativePerformanceObserverMock;