-
-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathtest_ffi.re
364 lines (282 loc) · 13.1 KB
/
test_ffi.re
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
/* This file is part of reason-promise, released under the MIT license. See
LICENSE.md for details, or visit
https://github.com/aantron/promise/blob/master/LICENSE.md. */
let test = Framework.test;
open Promise.PipeFirst;
/* Counts the number of live words in the heap at the time it is called. To get
the number allocated and retained between two points, call this function at
both points, then subtract the two results from each other. */
let countAllocatedWords = () => {
Gc.full_major();
let stat = Gc.stat();
stat.Gc.live_words;
};
/* Checks that loop() does not leak memory. loop() is a function that starts an
asynchronous computation, and the promise it returns resolves with the number
of words allocated in the heap during the computation. loop() is run twice,
the second time for 10 times as many iterations as the first. If the number
of words allocated is not roughly constant, the computation leaks memory.
baseIterations is used to adjust how many iterations to run. Different loops
take different amounts of time, and we don't want to slow down the tests too
much by running a slow loop for too many iterations.
loop must call countAllocatedWords itself. Factoring the call out to this
function doesNotLeakMemory will call countAllocatedWords too late, because
loop will have returned and released all references that it is holding. */
let doesNotLeakMemory = (loop, baseIterations) =>
Promise.Js.flatMap(loop(baseIterations), wordsAllocated =>
Promise.Js.flatMap(loop(baseIterations * 10), wordsAllocated' => {
let ratio = float_of_int(wordsAllocated') /. float_of_int(wordsAllocated);
Promise.Js.resolved(ratio < 2.);
}));
let promiseLoopTests = Framework.suite("promise loop", [
/* A pretty simple promise loop. This is just a function that takes a promise,
and calls .flatMap on it. The callback passed to .flatMap calls the loop
recursively, passing another promise to the next iteration. The interesting
part is not the argument promise, but the result promise returned by each
iteration.
If Promise is implemented naively, the iteration will result in a big
chain of promises hanging in memory: a memory leak. Here is how:
- At the first iteration, .flatMap creates an outer pending promise p0, and
returns it immediately to the rest of the code.
- Later, the callback passed to .flatMap runs. It again calls .flatMap,
creating another pending promise p1. The callback then returns p1. This
means that resolving p1 should resolve p0, so a naive implementation
will store a reference in p1 to p0.
- Later, the callback passed to p1's .flatMap runs, doing the same thing:
creating another pending promise p2, pointing to p1.
- By iteration N, there is a chain of N pending promises set up, such that
resolving the inner-most promise in the chain, created by the last
.flatMap, will resolve the outer-most promise p0, created by the first
.flatMap. This is the memory leak. */
test("promise loop memory leak", () => {
let instrumentedPromiseLoop = n => {
let initialWords = countAllocatedWords();
let rec promiseLoop: Promise.t(int) => Promise.t(int) =
previousPromise =>
Promise.flatMap(previousPromise, n => {
if (n == 0) {
let wordsAllocated = countAllocatedWords() - initialWords;
Promise.resolved(wordsAllocated);
}
else {
promiseLoop(Promise.resolved(n - 1))
}});
promiseLoop(Promise.resolved(n));
};
doesNotLeakMemory(instrumentedPromiseLoop, 1000);
}),
/* The fix for the above memory leak carries a potential pitfall: the fix is
to merge the inner promise returned to flatMap into flatMap's outer promise.
After that, all operations on the inner promise reference are actually
performed on the outer promise.
This carries the danger that a tower of these merged promises can build
up. If a pending promise is repeatedly returned to flatMap, it will
gradually become the head of a growing chain of forwarding promises, that
point to the outer promise created in the last call to flatMap.
To avoid this, the implementation has to perform union-find: each time it
traverses a chain of merged promises, it has to set the head promise to
point directly to the final outer promise, cutting out all intermediate
merged promises. Then, any of these merged promises that aren't being
referenced by the user program can be garbage-collected. */
test("promise tower memory leak", () => {
let instrumentedPromiseTower = n => {
let (foreverPendingPromise, _) = Promise.pending();
let initialWords = countAllocatedWords();
let rec tryToBuildTower = n =>
if (n == 0) {
let wordsAllocated = countAllocatedWords() - initialWords;
Promise.resolved(wordsAllocated);
}
else {
/* The purpose of the delay promise is to make sure the second call to
flatMap runs after the first. */
let delay = Promise.resolved();
/* If union-find is not implemented, we will leak memory here. */
ignore(Promise.flatMap(delay, () => foreverPendingPromise));
Promise.flatMap(delay, () => tryToBuildTower(n - 1));
};
tryToBuildTower(n);
};
doesNotLeakMemory(instrumentedPromiseTower, 1000);
}),
]);
/* The skeleton of a test for memory safety of Promise.race. Creates a
long-lived promise, and repeatedly calls the body function on it, which is
customized by each test. */
let raceTest = (name, body) =>
test(name, () => {
let instrumentedLoop = n => {
let (foreverPendingPromise, _, _) = Promise.Js.pending();
let initialWords = countAllocatedWords();
let rec theLoop: int => Promise.Js.t(int, unit) = n =>
if (n == 0) {
let wordsAllocated = countAllocatedWords() - initialWords;
Promise.Js.resolved(wordsAllocated);
}
else {
let nextIteration = () => theLoop(n - 1);
body(foreverPendingPromise, nextIteration);
};
theLoop(n);
};
Promise.Js.catch(
doesNotLeakMemory(instrumentedLoop, 100),
() => assert(false));
});
let raceLoopTests = Framework.suite("race loop", [
/* To implement p3 = Promise.race([p1, p2]), Promise has to attach
callbacks to p1 and p2, so that whichever of them is the first to resolve
will cause the resolution of p3. This means that p1 and p2 hold references
to p3.
If, say, p1 is a promise that remains pending for a really long time, and
it is raced with many other promises in a loop, i.e.
p3 = Promise.race([p1, p2])
p3' = Promise.race([p1, p2'])
etc.
Then p1 will accumulate callbacks with references to p3, p3', etc. This
will be a memory leak, that grows in proportion to the number of times the
race loop has run.
Since this is a common usage pattern, a reasonable implementation has to
remove callbacks from p1 when p3, p3', etc. are resolved by race. This
test checks for such an implementation. */
raceTest("race loop memory leak", (foreverPendingPromise, nextIteration) => {
let (shortLivedPromise, resolveShortLivedPromise, _) =
Promise.Js.pending();
let racePromise =
Promise.Js.race([foreverPendingPromise, shortLivedPromise]);
resolveShortLivedPromise();
Promise.Js.flatMap(racePromise, nextIteration);
}),
raceTest("race loop memory leak, with already-resolved promises",
(foreverPendingPromise, nextIteration) => {
let resolvedPromise = Promise.Js.resolved();
let racePromise =
Promise.Js.race([foreverPendingPromise, resolvedPromise]);
Promise.Js.flatMap(racePromise, nextIteration);
}),
raceTest("race loop memory leak, with rejection",
(foreverPendingPromise, nextIteration) => {
let (shortLivedPromise, _, rejectShortLivedPromise) =
Promise.Js.pending();
let racePromise =
Promise.Js.race([foreverPendingPromise, shortLivedPromise]);
rejectShortLivedPromise();
Promise.Js.flatMap(racePromise, () => assert(false))
->Promise.Js.catch(nextIteration);
}),
raceTest("race loop memory leak, with already-rejected promises",
(foreverPendingPromise, nextIteration) => {
let rejectedPromise = Promise.Js.rejected();
let racePromise =
Promise.Js.race([foreverPendingPromise, rejectedPromise]);
Promise.Js.flatMap(racePromise, () => assert(false))
->Promise.Js.catch(nextIteration);
}),
/* This test is like the first, but it tests for the interaction of the fixes
for the flatMap and race loop memory leaks. The danger is:
- The flatMap fix "wants" to merge callback lists when an inner pending
promise is returned from the callback of flatMap.
- The race fix "wants" to delete callbacks from a callback list, when a
promise "loses" to another one that resolved sooner.
It is important that the callback list merging performed by flatMap doesn't
prevent race from finding and deleting the correct callbacks in the merged
lists. */
raceTest("race loop memory leak with flatMap merging",
(foreverPendingPromise, nextIteration) => {
let (shortLivedPromise, resolveShortLivedPromise, _) =
Promise.Js.pending();
let racePromise =
Promise.Js.race([foreverPendingPromise, shortLivedPromise]);
/* Return foreverPendingPromise from the callback of flatMap. This causes all
of its callbacks to be moved to the outer promise of the flatMap (which we
don't give a name to). The delay promise is just used to make the second
call to flatMap definitely run after the first. */
let delay = Promise.Js.resolved();
ignore(Promise.Js.flatMap(delay, () => foreverPendingPromise));
Promise.Js.flatMap(delay, () => {
/* Now, we resolve the short-lived promise. If that doesn't delete the
callback that was merged away from foreverPendingPromise, then this is
where we will accumulate the memory leak. */
resolveShortLivedPromise();
Promise.Js.flatMap(racePromise, nextIteration);
});
}),
]);
let allLoopTests = Framework.suite("all loop", [
/* Like Promise.race, there is a danger of memory leak in Promise.all.
When one of the promises in Promise.all is rejected, the final promise is
rejected immediately. If callbacks attached to still-pending promises are
not removed, a memory leak will accumulate.
We reuse the raceTest helper, because the tests are structurally the same.
race remains the function with the most opportunities to leak memory. */
raceTest("all loop memory leak", (foreverPendingPromise, nextIteration) => {
let (shortLivedPromise, _, rejectShortLivedPromise) =
Promise.Js.pending();
let allPromise =
Promise.Js.all([foreverPendingPromise, shortLivedPromise]);
rejectShortLivedPromise();
Promise.Js.flatMap(allPromise, (_) => assert false)
->Promise.Js.catch(nextIteration);
}),
raceTest("all loop memory leak, with already-rejected promises",
(foreverPendingPromise, nextIteration) => {
let rejectedPromise = Promise.Js.rejected();
let allPromise =
Promise.Js.all([foreverPendingPromise, rejectedPromise]);
Promise.Js.flatMap(allPromise, (_) => assert false)
->Promise.Js.catch(nextIteration);
}),
/* Tests the interaction of the memory-leak fixes in all and flatMap, as tested
for race and flatMap above. */
raceTest("race loop memory leak with flatMap merging",
(foreverPendingPromise, nextIteration) => {
let (shortLivedPromise, _, rejectShortLivedPromise) =
Promise.Js.pending();
let allPromise =
Promise.Js.all([foreverPendingPromise, shortLivedPromise]);
let delay = Promise.Js.resolved();
let p = delay->Promise.Js.catch((_) => assert(false));
ignore(Promise.Js.flatMap(p, () => foreverPendingPromise));
let p = delay->Promise.Js.catch((_) => assert(false));
Promise.Js.flatMap(p, () => {
rejectShortLivedPromise();
Promise.Js.flatMap(allPromise, (_) => assert false)
->Promise.Js.catch(nextIteration);
});
}),
]);
let letTests = Framework.suite("let", Promise.Let.[
test("basic", () => {
let.promise n = Promise.resolved(42);
Promise.resolved(n == 42);
}),
test("sequence", () => {
let.promise n = Promise.resolved(42);
let.promise n' = Promise.resolved(n + 1);
Promise.resolved(n == 42 && n' == 43);
}),
test("map", () => {
let.promise_map n = Promise.resolved(42);
n == 42
}),
/*
test("map sequence", () => {
let.promise_map n = Promise.resolved(42);
let.promise_map n' = Promise.resolved(43);
n == 42 && n' == 43
}),
*/
test("flatMap then map", () => {
let.promise n = Promise.resolved(42);
let.promise_map n' = Promise.resolved(n + 1);
n == 42 && n' == 43;
}),
/*
test("flatMap then map", () => {
let.promise_map n = Promise.resolved(42);
let.promise n' = Promise.resolved(n + 1);
Promise.resolved(n == 42 && n' == 43);
}),
*/
]);
let suites = [promiseLoopTests, raceLoopTests, allLoopTests, letTests];