Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support caching of VectorTileFeatures resolved for a ThemeLayer #4

Merged
merged 3 commits into from Jan 2, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 56 additions & 0 deletions lib/src/themes/feature_resolver.dart
@@ -0,0 +1,56 @@
import 'package:vector_tile_renderer/src/themes/selector.dart';

import '../../vector_tile_renderer.dart';

/// Resolver for resolving the [LayerFeature]s, that a [ThemeLayer] should
blaugold marked this conversation as resolved.
Show resolved Hide resolved
/// render.
abstract class ThemeLayerFeatureResolver {
blaugold marked this conversation as resolved.
Show resolved Hide resolved
/// Resolves and returns the [LayerFeature]s that the given [selector]
/// selects for a [ThemeLayer].
blaugold marked this conversation as resolved.
Show resolved Hide resolved
Iterable<LayerFeature> resolveFeatures(TileLayerSelector selector);
}

/// Default implementation of [ThemeLayerFeatureResolver] that resolves the
/// features contained in a specific [tileset] for a [ThemeLayer].
class DefaultThemeLayerFeatureResolver implements ThemeLayerFeatureResolver {
DefaultThemeLayerFeatureResolver(this.tileset);

/// The [Tileset] from which to resolves [LayerFeature]s.
final Tileset tileset;

@override
Iterable<LayerFeature> resolveFeatures(TileLayerSelector selector) sync* {
for (final layer in selector.select(tileset)) {
for (final feature in selector.layerSelector.features(layer.features)) {
yield LayerFeature(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;
blaugold marked this conversation as resolved.
Show resolved Hide resolved

final _cache = <TileLayerSelector, List<LayerFeature>>{};

@override
Iterable<LayerFeature> resolveFeatures(TileLayerSelector selector) {
return _cache.putIfAbsent(
selector,
() => resolver.resolveFeatures(selector).toList(),
);
}
}

/// A feature that is rendered by a [ThemeLayer].
blaugold marked this conversation as resolved.
Show resolved Hide resolved
class LayerFeature {
LayerFeature(this.layer, this.feature);

final VectorTileLayer layer;
final VectorTileFeature feature;
}
30 changes: 16 additions & 14 deletions lib/src/themes/theme_layers.dart
Expand Up @@ -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.resolver.resolveFeatures(this.selector)) {
context.featureRenderer
.render(context, type, style, feature.layer, feature.feature);
if (!context.tileset.preprocessed) {
_releaseMemory(feature.feature);
}
}
}

void _releaseMemory(VectorTileFeature feature) {
Expand Down
37 changes: 23 additions & 14 deletions 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';

Expand All @@ -8,11 +9,17 @@ import 'themes/theme_layers.dart';
class Tileset {
final bool preprocessed;
final Map<String, VectorTile> tiles;
late final ThemeLayerFeatureResolver resolver;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is late needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because we need to access this to initialize resolver. That means we cannot initialize before the constructor body.


Tileset(this.tiles) : this.preprocessed = false;
Tileset(this.tiles) : this.preprocessed = false {
resolver = DefaultThemeLayerFeatureResolver(this);
}

Tileset._preprocessed(Tileset original)
: this.tiles = original.tiles,
Tileset._preprocessed(
Tileset original,
ThemeLayerFeatureResolver themeLayerFeatureResolver,
) : this.tiles = original.tiles,
this.resolver = themeLayerFeatureResolver,
this.preprocessed = true;

VectorTile? tile(String sourceId) => tiles[sourceId];
Expand All @@ -29,17 +36,19 @@ class TilesetPreprocessor {
/// Pre-processes a tileset to eliminate some expensive processing from
/// the rendering stage.
///
/// returns a pre-processed tileset
/// Returns a pre-processed tileset.
Tileset preprocess(Tileset tileset) {
theme.layers.whereType<DefaultLayer>().forEach((themeLayer) {
themeLayer.selector.select(tileset).forEach((layer) {
themeLayer.selector.layerSelector
.features(layer.features)
.forEach((feature) {
feature.decodeGeometry();
});
});
});
return Tileset._preprocessed(tileset);
final featureResolver = tileset.resolver is CachingThemeLayerFeatureResolver
? tileset.resolver
: CachingThemeLayerFeatureResolver(tileset.resolver);

for (final themeLayer in theme.layers.whereType<DefaultLayer>()) {
for (final feature
in featureResolver.resolveFeatures(themeLayer.selector)) {
feature.feature.decodeGeometry();
}
}

return Tileset._preprocessed(tileset, featureResolver);
}
}
7 changes: 7 additions & 0 deletions pubspec.lock
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Expand Up @@ -13,4 +13,5 @@ dependencies:
vector_tile: ^0.3.0

dev_dependencies:
benchmark_harness: ^2.0.0
test: ^1.17.5
89 changes: 89 additions & 0 deletions test/benchmark/rendering_test.dart
@@ -0,0 +1,89 @@
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,
blaugold marked this conversation as resolved.
Show resolved Hide resolved
}) : super('RenderPicture('
'zoom: $zoom, '
'preprocessTile: $preprocessTile'
')');

static Future<void> setupAll() async {
testTile = await readTestTile();
}

static late final VectorTile testTile;
greensopinion marked this conversation as resolved.
Show resolved Hide resolved

final double zoom;
final bool preprocessTile;

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)
: tileset;
}

@override
void run() => _renderPicture(
theme: theme,
tileset: tileset,
scale: 1,
zoom: zoom,
);
}

Future<void> main() async {
await RenderPicture.setupAll();

final benchmarks = [
for (final zoom in <double>[0, 12, 24]) ...[
RenderPicture(zoom: zoom, preprocessTile: false),
RenderPicture(zoom: zoom, preprocessTile: true),
]
];

for (final benchmark in benchmarks) {
benchmark.report();
}
}