diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart index 9f88d02..c5f642e 100644 --- a/lib/src/renderer.dart +++ b/lib/src/renderer.dart @@ -13,7 +13,7 @@ class Renderer { final FeatureDispatcher featureRenderer; Renderer({required this.theme, Logger? logger}) - : this.logger = logger ?? Logger.noop(), + : logger = logger ?? Logger.noop(), featureRenderer = FeatureDispatcher(logger ?? Logger.noop()); /// renders the given tile to the canvas diff --git a/lib/src/themes/feature_resolver.dart b/lib/src/themes/feature_resolver.dart new file mode 100644 index 0000000..f3a7f05 --- /dev/null +++ b/lib/src/themes/feature_resolver.dart @@ -0,0 +1,57 @@ +import '../../vector_tile_renderer.dart'; +import 'theme_layers.dart'; + +/// Resolver for resolving the [VectorTileFeature]s a [ThemeLayer] should +/// render. +abstract class ThemeLayerFeatureResolver { + /// Resolves and returns the [VectorTileFeature]s within the given [tileset] + /// that the given [themeLayer] should render. + Iterable resolveFeatures(DefaultLayer themeLayer); +} + +/// Default implementation of [ThemeLayerFeatureResolver]. +class DefaultThemeLayerFeatureResolver implements ThemeLayerFeatureResolver { + DefaultThemeLayerFeatureResolver(this.tileset); + + final Tileset tileset; + + @override + Iterable resolveFeatures( + DefaultLayer themeLayer, + ) sync* { + for (final layer in themeLayer.selector.select(tileset)) { + for (final feature + in themeLayer.selector.layerSelector.features(layer.features)) { + yield ResolvedThemeLayerFeature(layer, feature); + } + } + } +} + +/// A [ThemeLayerFeatureResolver] that uses another [resolver] and caches +/// its results. +class CachingThemeLayerFeatureResolver implements ThemeLayerFeatureResolver { + CachingThemeLayerFeatureResolver(this.resolver); + + /// The resolver whose results will be cached. + final ThemeLayerFeatureResolver resolver; + + final _cache = >{}; + + @override + Iterable resolveFeatures(DefaultLayer themeLayer) { + return _cache.putIfAbsent( + themeLayer, + () => resolver.resolveFeatures(themeLayer).toList(), + ); + } +} + +/// Wrapper around [VectorTileFeature] that includes the containing +/// [VectorTileLayer]. +class ResolvedThemeLayerFeature { + ResolvedThemeLayerFeature(this.layer, this.feature); + + final VectorTileLayer layer; + final VectorTileFeature feature; +} diff --git a/lib/src/themes/theme_layers.dart b/lib/src/themes/theme_layers.dart index 302692d..992fc1d 100644 --- a/lib/src/themes/theme_layers.dart +++ b/lib/src/themes/theme_layers.dart @@ -11,23 +11,25 @@ class DefaultLayer extends ThemeLayer { final TileLayerSelector selector; final Style style; - DefaultLayer(String id, ThemeLayerType type, - {required this.selector, - required this.style, - required double? minzoom, - required double? maxzoom}) - : super(id, type, minzoom: minzoom, maxzoom: maxzoom); + DefaultLayer( + String id, + ThemeLayerType type, { + required this.selector, + required this.style, + required double? minzoom, + required double? maxzoom, + }) : super(id, type, minzoom: minzoom, maxzoom: maxzoom); @override void render(Context context) { - selector.select(context.tileset).forEach((layer) { - selector.layerSelector.features(layer.features).forEach((feature) { - context.featureRenderer.render(context, type, style, layer, feature); - if (!context.tileset.preprocessed) { - _releaseMemory(feature); - } - }); - }); + for (final feature + in context.tileset.themeLayerFeatureResolver.resolveFeatures(this)) { + context.featureRenderer + .render(context, type, style, feature.layer, feature.feature); + if (!context.tileset.preprocessed) { + _releaseMemory(feature.feature); + } + } } void _releaseMemory(VectorTileFeature feature) { diff --git a/lib/src/tileset.dart b/lib/src/tileset.dart index d744279..71a92f8 100644 --- a/lib/src/tileset.dart +++ b/lib/src/tileset.dart @@ -1,5 +1,6 @@ import 'package:vector_tile/vector_tile.dart'; +import 'themes/feature_resolver.dart'; import 'themes/theme.dart'; import 'themes/theme_layers.dart'; @@ -8,11 +9,17 @@ import 'themes/theme_layers.dart'; class Tileset { final bool preprocessed; final Map tiles; + late final ThemeLayerFeatureResolver themeLayerFeatureResolver; - Tileset(this.tiles) : this.preprocessed = false; + Tileset(this.tiles) : this.preprocessed = false { + themeLayerFeatureResolver = DefaultThemeLayerFeatureResolver(this); + } - Tileset._preprocessed(Tileset original) - : this.tiles = original.tiles, + Tileset._preprocessed( + Tileset original, + ThemeLayerFeatureResolver themeLayerFeatureResolver, + ) : this.tiles = original.tiles, + this.themeLayerFeatureResolver = themeLayerFeatureResolver, this.preprocessed = true; VectorTile? tile(String sourceId) => tiles[sourceId]; @@ -30,16 +37,18 @@ class TilesetPreprocessor { /// the rendering stage. /// /// returns a pre-processed tileset - Tileset preprocess(Tileset tileset) { - theme.layers.whereType().forEach((themeLayer) { - themeLayer.selector.select(tileset).forEach((layer) { - themeLayer.selector.layerSelector - .features(layer.features) - .forEach((feature) { - feature.decodeGeometry(); - }); - }); - }); - return Tileset._preprocessed(tileset); + Tileset preprocess(Tileset tileset, {bool themeLayerFeatures = true}) { + final themeLayerFeatureResolver = themeLayerFeatures + ? CachingThemeLayerFeatureResolver(tileset.themeLayerFeatureResolver) + : tileset.themeLayerFeatureResolver; + + for (final themeLayer in theme.layers.whereType()) { + for (final feature + in themeLayerFeatureResolver.resolveFeatures(themeLayer)) { + feature.feature.decodeGeometry(); + } + } + + return Tileset._preprocessed(tileset, themeLayerFeatureResolver); } } diff --git a/pubspec.lock b/pubspec.lock index dc8b88a..f7381a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.7.0" + benchmark_harness: + dependency: "direct dev" + description: + name: benchmark_harness + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 362a9b7..4ec8bc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,4 +13,5 @@ dependencies: vector_tile: ^0.3.0 dev_dependencies: + benchmark_harness: ^2.0.0 test: ^1.17.5 diff --git a/test/benchmark/rendering_test.dart b/test/benchmark/rendering_test.dart new file mode 100644 index 0000000..f6dbfaa --- /dev/null +++ b/test/benchmark/rendering_test.dart @@ -0,0 +1,105 @@ +import 'dart:ui'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:vector_tile_renderer/src/constants.dart'; +import 'package:vector_tile_renderer/src/themes/light_theme.dart'; +import 'package:vector_tile_renderer/vector_tile_renderer.dart'; + +import '../src/test_tile.dart'; + +Picture _renderPicture({ + required Theme theme, + required double scale, + required Tileset tileset, + required double zoom, +}) { + assert(scale >= 1 && scale <= 4); + + double size = scale * tileSize; + final rect = Rect.fromLTRB(0, 0, size, size); + + final recorder = PictureRecorder(); + final canvas = Canvas(recorder, rect); + canvas.clipRect(rect); + canvas.scale(scale.toDouble(), scale.toDouble()); + + Renderer(theme: theme).render( + canvas, + tileset, + zoomScaleFactor: scale, + zoom: zoom, + ); + + return recorder.endRecording(); +} + +class RenderPicture extends BenchmarkBase { + RenderPicture({ + required this.zoom, + this.preprocessTile = false, + this.preprocessThemeLayerFeatures = false, + }) : super('RenderPicture(' + 'zoom: $zoom, ' + 'preprocessTile: $preprocessTile, ' + 'preprocessThemeLayerFeatures: $preprocessThemeLayerFeatures' + ')'); + + static Future setupAll() async { + testTile = await readTestTile(); + } + + static late final VectorTile testTile; + + final double zoom; + final bool preprocessTile; + final bool preprocessThemeLayerFeatures; + + late final Theme theme; + late final Tileset tileset; + + @override + void setup() { + theme = ThemeReader().read(lightThemeData()); + final tileset = Tileset({'openmaptiles': testTile}); + + this.tileset = preprocessTile + ? TilesetPreprocessor(theme).preprocess( + tileset, + themeLayerFeatures: preprocessThemeLayerFeatures, + ) + : tileset; + } + + @override + void run() => _renderPicture( + theme: theme, + tileset: tileset, + scale: 1, + zoom: zoom, + ); +} + +Future main() async { + await RenderPicture.setupAll(); + + final benchmarks = [ + for (final zoom in [0, 12, 24]) ...[ + RenderPicture(zoom: zoom, preprocessTile: false), + RenderPicture(zoom: zoom, preprocessTile: true), + RenderPicture( + zoom: zoom, + preprocessTile: false, + preprocessThemeLayerFeatures: false, + ), + RenderPicture( + zoom: zoom, + preprocessTile: true, + preprocessThemeLayerFeatures: true, + ), + ] + ]; + + for (final benchmark in benchmarks) { + benchmark.report(); + } +}