core: Add error reporting for corrupted symbols (#4458)

This commit is contained in:
Adrian Cojocaru
2026-08-11 19:20:59 +03:00
committed by GitHub
parent 7f430aa624
commit d9664ee187
39 changed files with 331 additions and 29 deletions

View File

@@ -441,6 +441,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/util/size.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/string_indexer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/string.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/symbol_error_observer.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/thread.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/tile_server_options.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/util/tileset.hpp
@@ -929,6 +930,8 @@ list(APPEND SRC_FILES
${PROJECT_SOURCE_DIR}/src/mbgl/util/color$<IF:$<BOOL:${MLN_USE_RUST}>,.rs.cpp,.cpp>
${PROJECT_SOURCE_DIR}/src/mbgl/util/constants.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/util/convert.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/util/error_sink.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/util/error_sink.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/util/event.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/util/filesystem.hpp
${PROJECT_SOURCE_DIR}/src/mbgl/util/font_stack.cpp

View File

@@ -557,6 +557,8 @@ MLN_CORE_SOURCE = [
"src/mbgl/util/client_options.cpp",
"src/mbgl/util/constants.cpp",
"src/mbgl/util/convert.cpp",
"src/mbgl/util/error_sink.hpp",
"src/mbgl/util/error_sink.cpp",
"src/mbgl/util/event.cpp",
"src/mbgl/util/filesystem.hpp",
"src/mbgl/util/font_stack.cpp",
@@ -855,6 +857,7 @@ MLN_CORE_HEADERS = [
"include/mbgl/util/scoped.hpp",
"include/mbgl/util/size.hpp",
"include/mbgl/util/string.hpp",
"include/mbgl/util/symbol_error_observer.hpp",
"include/mbgl/util/string_indexer.hpp",
"include/mbgl/util/tile_server_options.hpp",
"include/mbgl/util/thread.hpp",

View File

@@ -92,6 +92,10 @@ public:
// Renderer
virtual void onRenderError(std::exception_ptr) {}
/// Called with a description of a corrupted symbol detected by the symbol guard checks
/// (see `MLN_SYMBOL_GUARDS`), on the thread which detected it.
virtual void onSymbolError(const std::string&) {}
};
} // namespace mbgl

View File

@@ -7,6 +7,7 @@
#include <mbgl/tile/tile_operation.hpp>
#include <mbgl/gfx/backend.hpp>
#include <mbgl/shaders/shader_source.hpp>
#include <mbgl/util/symbol_error_observer.hpp>
#include <cstdint>
#include <exception>
@@ -19,9 +20,9 @@ namespace gfx {
class ShaderRegistry;
}
class RendererObserver {
class RendererObserver : public SymbolErrorObserver {
public:
virtual ~RendererObserver() = default;
~RendererObserver() override = default;
enum class RenderMode : uint32_t {
Partial,

View File

@@ -0,0 +1,19 @@
#pragma once
#include <string>
namespace mbgl {
/// Receives errors detected by the symbol guard checks (see `MLN_SYMBOL_GUARDS`).
/// Both `TileObserver` and `RendererObserver` derive from this, so that whichever of them owns
/// the thread a check ran on can be installed as that thread's error destination, see `ErrorSink`.
class SymbolErrorObserver {
public:
virtual ~SymbolErrorObserver() = default;
/// Called with a description of a single corrupted item, on the thread which detected it.
/// Several calls may be made for the same bucket if it holds more than one corrupted item.
virtual void onSymbolError(const std::string&) {}
};
} // namespace mbgl

View File

@@ -54,6 +54,10 @@ public:
delegate.invoke(&RendererObserver::onRemoveUnusedStyleImages, ids);
}
void onSymbolError(const std::string& message) override {
delegate.invoke(&RendererObserver::onSymbolError, message);
}
void onPreCompileShader(mbgl::shaders::BuiltIn id,
mbgl::gfx::Backend::Type type,
const std::string& additionalDefines) override {

View File

@@ -1737,5 +1737,17 @@ void NativeMapView::onRenderError(std::exception_ptr) {
}
}
void NativeMapView::onSymbolError(const std::string& message) {
assert(vm != nullptr);
android::UniqueEnv _env = android::AttachEnv();
static auto& javaClass = jni::Class<NativeMapView>::Singleton(*_env);
static auto onSymbolError = javaClass.GetMethod<void(jni::String)>(*_env, "onSymbolError");
auto weakReference = javaPeer.get(*_env);
if (weakReference) {
weakReference.Call(*_env, onSymbolError, jni::Make<jni::String>(*_env, message));
}
}
} // namespace android
} // namespace mbgl

View File

@@ -367,6 +367,8 @@ public:
// Renderer
void onRenderError(std::exception_ptr) override;
void onSymbolError(const std::string&) override;
private:
std::shared_ptr<AndroidRendererFrontend> rendererFrontend;

View File

@@ -60,6 +60,8 @@ class MapChangeReceiver implements NativeMapView.StateCallback {
= new CopyOnWriteArrayList<>();
private final List<MapView.OnRenderErrorListener> onRenderErrorList
= new CopyOnWriteArrayList<>();
private final List<MapView.OnSymbolErrorListener> onSymbolErrorList
= new CopyOnWriteArrayList<>();
@Override
public void onCameraWillChange(boolean animated) {
@@ -440,6 +442,17 @@ class MapChangeReceiver implements NativeMapView.StateCallback {
}
}
@Override
public void onSymbolError(String message) {
try {
for (MapView.OnSymbolErrorListener listener : onSymbolErrorList) {
listener.onSymbolError(message);
}
} catch (Throwable err) {
Logger.e(TAG, "Exception in onSymbolError", err);
}
}
void addOnCameraWillChangeListener(MapView.OnCameraWillChangeListener listener) {
onCameraWillChangeListenerList.add(listener);
}
@@ -612,6 +625,10 @@ class MapChangeReceiver implements NativeMapView.StateCallback {
onRenderErrorList.add(callback);
}
public void addOnSymbolErrorListener(MapView.OnSymbolErrorListener callback) {
onSymbolErrorList.add(callback);
}
public void removeOnPreCompileShaderListener(MapView.OnPreCompileShaderListener callback) {
onPreCompileShaderList.remove(callback);
}
@@ -656,6 +673,10 @@ class MapChangeReceiver implements NativeMapView.StateCallback {
onRenderErrorList.remove(callback);
}
public void removeOnSymbolErrorListener(MapView.OnSymbolErrorListener callback) {
onSymbolErrorList.remove(callback);
}
void clear() {
onCameraWillChangeListenerList.clear();
onCameraIsChangingListenerList.clear();
@@ -683,5 +704,6 @@ class MapChangeReceiver implements NativeMapView.StateCallback {
onSpriteErrorList.clear();
onSpriteRequestedList.clear();
onRenderErrorList.clear();
onSymbolErrorList.clear();
}
}

View File

@@ -1121,6 +1121,19 @@ public class MapView extends FrameLayout implements NativeMapView.ViewCallback {
mapChangeReceiver.removeOnRenderErrorListener(callback);
}
/**
* Set a callback that's invoked after a corrupted symbol is detected
*
* @param listener The callback that's invoked
*/
public void addOnSymbolErrorListener(MapView.OnSymbolErrorListener callback) {
mapChangeReceiver.addOnSymbolErrorListener(callback);
}
public void removeOnSymbolErrorListener(MapView.OnSymbolErrorListener callback) {
mapChangeReceiver.removeOnSymbolErrorListener(callback);
}
/**
* Interface definition for a callback to be invoked when the camera will change.
* <p>
@@ -1549,6 +1562,19 @@ public class MapView extends FrameLayout implements NativeMapView.ViewCallback {
void onRenderError();
}
/**
* Interface definition for a callback to be invoked after a corrupted symbol is detected
* <p>
* {@link MapView#addOnSymbolErrorListener(OnSymbolErrorListener)}
* </p>
*/
public interface OnSymbolErrorListener {
/**
* Called when a corrupted symbol is detected.
*/
void onSymbolError(@NonNull String message);
}
/**
* Sets a callback object which will be triggered when the {@link MapLibreMap} instance is ready to be used.
*

View File

@@ -1430,6 +1430,13 @@ final class NativeMapView implements NativeMap {
}
}
@Keep
private void onSymbolError(String message) {
if (stateCallback != null) {
stateCallback.onSymbolError(message);
}
}
//
// JNI methods
//
@@ -1938,5 +1945,7 @@ final class NativeMapView implements NativeMap {
void onSpriteRequested(String id, String url);
void onRenderError();
void onSymbolError(String message);
}
}

View File

@@ -19,12 +19,12 @@ fun obtainTestBuildType(): String {
android {
ndkVersion = Versions.ndkVersion
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "org.maplibre.android.testapp"
minSdk = 23
targetSdk = 33
targetSdk = 35
versionCode = 14
testInstrumentationRunner = "org.maplibre.android.InstrumentationRunner"
multiDexEnabled = true
@@ -155,6 +155,8 @@ dependencies {
implementation(libs.maplibreJavaTurf)
implementation(libs.sentryAndroid)
implementation(libs.supportRecyclerView)
implementation(libs.supportPrint)
implementation(libs.supportDesign)

View File

@@ -62,7 +62,7 @@ class SequentialActivityTest(private val activity: Class<out Activity>) {
PackageManager.GET_ACTIVITIES
)
val activities = packageInfo.activities
val activities = packageInfo.activities!!
.filter { info ->
info.name.startsWith("org.maplibre.android.testapp.activity")
}

View File

@@ -131,7 +131,7 @@ class FeatureOverviewActivity : AppCompatActivity() {
val packageName = FeatureOverviewActivity::class.java.`package`!!.name
val metaDataKey = getString(R.string.category)
if (app != null) {
for (info in app.activities) {
for (info in app.activities!!) {
if (info.labelRes != 0 && info.name.startsWith(packageName) &&
info.name != FeatureOverviewActivity::class.java.name
) {

View File

@@ -27,7 +27,8 @@ class ObserverActivity : AppCompatActivity(),
MapView.OnGlyphsRequestedListener,
MapView.OnSpriteLoadedListener,
MapView.OnSpriteRequestedListener,
MapView.OnDidFinishRenderingFrameWithStatsListener {
MapView.OnDidFinishRenderingFrameWithStatsListener,
MapView.OnSymbolErrorListener {
// # --8<-- [end:ObserverActivity]
private lateinit var mapView: MapView
@@ -52,6 +53,7 @@ class ObserverActivity : AppCompatActivity(),
mapView.addOnSpriteLoadedListener(this)
mapView.addOnSpriteRequestedListener(this)
mapView.addOnDidFinishRenderingFrameListener(this)
mapView.addOnSymbolErrorListener(this)
// # --8<-- [end:addListeners]
// # --8<-- [start:renderStatsTracker]
@@ -158,6 +160,10 @@ class ObserverActivity : AppCompatActivity(),
override fun onDidFinishRenderingFrame(fully: Boolean, stats: RenderingStats) {
renderStatsTracker.addFrame(stats)
}
override fun onSymbolError(message: String) {
Logger.i(TAG, "onSymbolError: $message")
}
// # --8<-- [end:mapEvents]
override fun onStart() {

View File

@@ -3,7 +3,6 @@ package org.maplibre.android.testapp.activity.stability
import android.annotation.SuppressLint
import android.location.Location
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.gson.Gson
@@ -45,8 +44,9 @@ import org.maplibre.navigation.android.navigation.v5.routeprogress.RouteProgress
import java.util.Locale
import java.util.logging.Logger
import kotlin.random.Random
import io.sentry.Sentry
class NavigationMap : SupportMapFragment(), ProgressChangeListener, MilestoneEventListener {
class NavigationMap : SupportMapFragment(), ProgressChangeListener, MilestoneEventListener, MapView.OnSymbolErrorListener {
private lateinit var map: MapLibreMap
private lateinit var mapView: MapView
@@ -105,6 +105,7 @@ class NavigationMap : SupportMapFragment(), ProgressChangeListener, MilestoneEve
this.map = maplibreMap
this.mapView = view as MapView
this.mapView.addOnSymbolErrorListener(this)
LOG.info("NavigationMap seed $RANDOM_SEED")
run()
@@ -357,6 +358,11 @@ class NavigationMap : SupportMapFragment(), ProgressChangeListener, MilestoneEve
startNewRoute()
}
}
override fun onSymbolError(message: String) {
Sentry.captureMessage("NavigationMap: $message")
//Bugsnag.notify(RuntimeException(message))
}
}
class NavigationMapActivity : AppCompatActivity() {

View File

@@ -27,8 +27,9 @@ import java.lang.reflect.Field
import java.util.logging.Logger
import kotlin.math.min
import kotlin.random.Random
import io.sentry.Sentry
class UserMap : SupportMapFragment() {
class UserMap : SupportMapFragment(), MapView.OnSymbolErrorListener {
private lateinit var map: MapLibreMap
private lateinit var mapView: MapView
private lateinit var bitmapDrawables: List<Drawable>
@@ -140,6 +141,7 @@ class UserMap : SupportMapFragment() {
this.map = maplibreMap
this.mapView = view as MapView
this.mapView.addOnSymbolErrorListener(this)
LOG.info("UserMap seed $RANDOM_SEED")
run()
@@ -267,6 +269,11 @@ class UserMap : SupportMapFragment() {
)
}
}
override fun onSymbolError(message: String) {
Sentry.captureMessage("UserMap: $message")
//Bugsnag.notify(RuntimeException(message))
}
}
class UserMapActivity : AppCompatActivity() {

View File

@@ -4,6 +4,7 @@ constraintLayout = "2.2.1"
interpolator = "1.0.0"
recyclerView = "1.3.0"
material = "1.13.0"
sentryAndroid = "8.51.0"
uiAutomator = "2.3.0"
espresso = "3.7.0"
testRunner = "1.3.0"
@@ -41,6 +42,7 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
assertjcore = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" }
sentryAndroid = { module = "io.sentry:sentry-android", version.ref = "sentryAndroid" }
testRunner = { group = "androidx.test.ext", name = "junit", version.ref = "testRunner" }
testRules = { group = "androidx.test", name = "rules", version.ref = "testRules" }
testEspressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }

View File

@@ -49,6 +49,10 @@ public:
delegate.invoke(&RendererObserver::onStyleImageMissing, image, cb);
}
void onSymbolError(const std::string& message) override {
delegate.invoke(&RendererObserver::onSymbolError, message);
}
private:
std::shared_ptr<Mailbox> mailbox;
ActorRef<RendererObserver> delegate;
@@ -108,6 +112,8 @@ public:
rendererObserver->onStyleImageMissing(id, done);
}
void onSymbolError(const std::string& message) override { rendererObserver->onSymbolError(message); }
void setObserver(std::shared_ptr<RendererObserver> observer) {
assert(observer);
rendererObserver = std::move(observer);

View File

@@ -1,5 +1,6 @@
#include <mbgl/layout/symbol_instance.hpp>
#include <mbgl/style/layers/symbol_layer_properties.hpp>
#include <mbgl/util/error_sink.hpp>
#include <mbgl/util/logging.hpp>
#include <utility>
@@ -265,21 +266,29 @@ inline std::string locationSuffix(const source_location& source) {
util::toString(source.line()) + ")";
}
} // namespace
void SymbolInstance::fail(const std::string& message) const {
isFailed = true;
Log::Error(Event::Crash, message);
// Report as soon as the failure is detected, on the thread which detected it. The destination
// is the observer owning that thread, installed with an `ErrorScope`, see `SymbolErrorObserver`.
ErrorSink::report(message);
assert(false);
}
bool SymbolInstance::check(std::uint64_t v, int n, const source_location& source) const {
if (!isFailed && v != checkVal) {
isFailed = true;
Log::Error(Event::Crash,
"SymbolInstance corrupted at " + util::toString(n) + " with value " + util::toString(v) +
locationSuffix(source));
fail("SymbolInstance corrupted at " + util::toString(n) + " with value " + util::toString(v) +
locationSuffix(source));
}
return !isFailed;
}
bool SymbolInstance::checkKey(const source_location& source) const {
if (!isFailed && key.size() > 10000) { // largest observed value=62
isFailed = true;
Log::Error(Event::Crash,
"SymbolInstance key corrupted with size=" + util::toString(key.size()) + locationSuffix(source));
fail("SymbolInstance key corrupted with size=" + util::toString(key.size()) + locationSuffix(source));
}
return !isFailed;
}
@@ -288,16 +297,16 @@ bool SymbolInstance::checkIndex(const std::optional<std::size_t>& index,
std::size_t size,
const source_location& source) const {
if (index.has_value() && *index >= size) {
isFailed = true;
Log::Error(Event::Crash,
"SymbolInstance index corrupted with value=" + util::toString(*index) +
" size=" + util::toString(size) + locationSuffix(source));
fail("SymbolInstance index corrupted with value=" + util::toString(*index) + " size=" + util::toString(size) +
locationSuffix(source));
}
return !isFailed;
}
void SymbolInstance::forceFail() const {
isFailed = true;
if (!isFailed) {
fail("SymbolInstance marked as failed by an external check");
}
}
// this is just to avoid warnings about the values never being set

View File

@@ -183,8 +183,10 @@ protected:
bool check(std::uint64_t v, int n, const source_location&) const;
bool checkKey(const source_location&) const;
void forceFailInternal(); // this is just to avoid warnings about the values never being set
void fail(const std::string&) const;
#else
bool checkKey(std::string_view) const { return true; }
void fail(const std::string&) const {}
#endif
private:

View File

@@ -432,6 +432,18 @@ void Map::Impl::onTileAction(TileOperation op, const OverscaledTileID& id, const
void Map::Impl::onRenderError(std::exception_ptr error) {
observer.onRenderError(error);
if (actionJournal) {
actionJournal->impl->onRenderError(error);
}
}
void Map::Impl::onSymbolError(const std::string& message) {
observer.onSymbolError(message);
if (actionJournal) {
actionJournal->impl->onSymbolError(message);
}
}
} // namespace mbgl

View File

@@ -75,6 +75,7 @@ public:
void onGlyphsRequested(const FontStack&, const GlyphRange&) final;
void onTileAction(TileOperation op, const OverscaledTileID&, const std::string&) final;
void onRenderError(std::exception_ptr) final;
void onSymbolError(const std::string&) final;
// Map
void jumpTo(const CameraOptions&);

View File

@@ -1068,6 +1068,10 @@ void RenderOrchestrator::onTileAction(RenderSource&,
observer->onTileAction(op, id, sourceID);
}
void RenderOrchestrator::onSymbolError(RenderSource&, const std::string& message) {
observer->onSymbolError(message);
}
void RenderOrchestrator::onStyleImageMissing(const std::string& id, const std::function<void()>& done) {
MLN_TRACE_FUNC();

View File

@@ -191,6 +191,7 @@ private:
void onTileChanged(RenderSource&, const OverscaledTileID&) override;
void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) override;
void onTileAction(RenderSource&, TileOperation, const OverscaledTileID&, const std::string&) override;
void onSymbolError(RenderSource&, const std::string&) override;
// ImageManagerObserver implementation
void onStyleImageMissing(const std::string&, const std::function<void()>&) override;

View File

@@ -86,6 +86,10 @@ void RenderSource::onTileAction(OverscaledTileID id, std::string sourceID, TileO
observer->onTileAction(*this, op, id, sourceID);
}
void RenderSource::onSymbolError(const std::string& message) {
observer->onSymbolError(*this, message);
}
bool RenderSource::isEnabled() const {
return enabled;
}

View File

@@ -127,6 +127,7 @@ protected:
void onTileChanged(Tile&) override;
void onTileError(Tile&, std::exception_ptr) final;
void onTileAction(OverscaledTileID, std::string, TileOperation) override;
void onSymbolError(const std::string&) override;
};
} // namespace mbgl

View File

@@ -16,6 +16,8 @@ public:
virtual void onTileChanged(RenderSource&, const OverscaledTileID&) {}
virtual void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) {}
virtual void onTileAction(RenderSource&, TileOperation, const OverscaledTileID&, const std::string&) {}
virtual void onSymbolError(RenderSource&, const std::string&) {}
};
} // namespace mbgl

View File

@@ -9,6 +9,7 @@
#include <mbgl/renderer/render_static_data.hpp>
#include <mbgl/renderer/render_tree.hpp>
#include <mbgl/renderer/update_parameters.hpp>
#include <mbgl/util/error_sink.hpp>
#include <mbgl/util/instrumentation.hpp>
#include <algorithm>
@@ -35,6 +36,11 @@ void Renderer::setObserver(RendererObserver* observer) {
void Renderer::render(const std::shared_ptr<UpdateParameters>& updateParameters) {
MLN_TRACE_FUNC();
assert(updateParameters);
// Route the errors detected on the render thread to the renderer observer for the duration of the frame (see
// `SymbolErrorObserver`).
const ErrorScope errorScope{impl->observer};
const bool styleChanged = impl->styleLoaded && !updateParameters->styleLoaded;
impl->styleLoaded = updateParameters->styleLoaded;
if (!impl->dynamicTextureAtlas || styleChanged) {

View File

@@ -17,6 +17,7 @@
#include <mbgl/tile/geometry_tile_data.hpp>
#include <mbgl/tile/geometry_tile_worker.hpp>
#include <mbgl/tile/tile_observer.hpp>
#include <mbgl/util/error_sink.hpp>
#include <mbgl/util/instrumentation.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/thread_pool.hpp>
@@ -204,7 +205,8 @@ GeometryTile::GeometryTile(const OverscaledTileID& id_,
parameters.pixelRatio,
parameters.debugOptions & MapDebugOptions::Collision,
parameters.dynamicTextureAtlas,
parameters.glyphManager->getFontFaces()),
parameters.glyphManager->getFontFaces(),
observer),
fileSource(parameters.fileSource),
glyphManager(parameters.glyphManager),
imageManager(parameters.imageManager),
@@ -344,6 +346,8 @@ void GeometryTile::onLayout(std::shared_ptr<LayoutResult>&& result, const uint64
observer->onTileAction(id, sourceID, TileOperation::EndParse);
}
const ErrorScope errorScope{observer};
layoutResult = std::move(result);
if (!atlasTextures) {
atlasTextures = std::make_shared<TileAtlasTextures>();
@@ -368,6 +372,12 @@ void GeometryTile::onLayout(std::shared_ptr<LayoutResult>&& result, const uint64
}
}
void GeometryTile::setObserver(TileObserver* observer_) {
Tile::setObserver(observer_);
worker.self().invoke(&GeometryTileWorker::setObserver, observer);
}
void GeometryTile::onError(std::exception_ptr err, const uint64_t resultCorrelationID) {
loaded = true;
if (resultCorrelationID == correlationID) {

View File

@@ -93,6 +93,8 @@ public:
void onError(std::exception_ptr, uint64_t correlationID);
void setObserver(TileObserver*) override;
bool holdForFade() const override;
void markRenderedIdeal() override;
void markRenderedPreviously() override;

View File

@@ -1,6 +1,7 @@
#include <mbgl/tile/geometry_tile_worker.hpp>
#include <mbgl/tile/geometry_tile_data.hpp>
#include <mbgl/tile/geometry_tile.hpp>
#include <mbgl/tile/tile_observer.hpp>
#include <mbgl/layermanager/layer_manager.hpp>
#include <mbgl/layout/layout.hpp>
#include <mbgl/layout/symbol_layout.hpp>
@@ -14,6 +15,7 @@
#include <mbgl/renderer/layers/render_line_layer.hpp>
#include <mbgl/renderer/layers/render_symbol_layer.hpp>
#include <mbgl/renderer/buckets/symbol_bucket.hpp>
#include <mbgl/util/error_sink.hpp>
#include <mbgl/util/instrumentation.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/constants.hpp>
@@ -39,7 +41,8 @@ GeometryTileWorker::GeometryTileWorker(OptionalActorRef<GeometryTileWorker> self
const float pixelRatio_,
const bool showCollisionBoxes_,
gfx::DynamicTextureAtlasPtr dynamicTextureAtlas_,
std::shared_ptr<FontFaces> fontFaces_)
std::shared_ptr<FontFaces> fontFaces_,
TileObserver* observer_)
: self(std::move(self_)),
parent(std::move(parent_)),
scheduler(scheduler_),
@@ -50,7 +53,8 @@ GeometryTileWorker::GeometryTileWorker(OptionalActorRef<GeometryTileWorker> self
pixelRatio(pixelRatio_),
showCollisionBoxes(showCollisionBoxes_),
dynamicTextureAtlas(dynamicTextureAtlas_),
fontFaces(fontFaces_) {}
fontFaces(fontFaces_),
observer(observer_) {}
GeometryTileWorker::~GeometryTileWorker() {
MLN_TRACE_FUNC();
@@ -406,6 +410,10 @@ void GeometryTileWorker::requestNewImages(const ImageDependencies& imageDependen
}
}
void GeometryTileWorker::setObserver(TileObserver* observer_) {
observer = observer_;
}
void GeometryTileWorker::parse() {
MLN_TRACE_FUNC();
@@ -413,6 +421,10 @@ void GeometryTileWorker::parse() {
return;
}
// The layouts created below check the symbols they build; report through the tile's observer,
// called on this worker thread.
const ErrorScope errorScope{observer};
MBGL_TIMING_START(watch)
std::unordered_map<std::string, std::unique_ptr<SymbolLayout>> symbolLayoutMap;
@@ -537,6 +549,10 @@ void GeometryTileWorker::finalizeLayout() {
return;
}
// The layouts check the symbols they build below; report through the tile's observer,
// called on this worker thread.
const ErrorScope errorScope{observer};
MBGL_TIMING_START(watch);
gfx::ImageAtlas imageAtlas;
gfx::GlyphAtlas glyphAtlas;

View File

@@ -44,9 +44,12 @@ public:
float pixelRatio,
bool showCollisionBoxes_,
gfx::DynamicTextureAtlasPtr,
std::shared_ptr<FontFaces> fontFaces);
std::shared_ptr<FontFaces> fontFaces,
TileObserver* observer);
~GeometryTileWorker();
void setObserver(TileObserver* observer);
void setLayers(std::vector<Immutable<style::LayerProperties>>,
std::set<std::string> availableImages,
uint64_t correlationID);
@@ -123,6 +126,8 @@ private:
gfx::DynamicTextureAtlasPtr dynamicTextureAtlas;
std::shared_ptr<FontFaces> fontFaces;
TileObserver* observer;
};
} // namespace mbgl

View File

@@ -62,7 +62,7 @@ public:
virtual std::unique_ptr<TileRenderData> createRenderData() = 0;
void setObserver(TileObserver* observer);
virtual void setObserver(TileObserver* observer);
virtual void setNecessity(TileNecessity) {}

View File

@@ -2,15 +2,17 @@
#include <mbgl/tile/tile_id.hpp>
#include <mbgl/tile/tile_operation.hpp>
#include <mbgl/util/symbol_error_observer.hpp>
#include <exception>
namespace mbgl {
class Tile;
class TileObserver {
class TileObserver : public SymbolErrorObserver {
public:
virtual ~TileObserver() = default;
~TileObserver() override = default;
virtual void onTileChanged(Tile&) {}
virtual void onTileError(Tile&, std::exception_ptr) {}

View File

@@ -487,6 +487,26 @@ void ActionJournal::Impl::onSpriteRequested(const std::optional<style::Sprite>&
});
}
void ActionJournal::Impl::onRenderError(std::exception_ptr e) {
scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() {
ActionJournalEvent event("onRenderError", env);
event.addEvent("exception", toString(e));
log(event);
});
}
void ActionJournal::Impl::onSymbolError(const std::string& message) {
scheduler->schedule([=, this, env = MapEnvironmentSnapshot(*this)]() {
ActionJournalEvent event("onSymbolError", env);
event.addEvent("message", message);
log(event);
});
}
void ActionJournal::Impl::onMapCreate() {
scheduler->schedule(
[=, this, env = MapEnvironmentSnapshot(*this)]() { log(ActionJournalEvent("onMapCreate", env)); });

View File

@@ -55,6 +55,8 @@ public:
void onSpriteLoaded(const std::optional<style::Sprite>&) override;
void onSpriteError(const std::optional<style::Sprite>&, std::exception_ptr) override;
void onSpriteRequested(const std::optional<style::Sprite>&) override;
void onRenderError(std::exception_ptr) override;
void onSymbolError(const std::string&) override;
void onMapCreate();
void onMapDestroy();

View File

@@ -0,0 +1,26 @@
#include <mbgl/util/error_sink.hpp>
#include <mbgl/util/symbol_error_observer.hpp>
namespace mbgl {
SymbolErrorObserver*& ErrorSink::current() {
static thread_local SymbolErrorObserver* observer = nullptr;
return observer;
}
void ErrorSink::report(const std::string& message) {
if (auto* observer = current()) {
observer->onSymbolError(message);
}
}
ErrorScope::ErrorScope(SymbolErrorObserver* observer)
: previous(ErrorSink::current()) {
ErrorSink::current() = observer;
}
ErrorScope::~ErrorScope() {
ErrorSink::current() = previous;
}
} // namespace mbgl

View File

@@ -0,0 +1,43 @@
#pragma once
#include <string>
namespace mbgl {
class SymbolErrorObserver;
/// Thread-local destination for errors detected by code too deep to reach an observer on its own,
/// e.g. the symbol guard checks in `SymbolInstance` and `SymbolBucket` (see `MLN_SYMBOL_GUARDS`).
///
/// Each thread which owns an observer installs it with an `ErrorScope` around the work it drives,
/// so that a failure is reported as soon as it's detected, through the observer owning the thread
/// the check ran on: `TileObserver` for tile parsing/layout, `RendererObserver` for the render loop.
class ErrorSink {
public:
/// Report an error to the observer installed on the current thread, if any. Does nothing
/// otherwise, so check sites don't have to care whether an observer is in reach.
static void report(const std::string& message);
private:
friend class ErrorScope;
/// The observer installed on the calling thread, `nullptr` when there's none.
static SymbolErrorObserver*& current();
};
/// Installs an observer as the current thread's error destination for the lifetime of the scope,
/// restoring the previous one on destruction.
class ErrorScope {
public:
/// @param observer May be null, which mutes reporting for the duration of the scope.
explicit ErrorScope(SymbolErrorObserver* observer);
~ErrorScope();
ErrorScope(const ErrorScope&) = delete;
ErrorScope& operator=(const ErrorScope&) = delete;
private:
SymbolErrorObserver* previous;
};
} // namespace mbgl