diff --git a/include/mbgl/gfx/rendering_stats.hpp b/include/mbgl/gfx/rendering_stats.hpp index 8ee33894e..29e4ed314 100644 --- a/include/mbgl/gfx/rendering_stats.hpp +++ b/include/mbgl/gfx/rendering_stats.hpp @@ -15,12 +15,11 @@ class SymbolLayer; namespace gfx { struct RenderingStats { - RenderingStats() = default; bool isZero() const; - /// Frame CPU encoding time (milliseconds) + /// Frame CPU encoding time (seconds) double encodingTime = 0.0; - /// Frame CPU rendering time (milliseconds) + /// Frame CPU rendering time (seconds) double renderingTime = 0.0; /// Number of frames rendered diff --git a/include/mbgl/util/action_journal_options.hpp b/include/mbgl/util/action_journal_options.hpp index c666e9de8..e404ab174 100644 --- a/include/mbgl/util/action_journal_options.hpp +++ b/include/mbgl/util/action_journal_options.hpp @@ -87,6 +87,23 @@ public: */ uint32_t logFileCount() const { return logFileCount_; } + /** + * @brief Set the number of seconds to wait between rendering stats reports. + * + * @param interval time interval in seconds. + * @return ActionJournalOptions for chaining options together. + */ + ActionJournalOptions& withRenderingStatsReportInterval(const uint32_t interval) { + renderingStatsReportInterval_ = interval; + return *this; + } + + /** + * @brief Gets the previously set (or default) time interval. + * @return Returns report time interval in seconds + */ + uint32_t renderingStatsReportInterval() const { return renderingStatsReportInterval_; } + protected: bool enable_ = false; // path of the log @@ -95,6 +112,8 @@ protected: uint32_t logFileSize_ = 1024 * 1024; // number of log files (each of `logFileSize` size) uint32_t logFileCount_ = 5; + // the wait time (seconds) between rendering reports + uint32_t renderingStatsReportInterval_ = 60; }; } // namespace util diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp index 42ce9d2b1..45182fcfe 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_options.cpp @@ -19,12 +19,14 @@ util::ActionJournalOptions NativeMapOptions::getActionJournalOptions(jni::JNIEnv auto pathField = javaClass.GetField(env, "actionJournalPath"); auto logFileSizeField = javaClass.GetField(env, "actionJournalLogFileSize"); auto logFileCountField = javaClass.GetField(env, "actionJournalLogFileCount"); + auto renderingReportIntervalField = javaClass.GetField(env, "actionJournalRenderingReportInterval"); return util::ActionJournalOptions() .enable(obj.Get(env, enabledField)) .withPath(jni::Make(env, obj.Get(env, pathField))) .withLogFileSize(obj.Get(env, logFileSizeField)) - .withLogFileCount(obj.Get(env, logFileCountField)); + .withLogFileCount(obj.Get(env, logFileCountField)) + .withRenderingStatsReportInterval(obj.Get(env, renderingReportIntervalField)); } float NativeMapOptions::pixelRatio(jni::JNIEnv &env, const jni::Object &obj) { diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java index 231f2e915..c54bd5c50 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapLibreMapOptions.java @@ -98,6 +98,7 @@ public class MapLibreMapOptions implements Parcelable { private String actionJournalPath = ""; private long actionJournalLogFileSize = 1024 * 1024; private long actionJournalLogFileCount = 5; + private int actionJournalRenderingReportInterval = 60; /** * Creates a new MapLibreMapOptions object. @@ -161,6 +162,7 @@ public class MapLibreMapOptions implements Parcelable { actionJournalPath = in.readString(); actionJournalLogFileSize = in.readLong(); actionJournalLogFileCount = in.readLong(); + actionJournalRenderingReportInterval = in.readInt(); } /** @@ -327,6 +329,9 @@ public class MapLibreMapOptions implements Parcelable { maplibreMapOptions.actionJournalLogFileCount( typedArray.getInteger(R.styleable.maplibre_MapView_maplibre_actionJournalLogFileCount, 5) ); + maplibreMapOptions.actionJournalRenderingReportInterval( + typedArray.getInteger(R.styleable.maplibre_MapView_maplibre_actionJournalRenderingReportInterval, 60) + ); } finally { typedArray.recycle(); } @@ -811,6 +816,18 @@ public class MapLibreMapOptions implements Parcelable { return this; } + /** + * Set the number of seconds to wait between rendering stats reports. + * + * @param actionJournalRenderingReportInterval time interval in seconds + * @return This + */ + @NonNull + public MapLibreMapOptions actionJournalRenderingReportInterval(int actionJournalRenderingReportInterval) { + this.actionJournalRenderingReportInterval = actionJournalRenderingReportInterval; + return this; + } + /** * Enable local ideograph font family, defaults to true. * @@ -940,6 +957,15 @@ public class MapLibreMapOptions implements Parcelable { return actionJournalLogFileCount; } + /** + * Get the current configured action journal rendering stats report time interval. + * + * @return time interval in seconds + */ + public int getActionJournalRenderingReportInterval() { + return actionJournalRenderingReportInterval; + } + /** * Set the flag to render the map surface on top of another surface. * @@ -1329,6 +1355,7 @@ public class MapLibreMapOptions implements Parcelable { dest.writeString(actionJournalPath); dest.writeLong(actionJournalLogFileSize); dest.writeLong(actionJournalLogFileCount); + dest.writeInt(actionJournalRenderingReportInterval); } @Override @@ -1465,6 +1492,10 @@ public class MapLibreMapOptions implements Parcelable { return false; } + if (actionJournalRenderingReportInterval != options.actionJournalRenderingReportInterval) { + return false; + } + return false; } @@ -1516,6 +1547,7 @@ public class MapLibreMapOptions implements Parcelable { result = 31 * result + (actionJournalPath != null ? actionJournalPath.hashCode() : 0); result = 31 * result + (int) actionJournalLogFileSize; result = 31 * result + (int) actionJournalLogFileCount; + result = 31 * result + (int) actionJournalRenderingReportInterval; return result; } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java index 788a9d2d7..30c45a94f 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapOptions.java @@ -9,6 +9,7 @@ public class NativeMapOptions { private final String actionJournalPath; private final long actionJournalLogFileSize; private final long actionJournalLogFileCount; + private final int actionJournalRenderingReportInterval; public NativeMapOptions(MapLibreMapOptions options) { pixelRatio = options.getPixelRatio(); @@ -18,6 +19,7 @@ public class NativeMapOptions { actionJournalPath = options.getActionJournalPath(); actionJournalLogFileSize = options.getActionJournalLogFileSize(); actionJournalLogFileCount = options.getActionJournalLogFileCount(); + actionJournalRenderingReportInterval = options.getActionJournalRenderingReportInterval(); } public NativeMapOptions(float pixelRatio, boolean crossSourceCollisions) { @@ -28,6 +30,7 @@ public class NativeMapOptions { actionJournalPath = ""; actionJournalLogFileSize = 0; actionJournalLogFileCount = 0; + actionJournalRenderingReportInterval = 0; } public float pixelRatio() { @@ -53,4 +56,8 @@ public class NativeMapOptions { public long actionJournalLogFileCount() { return actionJournalLogFileCount; } + + public int actionJournalRenderingReportInterval() { + return actionJournalRenderingReportInterval; + } } diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RenderingStats.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RenderingStats.java index 4e250db53..a831913fa 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RenderingStats.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RenderingStats.java @@ -1,9 +1,9 @@ package org.maplibre.android.maps; public class RenderingStats { - /// Frame CPU encoding time (milliseconds) + /// Frame CPU encoding time (seconds) public double encodingTime = 0.0; - /// Frame CPU rendering time (milliseconds) + /// Frame CPU rendering time (seconds) public double renderingTime = 0.0; /// Number of frames rendered diff --git a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml index 658184e4d..eb367bd91 100644 --- a/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml +++ b/platform/android/MapLibreAndroid/src/main/res/values/attrs.xml @@ -16,6 +16,7 @@ + diff --git a/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml b/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml index 5a0cdfd4e..e3fc32ef4 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml +++ b/platform/android/MapLibreAndroidTestApp/src/main/res/layout/activity_map_events.xml @@ -14,7 +14,8 @@ android:layout_height="match_parent" app:maplibre_actionJournalEnabled="true" app:maplibre_actionJournalLogFileSize="1024" - app:maplibre_actionJournalLogFileCount="2"/> + app:maplibre_actionJournalLogFileCount="2" + app:maplibre_actionJournalRenderingReportInterval="10"/> logFileCount(); } +- (void)setRenderingStatsReportInterval:(NSInteger)value { + _actionJournalOptionsInternal->withRenderingStatsReportInterval(static_cast(value)); +} + +- (NSInteger)renderingStatsReportInterval { + return _actionJournalOptionsInternal->renderingStatsReportInterval(); +} + @end diff --git a/platform/darwin/src/MLNRenderingStats.h b/platform/darwin/src/MLNRenderingStats.h index 0c5e208fd..82832e971 100644 --- a/platform/darwin/src/MLNRenderingStats.h +++ b/platform/darwin/src/MLNRenderingStats.h @@ -6,9 +6,9 @@ NS_ASSUME_NONNULL_BEGIN MLN_EXPORT @interface MLNRenderingStats : NSObject -/// Frame CPU encoding time (milliseconds) +/// Frame CPU encoding time (seconds) @property (readonly) double encodingTime; -/// Frame CPU rendering time (milliseconds) +/// Frame CPU rendering time (seconds) @property (readonly) double renderingTime; /// Number of frames rendered diff --git a/platform/ios/app-swift/Sources/ObserverExample.swift b/platform/ios/app-swift/Sources/ObserverExample.swift index 7fcea92ae..0d66f31d5 100644 --- a/platform/ios/app-swift/Sources/ObserverExample.swift +++ b/platform/ios/app-swift/Sources/ObserverExample.swift @@ -12,6 +12,7 @@ class ObserverExampleView: UIViewController, MLNMapViewDelegate { // #-example-code(actionJournalOptions) let options = MLNMapOptions() options.actionJournalOptions.enabled = true + options.actionJournalOptions.renderingStatsReportInterval = 10 options.styleURL = AMERICANA_STYLE mapView = MLNMapView(frame: view.bounds, options: options) // #-end-example-code diff --git a/src/mbgl/util/action_journal_impl.cpp b/src/mbgl/util/action_journal_impl.cpp index a0adebc17..207db0b17 100644 --- a/src/mbgl/util/action_journal_impl.cpp +++ b/src/mbgl/util/action_journal_impl.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -137,11 +138,14 @@ public: ActionJournal::Impl::Impl(const Map& map_, const ActionJournalOptions& options_) : map(map_), options(options_), - scheduler(Scheduler::GetSequenced()) { + scheduler(Scheduler::GetSequenced()), + renderingInfoReportTime(options.renderingStatsReportInterval()) { assert(!options.path().empty()); assert(options.logFileSize() > 0); assert(options.logFileCount() > 1); + previousFrameTime = util::MonotonicTimer::now().count(); + options.withPath((mbgl::filesystem::canonical(options.path()) / ACTION_JOURNAL_DIRECTORY_NAME).generic_string()); if (!openFile(detectFiles(), false)) { @@ -266,6 +270,58 @@ void ActionJournal::Impl::onDidFailLoadingMap(MapLoadError error, const std::str }); } +void ActionJournal::Impl::onDidFinishRenderingFrame(const RenderFrameStatus& frame) { + // update report time + double currentFrameTime = util::MonotonicTimer::now().count(); + double elapsedTime = currentFrameTime - previousFrameTime; + previousFrameTime = currentFrameTime; + + // update rendering stats + if (renderingStats.encodingMin > frame.renderingStats.encodingTime) { + renderingStats.encodingMin = frame.renderingStats.encodingTime; + } + + if (renderingStats.encodingMax < frame.renderingStats.encodingTime) { + renderingStats.encodingMax = frame.renderingStats.encodingTime; + } + + if (renderingStats.renderingMin > frame.renderingStats.renderingTime) { + renderingStats.renderingMin = frame.renderingStats.renderingTime; + } + + if (renderingStats.renderingMax < frame.renderingStats.renderingTime) { + renderingStats.renderingMax = frame.renderingStats.renderingTime; + } + + renderingStats.encodingTotal += frame.renderingStats.encodingTime; + renderingStats.renderingTotal += frame.renderingStats.renderingTime; + ++renderingStats.frameCount; + + renderingInfoReportTime -= elapsedTime; + if (renderingInfoReportTime > 1e-9) { + return; + } + + renderingInfoReportTime = options.renderingStatsReportInterval(); + + // skip if the full interval had an idle map + if (renderingStats.frameCount == 0) { + return; + } + + scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this), stats = std::move(renderingStats)]() { + log(ActionJournalEvent("renderingStats", env) + .addEvent("encodingMin", stats.encodingMin) + .addEvent("encodingMax", stats.encodingMax) + .addEvent("encodingAvg", stats.encodingTotal / stats.frameCount) + .addEvent("renderingMin", stats.renderingMin) + .addEvent("renderingMax", stats.renderingMax) + .addEvent("renderingAvg", stats.renderingTotal / stats.frameCount)); + }); + + renderingStats = {}; +} + void ActionJournal::Impl::onWillStartRenderingMap() { scheduler->schedule( [=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onWillStartRenderingMap", env)); }); diff --git a/src/mbgl/util/action_journal_impl.hpp b/src/mbgl/util/action_journal_impl.hpp index ce07bc3a1..152c95028 100644 --- a/src/mbgl/util/action_journal_impl.hpp +++ b/src/mbgl/util/action_journal_impl.hpp @@ -37,6 +37,7 @@ public: void onWillStartLoadingMap() override; void onDidFinishLoadingMap() override; void onDidFailLoadingMap(MapLoadError, const std::string&) override; + void onDidFinishRenderingFrame(const RenderFrameStatus&) override; void onWillStartRenderingMap() override; void onDidFinishRenderingMap(RenderMode) override; void onDidFinishLoadingStyle() override; @@ -95,10 +96,27 @@ protected: const std::shared_ptr scheduler; + // log file std::mutex fileMutex; std::fstream currentFile; uint32_t currentFileIndex{0}; size_t currentFileSize{0}; + + // rendering info reporting + double previousFrameTime; + double renderingInfoReportTime; + + struct { + double encodingMin{std::numeric_limits::max()}; + double encodingMax{-std::numeric_limits::max()}; + double encodingTotal{0.0}; + + double renderingMin{std::numeric_limits::max()}; + double renderingMax{-std::numeric_limits::max()}; + double renderingTotal{0.0}; + + uint32_t frameCount{0}; + } renderingStats; }; } // namespace util diff --git a/test/util/action_journal.test.cpp b/test/util/action_journal.test.cpp index d97a8206a..57609a956 100644 --- a/test/util/action_journal.test.cpp +++ b/test/util/action_journal.test.cpp @@ -428,3 +428,88 @@ TEST(ActionJournal, ValidateEvents) { false, gfx::RenderingStats()); } + +TEST(ActionJournal, RenderingStats) { + ActionJournalTest test(ActionJournalOptions() + .enable() + .withPath(".") + .withLogFileCount(2) + .withLogFileSize(1024 * 1024 * 5) + .withRenderingStatsReportInterval(2), + MapMode::Continuous); + + test.map->getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json")); + test.map->getActionJournal()->impl->flush(); + test.map->getActionJournal()->impl->clearLog(); + + const auto onDidFinishRenderingFrame = + static_cast( + &RendererObserver::onDidFinishRenderingFrame); + + const std::vector testStats = { + {.encodingTime = 0.00273499998729676, .renderingTime = 0.0000957687443587929}, + {.encodingTime = 0.010939366680880388, .renderingTime = 0.0001340633297028641}, + {.encodingTime = 0.023845999967306852, .renderingTime = 0.00011808499693870545}, + {.encodingTime = 0.018161798150416603, .renderingTime = 0.00011148519331106433}, + }; + + const auto encodingMin = std::ranges::min_element( + testStats, [](const auto& a, const auto& b) { return a.encodingTime < b.encodingTime; }); + const auto encodingMax = std::ranges::min_element( + testStats, [](const auto& a, const auto& b) { return a.encodingTime > b.encodingTime; }); + const auto renderingMin = std::ranges::min_element( + testStats, [](const auto& a, const auto& b) { return a.renderingTime < b.renderingTime; }); + const auto renderingMax = std::ranges::min_element( + testStats, [](const auto& a, const auto& b) { return a.renderingTime > b.renderingTime; }); + + const auto encodingAvg = std::accumulate(testStats.begin(), + testStats.end(), + 0.0, + [](const auto& a, const auto& b) { return a + b.encodingTime; }) / + testStats.size(); + + const auto renderingAvg = std::accumulate(testStats.begin(), + testStats.end(), + 0.0, + [](const auto& a, const auto& b) { return a + b.renderingTime; }) / + testStats.size(); + + const auto validateDouble = [](const mbgl::JSValue& json, const auto name, const double value) { + EXPECT_TRUE(json.HasMember(name)); + EXPECT_TRUE(json[name].IsDouble()); + EXPECT_DOUBLE_EQ(json[name].GetDouble(), value); + }; + + const auto validateStats = [&](const mbgl::JSValue& json) { + ; + validateDouble(json, "encodingMin", encodingMin->encodingTime); + validateDouble(json, "encodingMax", encodingMax->encodingTime); + validateDouble(json, "encodingAvg", encodingAvg); + validateDouble(json, "renderingMin", renderingMin->renderingTime); + validateDouble(json, "renderingMax", renderingMax->renderingTime); + validateDouble(json, "renderingAvg", renderingAvg); + }; + + // accumulate frames in the journal + for (const auto& frameStats : testStats) { + std::this_thread::sleep_for( + std::chrono::milliseconds(test.options.renderingStatsReportInterval() * 1000 / 2 / testStats.size())); + test.map->getImpl().onDidFinishRenderingFrame(RendererObserver::RenderMode::Partial, false, false, frameStats); + + test.map->getActionJournal()->impl->flush(); + const auto& log = test.map->getActionJournal()->getLog(); + EXPECT_EQ(log.size(), 0); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(test.options.renderingStatsReportInterval() * 1000 / 2)); + + // trigger the journal event using a frame with average times to keep the state unchanged + validateEvent(test, + "renderingStats", + validateStats, + onDidFinishRenderingFrame, + RendererObserver::RenderMode::Partial, + false, + false, + gfx::RenderingStats{.encodingTime = encodingAvg, .renderingTime = renderingAvg}); +}