Skip to content

Commit cf5eeff

Browse files
authored
Add a method memoizer to dartdoc (#1571)
* New class memoizer for simplifying Dartdoc's caching * Reduce copy-pasting to bare minimum * One more tweak * dartfmt * Add the motivating case * systemTemp was not a constant * Cleanup * Review comments * dartfmt * Eliminate the if statement altogether. * dartfmt
1 parent 97b180c commit cf5eeff

File tree

4 files changed

+309
-22
lines changed

4 files changed

+309
-22
lines changed

lib/src/model_utils.dart

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
library dartdoc.model_utils;
66

7+
import 'dart:collection';
78
import 'dart:convert';
89
import 'dart:io';
910

@@ -12,6 +13,7 @@ import 'package:analyzer/src/generated/engine.dart';
1213
import 'package:analyzer/src/generated/sdk.dart';
1314
import 'package:analyzer/src/generated/source_io.dart';
1415
import 'package:dartdoc/src/model.dart';
16+
import 'package:quiver_hashcode/hashcode.dart';
1517

1618
import 'config.dart';
1719

@@ -166,3 +168,160 @@ String crossdartifySource(
166168
}
167169
return newSource;
168170
}
171+
172+
/// An UnmodifiableListView that computes equality and hashCode based on the
173+
/// equality and hashCode of its contained objects.
174+
class _HashableList extends UnmodifiableListView<dynamic> {
175+
_HashableList(Iterable<dynamic> iterable) : super(iterable);
176+
177+
@override
178+
bool operator ==(other) {
179+
if (other is _HashableList) {
180+
if (this.length == other.length) {
181+
for (var index = 0; index < length; ++index) {
182+
if (this[index] != other[index]) return false;
183+
}
184+
return true;
185+
}
186+
}
187+
return false;
188+
}
189+
190+
@override
191+
get hashCode => hashObjects(this);
192+
}
193+
194+
/// Extend or use as a mixin to track object-specific cached values, or
195+
/// instantiate directly to track other values.
196+
///
197+
/// For all methods in this class, the parameter [f] must be a tear-off method
198+
/// or top level function (not an inline closure) for memoization to work.
199+
/// [Memoizer] depends on the equality operator on the given function to detect
200+
/// when we are calling the same function.
201+
///
202+
/// Use:
203+
///
204+
/// ```dart
205+
/// String aTestFunction(String greeting, String name) => "${greeting}, ${name}";
206+
/// int aSlowFunction() { doSome(); return expensiveCalculations(); }
207+
///
208+
/// myMemoizer.memoized2(aTestFunction, "Hello, "world");
209+
/// myMemoizer.memoized(aSlowFunction);
210+
/// ```
211+
///
212+
/// *Not*:
213+
///
214+
/// ```dart
215+
/// String aTestFunction(String greeting, String name) => "${greeting}, ${name}";
216+
///
217+
/// myMemoizer.memoized2((a, b) => aTestFunction(a, b), "Hello", "world");
218+
/// myMemoizer.memoized(() => aSlowFunction());;
219+
/// ```
220+
class Memoizer {
221+
/// Map of a function and its positional parameters (if any), to a value.
222+
Map<_HashableList, dynamic> _memoizationTable;
223+
224+
Memoizer() {
225+
invalidateMemos();
226+
}
227+
228+
/// Reset the memoization table, forcing calls of the underlying functions.
229+
void invalidateMemos() {
230+
_memoizationTable = new Map();
231+
}
232+
233+
/// Calls and caches the return value of [f]() if not in the cache, then
234+
/// returns the cached value of [f]().
235+
R memoized<R>(Function f) {
236+
_HashableList key = new _HashableList([f]);
237+
return _memoizationTable.putIfAbsent(key, f);
238+
}
239+
240+
/// Calls and caches the return value of [f]([param1]) if not in the cache, then
241+
/// returns the cached value of [f]([param1]).
242+
R memoized1<R, A>(R Function(A) f, A param1) {
243+
_HashableList key = new _HashableList([f, param1]);
244+
return _memoizationTable.putIfAbsent(key, () => f(param1));
245+
}
246+
247+
/// Calls and caches the return value of [f]([param1], [param2]) if not in the
248+
/// cache, then returns the cached value of [f]([param1], [param2]).
249+
R memoized2<R, A, B>(R Function(A, B) f, A param1, B param2) {
250+
_HashableList key = new _HashableList([f, param1, param2]);
251+
return _memoizationTable.putIfAbsent(key, () => f(param1, param2));
252+
}
253+
254+
/// Calls and caches the return value of [f]([param1], [param2], [param3]) if
255+
/// not in the cache, then returns the cached value of [f]([param1],
256+
/// [param2], [param3]).
257+
R memoized3<R, A, B, C>(R Function(A, B, C) f, A param1, B param2, C param3) {
258+
_HashableList key = new _HashableList([f, param1, param2, param3]);
259+
return _memoizationTable.putIfAbsent(key, () => f(param1, param2, param3));
260+
}
261+
262+
/// Calls and caches the return value of [f]([param1], [param2], [param3],
263+
/// [param4]) if not in the cache, then returns the cached value of
264+
/// [f]([param1], [param2], [param3], [param4]).
265+
R memoized4<R, A, B, C, D>(
266+
R Function(A, B, C, D) f, A param1, B param2, C param3, D param4) {
267+
_HashableList key = new _HashableList([f, param1, param2, param3, param4]);
268+
return _memoizationTable.putIfAbsent(
269+
key, () => f(param1, param2, param3, param4));
270+
}
271+
272+
/// Calls and caches the return value of [f]([param1], [param2], [param3],
273+
/// [param4], [param5]) if not in the cache, then returns the cached value of [f](
274+
/// [param1], [param2], [param3], [param4], [param5]).
275+
R memoized5<R, A, B, C, D, E>(R Function(A, B, C, D, E) f, A param1, B param2,
276+
C param3, D param4, E param5) {
277+
_HashableList key =
278+
new _HashableList([f, param1, param2, param3, param4, param5]);
279+
return _memoizationTable.putIfAbsent(
280+
key, () => f(param1, param2, param3, param4, param5));
281+
}
282+
283+
/// Calls and caches the return value of [f]([param1], [param2], [param3],
284+
/// [param4], [param5], [param6]) if not in the cache, then returns the cached
285+
/// value of [f]([param1], [param2], [param3], [param4], [param5], [param6]).
286+
R memoized6<R, A, B, C, D, E, F>(R Function(A, B, C, D, E, F) f, A param1,
287+
B param2, C param3, D param4, E param5, F param6) {
288+
_HashableList key =
289+
new _HashableList([f, param1, param2, param3, param4, param5, param6]);
290+
return _memoizationTable.putIfAbsent(
291+
key, () => f(param1, param2, param3, param4, param5, param6));
292+
}
293+
294+
/// Calls and caches the return value of [f]([param1], [param2], [param3],
295+
/// [param4], [param5], [param6], [param7]) if not in the cache, then returns
296+
/// the cached value of [f]([param1], [param2], [param3], [param4], [param5],
297+
/// [param6], [param7]).
298+
R memoized7<R, A, B, C, D, E, F, G>(R Function(A, B, C, D, E, F, G) f,
299+
A param1, B param2, C param3, D param4, E param5, F param6, G param7) {
300+
_HashableList key = new _HashableList(
301+
[f, param1, param2, param3, param4, param5, param6, param7]);
302+
return _memoizationTable.putIfAbsent(
303+
key, () => f(param1, param2, param3, param4, param5, param6, param7));
304+
}
305+
306+
/// Calls and caches the return value of [f]([param1], [param2], [param3],
307+
/// [param4], [param5], [param6], [param7], [param8]) if not in the cache,
308+
/// then returns the cached value of [f]([param1], [param2], [param3],
309+
/// [param4], [param5], [param6], [param7], [param8]).
310+
R memoized8<R, A, B, C, D, E, F, G, H>(
311+
R Function(A, B, C, D, E, F, G, H) f,
312+
A param1,
313+
B param2,
314+
C param3,
315+
D param4,
316+
E param5,
317+
F param6,
318+
G param7,
319+
H param8) {
320+
_HashableList key = new _HashableList(
321+
[f, param1, param2, param3, param4, param5, param6, param7, param8]);
322+
return _memoizationTable.putIfAbsent(
323+
key,
324+
() =>
325+
f(param1, param2, param3, param4, param5, param6, param7, param8));
326+
}
327+
}

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,4 +374,4 @@ packages:
374374
source: hosted
375375
version: "2.1.13"
376376
sdks:
377-
dart: ">=1.23.0 <=2.0.0-dev.10.0"
377+
dart: ">=1.23.0 <=2.0.0-dev.12.0"

test/model_utils_test.dart

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,83 @@ library dartdoc.model_utils_test;
77
import 'package:dartdoc/src/model_utils.dart';
88
import 'package:test/test.dart';
99

10+
class MemoizerUser extends Memoizer {
11+
int foo = 0;
12+
// These are actually not things you would ordinarily memoize, because
13+
// they change. But they are useful for testing.
14+
int _toMemoize() {
15+
return foo++;
16+
}
17+
18+
int get toMemoize => memoized(_toMemoize);
19+
20+
String _memoizedParameter1(String param) => "${foo++} ${param}";
21+
String memoizedParameter1(String param) =>
22+
memoized1(_memoizedParameter1, param);
23+
24+
String _memoizedParameter2(String param, String param2) =>
25+
"${foo++} ${param} ${param2}";
26+
String memoizedParameter2(String param, String param2) =>
27+
memoized2(_memoizedParameter2, param, param2);
28+
29+
String _memoizedParameter3(String param, String param2, String param3) =>
30+
"${foo++} ${param} ${param2} ${param3}";
31+
String memoizedParameter3(String param, String param2, String param3) =>
32+
memoized3(_memoizedParameter3, param, param2, param3);
33+
34+
String _memoizedParameter4(
35+
String param, String param2, String param3, String param4) =>
36+
"${foo++} ${param} ${param2} ${param3} ${param4}";
37+
String memoizedParameter4(
38+
String param, String param2, String param3, String param4) =>
39+
memoized4(_memoizedParameter4, param, param2, param3, param4);
40+
41+
String _memoizedParameter5(String param, String param2, String param3,
42+
String param4, String param5) =>
43+
"${foo++} ${param} ${param2} ${param3} ${param4} ${param5}";
44+
String memoizedParameter5(String param, String param2, String param3,
45+
String param4, String param5) =>
46+
memoized5(_memoizedParameter5, param, param2, param3, param4, param5);
47+
48+
String _memoizedParameter6(String param, String param2, String param3,
49+
String param4, String param5, String param6) =>
50+
"${foo++} ${param} ${param2} ${param3} ${param4} ${param5} ${param6}";
51+
String memoizedParameter6(String param, String param2, String param3,
52+
String param4, String param5, String param6) =>
53+
memoized6(
54+
_memoizedParameter6, param, param2, param3, param4, param5, param6);
55+
56+
String _memoizedParameter7(String param, String param2, String param3,
57+
String param4, String param5, String param6, String param7) =>
58+
"${foo++} ${param} ${param2} ${param3} ${param4} ${param5} ${param6} ${param7}";
59+
String memoizedParameter7(String param, String param2, String param3,
60+
String param4, String param5, String param6, String param7) =>
61+
memoized7(_memoizedParameter7, param, param2, param3, param4, param5,
62+
param6, param7);
63+
64+
String _memoizedParameter8(
65+
String param,
66+
String param2,
67+
String param3,
68+
String param4,
69+
String param5,
70+
String param6,
71+
String param7,
72+
String param8) =>
73+
"${foo++} ${param} ${param2} ${param3} ${param4} ${param5} ${param6} ${param7} ${param8}";
74+
String memoizedParameter8(
75+
String param,
76+
String param2,
77+
String param3,
78+
String param4,
79+
String param5,
80+
String param6,
81+
String param7,
82+
String param8) =>
83+
memoized8(_memoizedParameter8, param, param2, param3, param4, param5,
84+
param6, param7, param8);
85+
}
86+
1087
void main() {
1188
group('model_utils stripIndentFromSource', () {
1289
test('no indent', () {
@@ -52,4 +129,67 @@ void main() {
52129
'void foo() {\n print(1);\n}\n');
53130
});
54131
});
132+
133+
group('model_utils MethodMemoizer', () {
134+
test('basic memoization and invalidation', () {
135+
var m = new MemoizerUser();
136+
expect(m.toMemoize, equals(0), reason: "initialization problem");
137+
expect(m.toMemoize, equals(0), reason: "failed to memoize");
138+
m.invalidateMemos();
139+
expect(m.toMemoize, equals(1), reason: "failed to invalidate");
140+
});
141+
142+
test('memoization of a method with parameter', () {
143+
var m = new MemoizerUser();
144+
expect(m.memoizedParameter1("hello"), equals("0 hello"),
145+
reason: "initialization problem");
146+
expect(m.memoizedParameter1("hello"), equals("0 hello"),
147+
reason: "failed to memoize");
148+
expect(m.memoizedParameter1("goodbye"), equals("1 goodbye"));
149+
expect(m.memoizedParameter1("something"), equals("2 something"));
150+
m.invalidateMemos();
151+
expect(m.memoizedParameter1("hello"), equals("3 hello"),
152+
reason: "failed to invalidate");
153+
});
154+
155+
test('memoization of many parameters', () {
156+
var m = new MemoizerUser();
157+
expect(m.memoizedParameter1("hello"), equals("0 hello"));
158+
expect(m.memoizedParameter2("hello", "obi"), equals("1 hello obi"));
159+
expect(m.memoizedParameter3("hello", "obi", "wan"),
160+
equals("2 hello obi wan"));
161+
expect(m.memoizedParameter4("hello", "obi", "wan", "how"),
162+
equals("3 hello obi wan how"));
163+
expect(m.memoizedParameter5("hello", "obi", "wan", "how", "are"),
164+
equals("4 hello obi wan how are"));
165+
expect(m.memoizedParameter6("hello", "obi", "wan", "how", "are", "you"),
166+
equals("5 hello obi wan how are you"));
167+
expect(
168+
m.memoizedParameter7(
169+
"hello", "obi", "wan", "how", "are", "you", "doing"),
170+
equals("6 hello obi wan how are you doing"));
171+
expect(
172+
m.memoizedParameter8(
173+
"hello", "obi", "wan", "how", "are", "you", "doing", "today"),
174+
equals("7 hello obi wan how are you doing today"));
175+
expect(m.memoizedParameter1("hello"), equals("0 hello"));
176+
expect(m.memoizedParameter2("hello", "obi"), equals("1 hello obi"));
177+
expect(m.memoizedParameter3("hello", "obi", "wan"),
178+
equals("2 hello obi wan"));
179+
expect(m.memoizedParameter4("hello", "obi", "wan", "how"),
180+
equals("3 hello obi wan how"));
181+
expect(m.memoizedParameter5("hello", "obi", "wan", "how", "are"),
182+
equals("4 hello obi wan how are"));
183+
expect(m.memoizedParameter6("hello", "obi", "wan", "how", "are", "you"),
184+
equals("5 hello obi wan how are you"));
185+
expect(
186+
m.memoizedParameter7(
187+
"hello", "obi", "wan", "how", "are", "you", "doing"),
188+
equals("6 hello obi wan how are you doing"));
189+
expect(
190+
m.memoizedParameter8(
191+
"hello", "obi", "wan", "how", "are", "you", "doing", "today"),
192+
equals("7 hello obi wan how are you doing today"));
193+
});
194+
});
55195
}

tool/grind.dart

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,23 @@ import 'dart:convert';
77
import 'dart:io' hide ProcessException;
88

99
import 'package:dartdoc/src/io_utils.dart';
10+
import 'package:dartdoc/src/model_utils.dart';
1011
import 'package:grinder/grinder.dart';
1112
import 'package:path/path.dart' as path;
1213
import 'package:yaml/yaml.dart' as yaml;
1314

1415
main([List<String> args]) => grind(args);
1516

16-
Directory _dartdocDocsDir;
17-
Directory get dartdocDocsDir {
18-
if (_dartdocDocsDir == null) {
19-
_dartdocDocsDir = Directory.systemTemp.createTempSync('dartdoc');
20-
}
21-
return _dartdocDocsDir;
22-
}
17+
// Directory.systemTemp is not a constant. So wrap it.
18+
Directory createTempSync(String prefix) =>
19+
Directory.systemTemp.createTempSync(prefix);
2320

24-
Directory _sdkDocsDir;
25-
Directory get sdkDocsDir {
26-
if (_sdkDocsDir == null) {
27-
_sdkDocsDir = Directory.systemTemp.createTempSync('sdkdocs');
28-
}
29-
return _sdkDocsDir;
30-
}
21+
final Memoizer tempdirsCache = new Memoizer();
3122

32-
Directory _flutterDir;
33-
Directory get flutterDir {
34-
if (_flutterDir == null) {
35-
_flutterDir = Directory.systemTemp.createTempSync('flutter');
36-
}
37-
return _flutterDir;
38-
}
23+
Directory get dartdocDocsDir =>
24+
tempdirsCache.memoized1(createTempSync, 'dartdoc');
25+
Directory get sdkDocsDir => tempdirsCache.memoized1(createTempSync, 'sdkdocs');
26+
Directory get flutterDir => tempdirsCache.memoized1(createTempSync, 'flutter');
3927

4028
final Directory flutterDirDevTools =
4129
new Directory(path.join(flutterDir.path, 'dev', 'tools'));

0 commit comments

Comments
 (0)