From 950472018d137f266358e439ffd17e374ec3686b Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Mon, 3 Apr 2023 06:13:16 -0700 Subject: [PATCH] Separate buffering per entry type in WebPerformance (#36737) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36737 ## Changelog: [Internal] - Use `BoundedConsumableBuffer` in WebPerformance This makes use of the `BoundedConsumableBuffer` container type, introduced in D44544057, for buffering/observing arbitrary performance entry types in `WebPerformance/PerformanceEntryReporter`, thus generalizing what was earlier done in an ad-hoc way for marks and measures, and also allowing to have both observable/retrievable/buffered property for arbitrary performance entry type (with the ultimate goal of adding custom entry types, related to e.g. startup timing, something that we currently have an ad hoc API for). Reviewed By: sammy-SC Differential Revision: D44574241 fbshipit-source-id: a858712ff1cf468914a80c99f6b82d060cb0b702 --- .../PerformanceEntryReporter.cpp | 244 +++++++++--------- .../WebPerformance/PerformanceEntryReporter.h | 130 ++++------ .../PerformanceEntryReporterTest.cpp | 39 +-- 3 files changed, 179 insertions(+), 234 deletions(-) diff --git a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp index 02661350f09..4bffe9e563f 100644 --- a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp +++ b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp @@ -12,71 +12,66 @@ #include -// All the unflushed entries beyond this amount will get discarded, with -// the amount of discarded ones sent back to the observers' callbacks as -// "droppedEntryCount" value -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; } +PerformanceEntryReporter::PerformanceEntryReporter() { + // For mark entry types we also want to keep the lookup by name, to make + // sure that marks can be referenced by measures + getBuffer(PerformanceEntryType::MARK).hasNameLookup = true; +} void PerformanceEntryReporter::setReportingCallback( std::optional> callback) { callback_ = callback; } void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) { - int entryTypeIdx = static_cast(entryType); - reportingType_[entryTypeIdx] = true; - durationThreshold_[entryTypeIdx] = DEFAULT_DURATION_THRESHOLD; + auto &buffer = getBuffer(entryType); + buffer.isReporting = true; + buffer.durationThreshold = DEFAULT_DURATION_THRESHOLD; } void PerformanceEntryReporter::setDurationThreshold( PerformanceEntryType entryType, double durationThreshold) { - durationThreshold_[static_cast(entryType)] = durationThreshold; + getBuffer(entryType).durationThreshold = durationThreshold; } void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) { - reportingType_[static_cast(entryType)] = false; + getBuffer(entryType).isReporting = false; } void PerformanceEntryReporter::stopReporting() { - reportingType_.fill(false); + for (auto &buffer : buffers_) { + buffer.isReporting = false; + } } GetPendingEntriesResult PerformanceEntryReporter::popPendingEntries() { std::lock_guard lock(entriesMutex_); + GetPendingEntriesResult res = { + std::vector(), droppedEntryCount_}; + for (auto &buffer : buffers_) { + buffer.entries.consume(res.entries); + } + + // Sort by starting time (or ending time, if starting times are equal) + std::sort( + res.entries.begin(), + res.entries.end(), + [](const RawPerformanceEntry &lhs, const RawPerformanceEntry &rhs) { + if (lhs.startTime != rhs.startTime) { + return lhs.startTime < rhs.startTime; + } else { + return lhs.duration < rhs.duration; + } + }); - GetPendingEntriesResult res = {std::move(entries_), droppedEntryCount_}; - entries_ = {}; droppedEntryCount_ = 0; return res; } @@ -91,24 +86,39 @@ void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) { return; } - if (entry.duration < durationThreshold_[entry.entryType]) { - // The entries duration is lower than the desired reporting threshold, skip - // return; - } - std::lock_guard lock(entriesMutex_); - if (entries_.size() == MAX_ENTRY_BUFFER_SIZE) { + auto &buffer = buffers_[entry.entryType]; + + if (entry.duration < buffer.durationThreshold) { + // The entries duration is lower than the desired reporting threshold, skip + return; + } + + if (buffer.hasNameLookup) { + auto overwriteCandidate = buffer.entries.getNextOverwriteCandidate(); + if (overwriteCandidate != nullptr) { + auto it = buffer.nameLookup.find(overwriteCandidate); + if (it != buffer.nameLookup.end() && *it == overwriteCandidate) { + buffer.nameLookup.erase(it); + } + } + } + + auto pushResult = buffer.entries.add(std::move(entry)); + if (pushResult == + BoundedConsumableBuffer::PushStatus::DROP) { // Start dropping entries once reached maximum buffer size. // The number of dropped entries will be reported back to the corresponding // PerformanceObserver callback. droppedEntryCount_ += 1; - return; } - entries_.emplace_back(entry); + if (buffer.hasNameLookup) { + buffer.nameLookup.insert(&buffer.entries.back()); + } - if (entries_.size() == 1) { + if (buffer.entries.getNumToConsume() == 1) { // If the buffer was empty, it signals that JS side just has possibly // consumed it and is ready to get more scheduleFlushBuffer(); @@ -119,95 +129,75 @@ void PerformanceEntryReporter::mark( const std::string &name, double startTime, double duration) { - // Register the mark for further possible "measure" lookup, as well as add - // 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 - // oldest one - auto it = marksRegistry_.find(&mark); - if (it != marksRegistry_.end() && *it == &mark) { - marksRegistry_.erase(it); - } - } - - mark.name = name; - mark.timeStamp = startTime; - marksRegistry_.insert(&mark); - - logEntry( - {name, - static_cast(PerformanceEntryType::MARK), - startTime, - duration, - std::nullopt, - std::nullopt, - std::nullopt}); + logEntry(RawPerformanceEntry{ + name, + static_cast(PerformanceEntryType::MARK), + startTime, + duration, + std::nullopt, + std::nullopt, + std::nullopt}); } void PerformanceEntryReporter::clearEntries( PerformanceEntryType entryType, const char *entryName) { - if (entryType == PerformanceEntryType::MARK || - entryType == PerformanceEntryType::UNDEFINED) { + if (entryType == PerformanceEntryType::UNDEFINED) { + // Clear all entry types + for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { + clearEntries(static_cast(i), entryName); + } + } else { + auto &buffer = getBuffer(entryType); if (entryName != nullptr) { - // remove a named mark from the mark/measure registry - PerformanceMark mark{{entryName, 0}}; - marksRegistry_.erase(&mark); - - clearCircularBuffer( - marksBuffer_, marksCount_, marksBufferPosition_, entryName); + if (buffer.hasNameLookup) { + RawPerformanceEntry entry{ + entryName, + static_cast(entryType), + 0.0, + 0.0, + std::nullopt, + std::nullopt, + std::nullopt}; + buffer.nameLookup.erase(&entry); + } + buffer.entries.clear([entryName](const RawPerformanceEntry &entry) { + return std::strcmp(entry.name.c_str(), entryName) == 0; + }); } else { - marksCount_ = 0; - marksRegistry_.clear(); + buffer.entries.clear(); + buffer.nameLookup.clear(); } } +} - if (entryType == PerformanceEntryType::MEASURE || - entryType == PerformanceEntryType::UNDEFINED) { - if (entryName != nullptr) { - clearCircularBuffer( - measuresBuffer_, measuresCount_, measuresBufferPosition_, entryName); +void PerformanceEntryReporter::getEntries( + PerformanceEntryType entryType, + const char *entryName, + std::vector &res) const { + if (entryType == PerformanceEntryType::UNDEFINED) { + // Collect all entry types + for (int i = 1; i < NUM_PERFORMANCE_ENTRY_TYPES; i++) { + getEntries(static_cast(i), entryName, res); + } + } else { + const auto &entries = getBuffer(entryType).entries; + if (entryName == nullptr) { + entries.getEntries(res); } else { - measuresCount_ = 0; + entries.getEntries(res, [entryName](const RawPerformanceEntry &entry) { + return std::strcmp(entry.name.c_str(), entryName) == 0; + }); } } - - int lastPos = entries_.size() - 1; - int pos = lastPos; - while (pos >= 0) { - const RawPerformanceEntry &entry = entries_[pos]; - if (entry.entryType == static_cast(entryType) && - (entryName == nullptr || entry.name == entryName)) { - entries_[pos] = entries_[lastPos]; - lastPos--; - } - pos--; - } - 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 {}; + std::vector res; + getEntries(entryType, entryName, res); + return res; } void PerformanceEntryReporter::measure( @@ -220,13 +210,6 @@ 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), @@ -239,10 +222,19 @@ void PerformanceEntryReporter::measure( double PerformanceEntryReporter::getMarkTime( const std::string &markName) const { - PerformanceMark mark{{std::move(markName), 0}}; - auto it = marksRegistry_.find(&mark); - if (it != marksRegistry_.end()) { - return (*it)->timeStamp; + RawPerformanceEntry mark{ + markName, + static_cast(PerformanceEntryType::MARK), + 0.0, + 0.0, + std::nullopt, + std::nullopt, + std::nullopt}; + + const auto &marksBuffer = getBuffer(PerformanceEntryType::MARK); + auto it = marksBuffer.nameLookup.find(&mark); + if (it != marksBuffer.nameLookup.end()) { + return (*it)->startTime; } else { return 0.0; } diff --git a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h index 7f7a014fade..0704cb05580 100644 --- a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h +++ b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h @@ -15,50 +15,45 @@ #include #include #include +#include "BoundedConsumableBuffer.h" #include "NativePerformanceObserver.h" namespace facebook::react { -struct PerformanceMark { - std::string name; - double timeStamp; - - RawPerformanceEntry toRawPerformanceEntry() const; -}; - -struct PerformanceMarkHash { - size_t operator()(const PerformanceMark *mark) const { - return std::hash()(mark->name); +struct PerformanceEntryHash { + size_t operator()(const RawPerformanceEntry *entry) const { + return std::hash()(entry->name); } }; -struct PerformanceMarkEqual { - bool operator()(const PerformanceMark *lhs, const PerformanceMark *rhs) - const { +struct PerformanceEntryEqual { + bool operator()( + const RawPerformanceEntry *lhs, + const RawPerformanceEntry *rhs) const { return lhs->name == rhs->name; } }; -struct PerformanceMeasure { - std::string name; - double timeStamp; - double duration; - - RawPerformanceEntry toRawPerformanceEntry() const; -}; - -using PerformanceMarkRegistryType = std:: - unordered_set; - -// 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; +using PerformanceEntryRegistryType = std::unordered_set< + const RawPerformanceEntry *, + PerformanceEntryHash, + PerformanceEntryEqual>; +// Default duration threshold for reporting performance entries (0 means "report +// all") constexpr double DEFAULT_DURATION_THRESHOLD = 0.0; +// Default buffer size limit, per entry type +constexpr size_t DEFAULT_MAX_BUFFER_SIZE = 1024; + +struct PerformanceEntryBuffer { + BoundedConsumableBuffer entries{DEFAULT_MAX_BUFFER_SIZE}; + bool isReporting{false}; + double durationThreshold{DEFAULT_DURATION_THRESHOLD}; + bool hasNameLookup{false}; + PerformanceEntryRegistryType nameLookup; +}; + enum class PerformanceEntryType { UNDEFINED = 0, MARK = 1, @@ -67,6 +62,9 @@ enum class PerformanceEntryType { _COUNT = 4, }; +constexpr size_t NUM_PERFORMANCE_ENTRY_TYPES = + (size_t)PerformanceEntryType::_COUNT; + class PerformanceEntryReporter : public EventLogger { public: PerformanceEntryReporter(PerformanceEntryReporter const &) = delete; @@ -87,10 +85,20 @@ class PerformanceEntryReporter : public EventLogger { double durationThreshold); GetPendingEntriesResult popPendingEntries(); + void logEntry(const RawPerformanceEntry &entry); + PerformanceEntryBuffer &getBuffer(PerformanceEntryType entryType) { + return buffers_[static_cast(entryType)]; + } + + const PerformanceEntryBuffer &getBuffer( + PerformanceEntryType entryType) const { + return buffers_[static_cast(entryType)]; + } + bool isReporting(PerformanceEntryType entryType) const { - return reportingType_[static_cast(entryType)]; + return getBuffer(entryType).isReporting; } bool isReportingEvents() const { @@ -137,23 +145,13 @@ class PerformanceEntryReporter : public EventLogger { private: std::optional> callback_; - std::vector entries_; + std::mutex entriesMutex_; - std::array reportingType_{false}; + std::array buffers_; std::unordered_map eventCounts_; - std::array durationThreshold_{ - DEFAULT_DURATION_THRESHOLD}; // Mark registry for "measure" lookup - PerformanceMarkRegistryType marksRegistry_; - std::array marksBuffer_; - size_t marksBufferPosition_{0}; - size_t marksCount_{0}; - - std::array measuresBuffer_; - size_t measuresBufferPosition_{0}; - size_t measuresCount_{0}; - + PerformanceEntryRegistryType marksRegistry_; uint32_t droppedEntryCount_{0}; struct EventEntry { @@ -171,49 +169,15 @@ class PerformanceEntryReporter : public EventLogger { static EventTag sCurrentEventTag_; - PerformanceEntryReporter() {} + PerformanceEntryReporter(); double getMarkTime(const std::string &markName) const; void scheduleFlushBuffer(); - 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 - entryCount + buffer.size()) % buffer.size(); - 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 - entryCount + buffer.size()) % buffer.size(); - 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 = newEntryCount; - entryCount = newEntryCount; - } + void getEntries( + PerformanceEntryType entryType, + const char *entryName, + std::vector &res) const; }; } // namespace facebook::react diff --git a/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp b/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp index badf5396ff5..40a3affe0f2 100644 --- a/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp +++ b/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp @@ -152,17 +152,6 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { ASSERT_EQ(0, res.droppedEntriesCount); - ASSERT_STREQ("mark0", entries[0].name.c_str()); - ASSERT_STREQ("mark1", entries[1].name.c_str()); - ASSERT_STREQ("mark2", entries[2].name.c_str()); - ASSERT_STREQ("measure0", entries[3].name.c_str()); - ASSERT_STREQ("measure1", entries[4].name.c_str()); - ASSERT_STREQ("measure2", entries[5].name.c_str()); - ASSERT_STREQ("measure3", entries[6].name.c_str()); - ASSERT_STREQ("measure4", entries[7].name.c_str()); - - ASSERT_EQ(8, entries.size()); - const std::vector expected = { {"mark0", static_cast(PerformanceEntryType::MARK), @@ -171,20 +160,6 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { std::nullopt, std::nullopt, std::nullopt}, - {"mark1", - static_cast(PerformanceEntryType::MARK), - 1.0, - 3.0, - std::nullopt, - std::nullopt, - std::nullopt}, - {"mark2", - static_cast(PerformanceEntryType::MARK), - 2.0, - 4.0, - std::nullopt, - std::nullopt, - std::nullopt}, {"measure0", static_cast(PerformanceEntryType::MEASURE), 0.0, @@ -206,6 +181,13 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { std::nullopt, std::nullopt, std::nullopt}, + {"mark1", + static_cast(PerformanceEntryType::MARK), + 1.0, + 3.0, + std::nullopt, + std::nullopt, + std::nullopt}, {"measure3", static_cast(PerformanceEntryType::MEASURE), 1.0, @@ -219,6 +201,13 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestReportMeasures) { 0.5, std::nullopt, std::nullopt, + std::nullopt}, + {"mark2", + static_cast(PerformanceEntryType::MARK), + 2.0, + 4.0, + std::nullopt, + std::nullopt, std::nullopt}}; ASSERT_EQ(expected, entries);