Skip to content

Commit

Permalink
introduce tile streaming
Browse files Browse the repository at this point in the history
extract tile data logic and caching from the tile model
and painter. this separation of concerns makes it easier
to tune behaviour, since it's more explicit, and to improve
performance.

introduced memory caching for vector tiles since they now
consume far less memory

improved heuristics used to provide tile data so that there
is less delay in rendering tiles
  • Loading branch information
greensopinion committed Jan 7, 2022
1 parent 80faf93 commit bb955ab
Show file tree
Hide file tree
Showing 21 changed files with 543 additions and 279 deletions.
85 changes: 85 additions & 0 deletions lib/src/cache/cache.dart
@@ -0,0 +1,85 @@
import 'dart:collection';

import 'cache_stats.dart';

class Sizer<V> {
int size(V value) => 1;
}

class Copier<V> {
V? copy(V? value) => value;
void dispose(V? value) {}
}

class Cache<K, V> with CacheStats {
int maxSize;
int _currentSize = 0;
bool _disposed = false;
final Sizer _sizer;
final Copier _copier;

final LinkedHashMap<K, V> _cache = LinkedHashMap();

Cache({required this.maxSize, required Sizer sizer, required Copier copier})
: _sizer = sizer,
_copier = copier;

int get size => _currentSize;

void remove(String key) {
_copier.dispose(_cache.remove(key));
}

V? get(K key) {
var value = _cache.remove(key);
if (value != null) {
_cache[key] = value;
cacheHit();
} else {
cacheMiss();
}
return _copier.copy(value);
}

void put(K key, V newValue) {
if (_disposed) {
return;
}
var previousValue = _cache.remove(key);
if (previousValue != null) {
_copier.dispose(previousValue);
_currentSize -= _sizer.size(previousValue);
}
_cache[key] = _copier.copy(newValue);
_currentSize += _sizer.size(newValue);
_applyConstraints();
}

void _applyConstraints() {
_remove(() => _currentSize > maxSize && _cache.isNotEmpty);
}

void didHaveMemoryPressure() {
maxSize = maxSize ~/ 2;
clear();
}

void dispose() {
_disposed = true;
clear();
}

void clear() {
_remove(() => _cache.isNotEmpty);
}

void _remove(bool Function() condition) {
while (condition()) {
final removed = _cache.remove(_cache.keys.first);
if (removed != null) {
_copier.dispose(removed);
_currentSize -= _sizer.size(removed);
}
}
}
}
11 changes: 10 additions & 1 deletion lib/src/cache/caches.dart
Expand Up @@ -10,12 +10,14 @@ import 'memory_image_cache.dart';
import 'storage_cache.dart';
import 'tile_image_cache.dart';
import 'vector_tile_loading_cache.dart';
import 'vector_tile_memory_cache.dart';

class Caches {
final ByteStorage _storage = ByteStorage(
pather: () => getTemporaryDirectory()
.then((value) => Directory('${value.path}/.vector_map')));
late final StorageCache _cache;
late final VectorTileMemoryCache memoryVectorTileCache;
late final VectorTileLoadingCache vectorTileCache;
late final ImageTileLoadingCache imageTileCache;
late final MemoryImageCache memoryImageCache;
Expand All @@ -25,11 +27,14 @@ class Caches {
{required TileProviders providers,
required RendererPipeline pipeline,
required Duration ttl,
required int maxTilesInMemory,
required int maxImagesInMemory,
required int maxSizeInBytes}) {
providerSources = providers.tileProviderBySource.keys.toList();
_cache = StorageCache(_storage, ttl, maxSizeInBytes);
vectorTileCache = VectorTileLoadingCache(_cache, providers);
memoryVectorTileCache = VectorTileMemoryCache(maxTilesInMemory);
vectorTileCache =
VectorTileLoadingCache(_cache, memoryVectorTileCache, providers);
imageTileCache = ImageTileLoadingCache(TileImageCache(_cache), pipeline);
memoryImageCache = MemoryImageCache(maxImagesInMemory);
}
Expand All @@ -38,16 +43,20 @@ class Caches {

void dispose() {
memoryImageCache.dispose();
memoryVectorTileCache.dispose();
}

void didHaveMemoryPressure() {
memoryVectorTileCache.didHaveMemoryPressure();
memoryImageCache.didHaveMemoryPressure();
}

String stats() {
final cacheStats = <String>[];
cacheStats
.add('Storage cache hit ratio: ${_cache.hitRatio.asPct()}%');
cacheStats.add(
'Vector tile cache hit ratio: ${memoryVectorTileCache.hitRatio.asPct()}%');
cacheStats.add(
'Image tile cache hit ratio: ${imageTileCache.hitRatio.asPct()}%');
cacheStats.add(
Expand Down
8 changes: 4 additions & 4 deletions lib/src/cache/image_tile_loading_cache.dart
Expand Up @@ -17,7 +17,7 @@ class ImageTileLoadingCache with CacheStats {
double get scale => _pipeline.scale;

Future<Image> retrieve(TileIdentity identity, Tileset tileset,
{required double zoom}) async {
{required int zoom}) async {
final key = _key(identity, zoom: zoom);
var future = _futuresByKey[key];
if (future == null) {
Expand All @@ -39,7 +39,7 @@ class ImageTileLoadingCache with CacheStats {
}

Future<Image> _load(TileIdentity identity, Tileset tileset,
{required double zoom}) async {
{required int zoom}) async {
final modifier = _toModifier(zoom);
final image = await delegate.retrieve(identity, modifier);
if (image == null) {
Expand All @@ -53,11 +53,11 @@ class ImageTileLoadingCache with CacheStats {
return image;
}

String _key(TileIdentity identity, {required double zoom}) {
String _key(TileIdentity identity, {required int zoom}) {
return '${identity.z}_${identity.x}_${identity.y}_$zoom';
}

String _toModifier(double zoom) {
String _toModifier(int zoom) {
return '${scale}_${zoom}_${_pipeline.theme.id}_${_pipeline.theme.version}';
}
}
Expand Down
59 changes: 8 additions & 51 deletions lib/src/cache/memory_cache.dart
@@ -1,56 +1,13 @@
import 'dart:collection';
import 'dart:typed_data';

import 'cache_stats.dart';
import 'cache.dart';

class MemoryCache with CacheStats {
int maxSizeBytes;
int _currentSizeBytes = 0;
final LinkedHashMap<String, Uint8List> _cache = LinkedHashMap();

MemoryCache({required this.maxSizeBytes});

int get sizeInBytes => _currentSizeBytes;

void removeItem(String key) {
_cache.remove(key);
}

Uint8List? getItem(String key) {
var value = _cache.remove(key);
if (value != null) {
_cache[key] = value;
cacheHit();
} else {
cacheMiss();
}
return value;
}

void putItem(String key, Uint8List bytes) {
var value = _cache.remove(key);
if (value != null) {
_currentSizeBytes -= value.lengthInBytes;
}
_cache[key] = bytes;
_currentSizeBytes += bytes.lengthInBytes;
_applyConstraints();
}

void _applyConstraints() {
while (_currentSizeBytes > maxSizeBytes && _cache.isNotEmpty) {
final removed = _cache.remove(_cache.keys.first);
_currentSizeBytes -= removed!.lengthInBytes;
}
}

void didHaveMemoryPressure() {
maxSizeBytes = maxSizeBytes ~/ 2;
clear();
}
class MemoryCache extends Cache<String, Uint8List> {
MemoryCache({required int maxSizeBytes})
: super(maxSize: maxSizeBytes, sizer: _Sizer(), copier: Copier());
}

void clear() {
_cache.clear();
_currentSizeBytes = 0;
}
class _Sizer extends Sizer<Uint8List> {
@override
int size(Uint8List value) => value.lengthInBytes;
}
77 changes: 21 additions & 56 deletions lib/src/cache/memory_image_cache.dart
@@ -1,66 +1,31 @@
import 'dart:collection';

import 'dart:ui';

import '../tile_identity.dart';
import 'cache_stats.dart';

class MemoryImageCache with CacheStats {
int _maxSize;
final _cache = LinkedHashMap<String, Image>();
bool _disposed = false;

MemoryImageCache(this._maxSize);
import 'cache.dart';

void putImage(TileIdentity id, {required double zoom, required Image image}) {
if (_disposed) {
return;
}
final key = _toKey(id, zoom);
_cache.remove(key)?.dispose();
_cache[key] = image.clone();
_applyMaxSize();
}
class ImageKey {
final TileIdentity id;
final int zoom;
ImageKey(this.id, this.zoom);

Image? getImage(TileIdentity id, {required double zoom}) {
if (_disposed) {
return null;
}
final key = _toKey(id, zoom);
final image = _cache.remove(key);
if (image != null) {
cacheHit();
_cache[key] = image;
return image.clone();
} else {
cacheMiss();
}
}
@override
operator ==(o) => o is ImageKey && o.id == id && o.zoom == zoom;

void _applyMaxSize() {
while (_cache.length > _maxSize) {
final oldest = _cache.keys.first;
final removed = _cache.remove(oldest)!;
removed.dispose();
}
}
@override
int get hashCode => hashValues(id, zoom);

void didHaveMemoryPressure() {
_clear();
_maxSize = _maxSize ~/ 2;
}

void dispose() {
_clear();
}
@override
String toString() => 'ImageKey(id=$id,zoom=$zoom)';
}

void _clear() {
_cache.values.forEach((image) {
image.dispose();
});
_cache.clear();
}
class MemoryImageCache extends Cache<ImageKey, Image> {
MemoryImageCache(int maxSize)
: super(maxSize: maxSize, copier: _Copier(), sizer: Sizer());
}

String _toKey(TileIdentity id, double zoom) =>
'${id.z}.${id.x}.${id.y}.$zoom';
class _Copier extends Copier<Image> {
@override
Image? copy(Image? value) => value?.clone();
@override
void dispose(Image? value) => value?.dispose();
}
7 changes: 6 additions & 1 deletion lib/src/cache/tile_image_cache.dart
@@ -1,14 +1,19 @@
import 'dart:typed_data';
import 'dart:ui';

import 'storage_cache.dart';
import '../tile_identity.dart';
import 'storage_cache.dart';

class TileImageCache {
final StorageCache _delegate;

TileImageCache(this._delegate);

Future<bool> contains(TileIdentity tile, String modifier) async {
final key = _toKey(tile, modifier);
return await _delegate.exists(key);
}

Future<Image?> retrieve(TileIdentity tile, String modifier) async {
final key = _toKey(tile, modifier);

Expand Down

0 comments on commit bb955ab

Please sign in to comment.