Add rendering info reports to ActionJournal (#3545)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,4 +51,12 @@
|
||||
return _actionJournalOptionsInternal->logFileCount();
|
||||
}
|
||||
|
||||
- (void)setRenderingStatsReportInterval:(NSInteger)value {
|
||||
_actionJournalOptionsInternal->withRenderingStatsReportInterval(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
- (NSInteger)renderingStatsReportInterval {
|
||||
return _actionJournalOptionsInternal->renderingStatsReportInterval();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)); });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user