Add rendering info reports to ActionJournal (#3545)

This commit is contained in:
Adrian Cojocaru
2025-07-29 19:46:46 +03:00
committed by GitHub
parent 2077df9a2c
commit 8011fa51be
15 changed files with 244 additions and 10 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -19,12 +19,14 @@ util::ActionJournalOptions NativeMapOptions::getActionJournalOptions(jni::JNIEnv
auto pathField = javaClass.GetField<jni::String>(env, "actionJournalPath");
auto logFileSizeField = javaClass.GetField<jni::jlong>(env, "actionJournalLogFileSize");
auto logFileCountField = javaClass.GetField<jni::jlong>(env, "actionJournalLogFileCount");
auto renderingReportIntervalField = javaClass.GetField<jni::jint>(env, "actionJournalRenderingReportInterval");
return util::ActionJournalOptions()
.enable(obj.Get(env, enabledField))
.withPath(jni::Make<std::string>(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<NativeMapOptions> &obj) {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -16,6 +16,7 @@
<attr name="maplibre_actionJournalEnabled" format="boolean"/>
<attr name="maplibre_actionJournalLogFileSize" format="integer"/>
<attr name="maplibre_actionJournalLogFileCount" format="integer"/>
<attr name="maplibre_actionJournalRenderingReportInterval" format="integer"/>
<!--Camera-->
<attr name="maplibre_cameraTargetLat" format="float"/>

View File

@@ -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"/>
<!-- -8<- [end:MapView] -->
<com.google.android.material.floatingactionbutton.FloatingActionButton

View File

@@ -30,6 +30,11 @@ MLN_EXPORT
*/
@property (nonatomic) NSInteger logFileCount;
/**
* The wait time (seconds) between rendering reports
*/
@property (nonatomic) NSInteger renderingStatsReportInterval;
@end
NS_ASSUME_NONNULL_END

View File

@@ -51,4 +51,12 @@
return _actionJournalOptionsInternal->logFileCount();
}
- (void)setRenderingStatsReportInterval:(NSInteger)value {
_actionJournalOptionsInternal->withRenderingStatsReportInterval(static_cast<uint32_t>(value));
}
- (NSInteger)renderingStatsReportInterval {
return _actionJournalOptionsInternal->renderingStatsReportInterval();
}
@end

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,6 @@
#include <mbgl/util/action_journal_impl.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/monotonic_timer.hpp>
#include <mbgl/style/style.hpp>
#include <mbgl/map/map.hpp>
@@ -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)); });

View File

@@ -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> 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<double>::max()};
double encodingMax{-std::numeric_limits<double>::max()};
double encodingTotal{0.0};
double renderingMin{std::numeric_limits<double>::max()};
double renderingMax{-std::numeric_limits<double>::max()};
double renderingTotal{0.0};
uint32_t frameCount{0};
} renderingStats;
};
} // namespace util

View File

@@ -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<void (RendererObserver::*)(RendererObserver::RenderMode, bool, bool, const gfx::RenderingStats&)>(
&RendererObserver::onDidFinishRenderingFrame);
const std::vector<gfx::RenderingStats> 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});
}