From ea04c910e734ac547ba7b24bfd6d310bf4113399 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 21 Mar 2016 22:07:47 -0700 Subject: [PATCH] Replace use of LruMap with a pure LinkedHashMap ...so that we can shed the quiver dependency in flutter's framework. --- .../flutter/lib/src/services/image_cache.dart | 49 ++++++++--- packages/flutter/pubspec.yaml | 1 - .../services/image_cache_resize_test.dart | 41 +++++++++ .../test/services/image_cache_test.dart | 83 +++++++++++++++++++ .../test/services/mocks_for_image_cache.dart | 48 +++++++++++ 5 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 packages/flutter/test/services/image_cache_resize_test.dart create mode 100644 packages/flutter/test/services/image_cache_test.dart create mode 100644 packages/flutter/test/services/mocks_for_image_cache.dart diff --git a/packages/flutter/lib/src/services/image_cache.dart b/packages/flutter/lib/src/services/image_cache.dart index 9762f840727..4141210ae9f 100644 --- a/packages/flutter/lib/src/services/image_cache.dart +++ b/packages/flutter/lib/src/services/image_cache.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'dart:ui' show hashValues; import 'package:mojo/mojo/url_response.mojom.dart'; -import 'package:quiver/collection.dart'; import 'fetch.dart'; import 'image_decoder.dart'; @@ -25,6 +25,12 @@ import 'image_resource.dart'; /// share the same cache as all the other image loading codepaths that used the /// [imageCache]. abstract class ImageProvider { // ignore: one_member_abstracts + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const ImageProvider(); + + /// Subclasses must implement this method by having it asynchronously return + /// an [ImageInfo] that represents the image provided by this [ImageProvider]. Future loadImage(); /// Subclasses must implement the `==` operator so that the image cache can @@ -88,22 +94,34 @@ const int _kDefaultSize = 1000; class ImageCache { ImageCache._(); - final LruMap _cache = - new LruMap(maximumSize: _kDefaultSize); + final LinkedHashMap _cache = + new LinkedHashMap(); /// Maximum number of entries to store in the cache. /// /// Once this many entries have been cached, the least-recently-used entry is /// evicted when adding a new entry. - int get maximumSize => _cache.maximumSize; + int get maximumSize => _maximumSize; + int _maximumSize = _kDefaultSize; /// Changes the maximum cache size. /// /// If the new size is smaller than the current number of elements, the /// extraneous elements are evicted immediately. Setting this to zero and then /// returning it to its original value will therefore immediately clear the - /// cache. However, doing this is not very efficient. - // (the quiver library does it one at a time rather than using clear()) - void set maximumSize(int value) { _cache.maximumSize = value; } + /// cache. + void set maximumSize(int value) { + assert(value != null); + assert(value >= 0); + if (value == maximumSize) + return; + _maximumSize = value; + if (maximumSize == 0) { + _cache.clear(); + } else { + while (_cache.length > maximumSize) + _cache.remove(_cache.keys.first); + } + } /// Calls the [ImageProvider.loadImage] method on the given image provider, if /// necessary, and returns an [ImageResource] that encapsulates a [Future] for @@ -113,9 +131,20 @@ class ImageCache { /// cache, then the [ImageResource] object is immediately usable and the /// provider is not invoked. ImageResource loadProvider(ImageProvider provider) { - return _cache.putIfAbsent(provider, () { - return new ImageResource(provider.loadImage()); - }); + ImageResource result = _cache[provider]; + if (result != null) { + _cache.remove(provider); + } else { + if (_cache.length == maximumSize && maximumSize > 0) + _cache.remove(_cache.keys.first); + result = new ImageResource(provider.loadImage());; + } + if (maximumSize > 0) { + assert(_cache.length < maximumSize); + _cache[provider] = result; + } + assert(_cache.length <= maximumSize); + return result; } /// Fetches the given URL, associating it with the given scale. diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index fdd9eafaed7..1c0dd54fd02 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -8,7 +8,6 @@ dependencies: collection: '>=1.4.0 <2.0.0' intl: '>=0.12.4+2 <0.13.0' vector_math: '>=1.4.5 <2.0.0' - quiver: '>=0.21.4 <0.22.0' sky_engine: path: ../../bin/cache/pkg/sky_engine diff --git a/packages/flutter/test/services/image_cache_resize_test.dart b/packages/flutter/test/services/image_cache_resize_test.dart new file mode 100644 index 00000000000..f7545e6d3f8 --- /dev/null +++ b/packages/flutter/test/services/image_cache_resize_test.dart @@ -0,0 +1,41 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:test/test.dart'; + +import 'mocks_for_image_cache.dart'; + +void main() { + test('Image cache resizing', () async { + + imageCache.maximumSize = 2; + + TestImageInfo a = (await imageCache.loadProvider(new TestProvider(1, 1)).first); + TestImageInfo b = (await imageCache.loadProvider(new TestProvider(2, 2)).first); + TestImageInfo c = (await imageCache.loadProvider(new TestProvider(3, 3)).first); + TestImageInfo d = (await imageCache.loadProvider(new TestProvider(1, 4)).first); + expect(a.value, equals(1)); + expect(b.value, equals(2)); + expect(c.value, equals(3)); + expect(d.value, equals(4)); + + imageCache.maximumSize = 0; + + TestImageInfo e = (await imageCache.loadProvider(new TestProvider(1, 5)).first); + expect(e.value, equals(5)); + + TestImageInfo f = (await imageCache.loadProvider(new TestProvider(1, 6)).first); + expect(f.value, equals(6)); + + imageCache.maximumSize = 3; + + TestImageInfo g = (await imageCache.loadProvider(new TestProvider(1, 7)).first); + expect(g.value, equals(7)); + + TestImageInfo h = (await imageCache.loadProvider(new TestProvider(1, 8)).first); + expect(h.value, equals(7)); + + }); +} diff --git a/packages/flutter/test/services/image_cache_test.dart b/packages/flutter/test/services/image_cache_test.dart new file mode 100644 index 00000000000..04bc05b7fe0 --- /dev/null +++ b/packages/flutter/test/services/image_cache_test.dart @@ -0,0 +1,83 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:test/test.dart'; + +import 'mocks_for_image_cache.dart'; + +void main() { + test('Image cache', () async { + + imageCache.maximumSize = 3; + + TestImageInfo a = (await imageCache.loadProvider(new TestProvider(1, 1)).first); + expect(a.value, equals(1)); + TestImageInfo b = (await imageCache.loadProvider(new TestProvider(1, 2)).first); + expect(b.value, equals(1)); + TestImageInfo c = (await imageCache.loadProvider(new TestProvider(1, 3)).first); + expect(c.value, equals(1)); + TestImageInfo d = (await imageCache.loadProvider(new TestProvider(1, 4)).first); + expect(d.value, equals(1)); + TestImageInfo e = (await imageCache.loadProvider(new TestProvider(1, 5)).first); + expect(e.value, equals(1)); + TestImageInfo f = (await imageCache.loadProvider(new TestProvider(1, 6)).first); + expect(f.value, equals(1)); + + expect(f, equals(a)); + + // cache still only has one entry in it: 1(1) + + TestImageInfo g = (await imageCache.loadProvider(new TestProvider(2, 7)).first); + expect(g.value, equals(7)); + + // cache has two entries in it: 1(1), 2(7) + + TestImageInfo h = (await imageCache.loadProvider(new TestProvider(1, 8)).first); + expect(h.value, equals(1)); + + // cache still has two entries in it: 2(7), 1(1) + + TestImageInfo i = (await imageCache.loadProvider(new TestProvider(3, 9)).first); + expect(i.value, equals(9)); + + // cache has three entries in it: 2(7), 1(1), 3(9) + + TestImageInfo j = (await imageCache.loadProvider(new TestProvider(1, 10)).first); + expect(j.value, equals(1)); + + // cache still has three entries in it: 2(7), 3(9), 1(1) + + TestImageInfo k = (await imageCache.loadProvider(new TestProvider(4, 11)).first); + expect(k.value, equals(11)); + + // cache has three entries: 3(9), 1(1), 4(11) + + TestImageInfo l = (await imageCache.loadProvider(new TestProvider(1, 12)).first); + expect(l.value, equals(1)); + + // cache has three entries: 3(9), 4(11), 1(1) + + TestImageInfo m = (await imageCache.loadProvider(new TestProvider(2, 13)).first); + expect(m.value, equals(13)); + + // cache has three entries: 4(11), 1(1), 2(13) + + TestImageInfo n = (await imageCache.loadProvider(new TestProvider(3, 14)).first); + expect(n.value, equals(14)); + + // cache has three entries: 1(1), 2(13), 3(14) + + TestImageInfo o = (await imageCache.loadProvider(new TestProvider(4, 15)).first); + expect(o.value, equals(15)); + + // cache has three entries: 2(13), 3(14), 4(15) + + TestImageInfo p = (await imageCache.loadProvider(new TestProvider(1, 16)).first); + expect(p.value, equals(16)); + + // cache has three entries: 3(14), 4(15), 1(16) + + }); +} diff --git a/packages/flutter/test/services/mocks_for_image_cache.dart b/packages/flutter/test/services/mocks_for_image_cache.dart new file mode 100644 index 00000000000..8f0f665cfe1 --- /dev/null +++ b/packages/flutter/test/services/mocks_for_image_cache.dart @@ -0,0 +1,48 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:ui' as ui show Image; + +import 'package:flutter/services.dart'; + +class TestImageInfo implements ImageInfo { + const TestImageInfo(this.value) : image = null, scale = null; + + @override + final ui.Image image; // ignored in test + + @override + final double scale; // ignored in test + + final int value; + + @override + String toString() => '$runtimeType($value)'; +} + +class TestProvider extends ImageProvider { + const TestProvider(this.equalityValue, this.imageValue); + final int imageValue; + final int equalityValue; + + @override + Future loadImage() async { + return new TestImageInfo(imageValue); + } + + @override + bool operator ==(dynamic other) { + if (other is! TestProvider) + return false; + final TestProvider typedOther = other; + return equalityValue == typedOther.equalityValue; + } + + @override + int get hashCode => equalityValue.hashCode; + + @override + String toString() => '$runtimeType($equalityValue, $imageValue)'; +}