Skip to content

Commit 99c27a5

Browse files
dop251tsedgwick
authored andcommitted
Changed WeakMap implementation to avoid memory leaks in some common usage scenarios. Fixes dop251#250.
1 parent 914821b commit 99c27a5

8 files changed

+80
-283
lines changed

README.md

+20-8
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,31 @@ Known incompatibilities and caveats
2727
-----------------------------------
2828

2929
### WeakMap
30-
WeakMap maintains "hard" references to its values. This means if a value references a key in a WeakMap or a WeakMap
31-
itself, it will not be garbage-collected until the WeakMap becomes unreferenced. To illustrate this:
30+
WeakMap is implemented by embedding references to the values into the keys. This means that as long
31+
as the key is reachable all values associated with it in any weak maps also remain reachable and therefore
32+
cannot be garbage collected even if they are not otherwise referenced, even after the WeakMap is gone.
33+
The reference to the value is dropped either when the key is explicitly removed from the WeakMap or when the
34+
key becomes unreachable.
3235

33-
```go
36+
To illustrate this:
37+
38+
```javascript
3439
var m = new WeakMap();
3540
var key = {};
36-
m.set(key, {key: key});
37-
// or m.set(key, key);
38-
key = undefined; // The value will NOT become garbage-collectable at this point
39-
m = undefined; // But it will at this point.
41+
var value = {/* a very large object */};
42+
m.set(key, value);
43+
value = undefined;
44+
m = undefined; // The value does NOT become garbage-collectable at this point
45+
key = undefined; // Now it does
46+
// m.delete(key); // This would work too
4047
```
4148

42-
Note, this does not have any effect on the application logic, but causes a higher-than-expected memory usage.
49+
The reason for it is the limitation of the Go runtime. At the time of writing (version 1.15) having a finalizer
50+
set on an object which is part of a reference cycle makes the whole cycle non-garbage-collectable. The solution
51+
above is the only reasonable way I can think of without involving finalizers. This is the third attempt
52+
(see https://github.com/dop251/goja/issues/250 and https://github.com/dop251/goja/issues/199 for more details).
53+
54+
Note, this does not have any effect on the application logic, but may cause a higher-than-expected memory usage.
4355

4456
FAQ
4557
---

builtin_weakmap.go

+14-42
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,35 @@
11
package goja
22

3-
type weakMap struct {
4-
data map[uint64]Value
5-
}
3+
type weakMap uint64
64

75
type weakMapObject struct {
86
baseObject
9-
m *weakMap
10-
}
11-
12-
func newWeakMap() *weakMap {
13-
return &weakMap{
14-
data: make(map[uint64]Value),
15-
}
7+
m weakMap
168
}
179

1810
func (wmo *weakMapObject) init() {
1911
wmo.baseObject.init()
20-
wmo.m = newWeakMap()
21-
}
22-
23-
func (wm *weakMap) removeId(id uint64) {
24-
delete(wm.data, id)
12+
wmo.m = weakMap(wmo.val.runtime.genId())
2513
}
2614

27-
func (wm *weakMap) set(key *Object, value Value) {
28-
ref := key.getWeakRef()
29-
wm.data[ref.id] = value
30-
key.runtime.addWeakKey(ref.id, wm)
15+
func (wm weakMap) set(key *Object, value Value) {
16+
key.getWeakRefs()[wm] = value
3117
}
3218

33-
func (wm *weakMap) get(key *Object) Value {
34-
ref := key.weakRef
35-
if ref == nil {
36-
return nil
37-
}
38-
ret := wm.data[ref.id]
39-
return ret
19+
func (wm weakMap) get(key *Object) Value {
20+
return key.weakRefs[wm]
4021
}
4122

42-
func (wm *weakMap) remove(key *Object) bool {
43-
ref := key.weakRef
44-
if ref == nil {
45-
return false
46-
}
47-
_, exists := wm.data[ref.id]
48-
if exists {
49-
delete(wm.data, ref.id)
50-
key.runtime.removeWeakKey(ref.id, wm)
23+
func (wm weakMap) remove(key *Object) bool {
24+
if _, exists := key.weakRefs[wm]; exists {
25+
delete(key.weakRefs, wm)
26+
return true
5127
}
52-
return exists
28+
return false
5329
}
5430

55-
func (wm *weakMap) has(key *Object) bool {
56-
ref := key.weakRef
57-
if ref == nil {
58-
return false
59-
}
60-
_, exists := wm.data[ref.id]
31+
func (wm weakMap) has(key *Object) bool {
32+
_, exists := key.weakRefs[wm]
6133
return exists
6234
}
6335

builtin_weakmap_test.go

+16-11
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
package goja
22

33
import (
4-
"runtime"
54
"testing"
65
)
76

8-
func TestWeakMapExpiry(t *testing.T) {
7+
func TestWeakMap(t *testing.T) {
98
vm := New()
109
_, err := vm.RunString(`
1110
var m = new WeakMap();
11+
var m1 = new WeakMap();
1212
var key = {};
1313
m.set(key, true);
14+
m1.set(key, false);
1415
if (!m.has(key)) {
1516
throw new Error("has");
1617
}
1718
if (m.get(key) !== true) {
1819
throw new Error("value does not match");
1920
}
20-
key = undefined;
21+
if (!m1.has(key)) {
22+
throw new Error("has (m1)");
23+
}
24+
if (m1.get(key) !== false) {
25+
throw new Error("m1 value does not match");
26+
}
27+
m.delete(key);
28+
if (m.has(key)) {
29+
throw new Error("m still has after delete");
30+
}
31+
if (!m1.has(key)) {
32+
throw new Error("m1 does not have after delete from m");
33+
}
2134
`)
2235
if err != nil {
2336
t.Fatal(err)
2437
}
25-
runtime.GC()
26-
runtime.GC()
27-
vm.RunString("true") // this will trigger dead keys removal
28-
wmo := vm.Get("m").ToObject(vm).self.(*weakMapObject)
29-
l := len(wmo.m.data)
30-
if l > 0 {
31-
t.Fatal("Object has not been removed")
32-
}
3338
}

builtin_weakset.go

+4-46
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,13 @@
11
package goja
22

3-
type weakSet struct {
4-
data map[uint64]struct{}
5-
}
6-
73
type weakSetObject struct {
84
baseObject
9-
s *weakSet
10-
}
11-
12-
func newWeakSet() *weakSet {
13-
return &weakSet{
14-
data: make(map[uint64]struct{}),
15-
}
5+
s weakMap
166
}
177

188
func (ws *weakSetObject) init() {
199
ws.baseObject.init()
20-
ws.s = newWeakSet()
21-
}
22-
23-
func (ws *weakSet) removeId(id uint64) {
24-
delete(ws.data, id)
25-
}
26-
27-
func (ws *weakSet) add(o *Object) {
28-
ref := o.getWeakRef()
29-
ws.data[ref.id] = struct{}{}
30-
o.runtime.addWeakKey(ref.id, ws)
31-
}
32-
33-
func (ws *weakSet) remove(o *Object) bool {
34-
ref := o.weakRef
35-
if ref == nil {
36-
return false
37-
}
38-
_, exists := ws.data[ref.id]
39-
if exists {
40-
delete(ws.data, ref.id)
41-
o.runtime.removeWeakKey(ref.id, ws)
42-
}
43-
return exists
44-
}
45-
46-
func (ws *weakSet) has(o *Object) bool {
47-
ref := o.weakRef
48-
if ref == nil {
49-
return false
50-
}
51-
_, exists := ws.data[ref.id]
52-
return exists
10+
ws.s = weakMap(ws.val.runtime.genId())
5311
}
5412

5513
func (r *Runtime) weakSetProto_add(call FunctionCall) Value {
@@ -58,7 +16,7 @@ func (r *Runtime) weakSetProto_add(call FunctionCall) Value {
5816
if !ok {
5917
panic(r.NewTypeError("Method WeakSet.prototype.add called on incompatible receiver %s", thisObj.String()))
6018
}
61-
wso.s.add(r.toObject(call.Argument(0)))
19+
wso.s.set(r.toObject(call.Argument(0)), nil)
6220
return call.This
6321
}
6422

@@ -119,7 +77,7 @@ func (r *Runtime) builtin_newWeakSet(args []Value, newTarget *Object) *Object {
11977
if adder == r.global.weakSetAdder {
12078
if arr := r.checkStdArrayIter(arg); arr != nil {
12179
for _, v := range arr.values {
122-
wso.s.add(r.toObject(v))
80+
wso.s.set(r.toObject(v), nil)
12381
}
12482
return o
12583
}

builtin_weakset_test.go

-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package goja
22

33
import (
4-
"runtime"
54
"testing"
65
)
76

@@ -21,30 +20,6 @@ func TestWeakSetBasic(t *testing.T) {
2120
testScript1(SCRIPT, _undefined, t)
2221
}
2322

24-
func TestWeakSetExpiry(t *testing.T) {
25-
vm := New()
26-
_, err := vm.RunString(`
27-
var s = new WeakSet();
28-
var o = {};
29-
s.add(o);
30-
if (!s.has(o)) {
31-
throw new Error("has");
32-
}
33-
o = undefined;
34-
`)
35-
if err != nil {
36-
t.Fatal(err)
37-
}
38-
runtime.GC()
39-
runtime.GC()
40-
vm.RunString("true") // this will trigger dead keys removal
41-
wso := vm.Get("s").ToObject(vm).self.(*weakSetObject)
42-
l := len(wso.s.data)
43-
if l > 0 {
44-
t.Fatal("Object has not been removed")
45-
}
46-
}
47-
4823
func TestWeakSetArraySimple(t *testing.T) {
4924
const SCRIPT = `
5025
var o1 = {}, o2 = {}, o3 = {};

0 commit comments

Comments
 (0)