Skip to content

Commit 81ddb8a

Browse files
committed
Rewrote the handling of weak keys to avoid creating circular structures when weak keys are reachable from Runtime. Fixes #199.
1 parent 9af81dd commit 81ddb8a

8 files changed

+148
-81
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ Features
2121
* Sourcemaps.
2222
* Some ES6 functionality, still work in progress, see https://github.com/dop251/goja/milestone/1?closed=1
2323

24+
Known incompatibilities and caveats
25+
-----------------------------------
26+
27+
### WeakMap
28+
WeakMap maintains "hard" references to its values. This means if a value references a key in a WeakMap or a WeakMap
29+
itself, it will not be garbage-collected until the WeakMap becomes unreferenced. To illustrate this:
30+
31+
```go
32+
var m = new WeakMap();
33+
var key = {};
34+
m.set(key, {key: key});
35+
// or m.set(key, key);
36+
key = undefined; // The value will NOT become garbage-collectable at this point
37+
m = undefined; // But it will at this point.
38+
```
39+
40+
Note, this does not have any effect on the application logic, but causes a higher-than-expected memory usage.
41+
2442
FAQ
2543
---
2644

builtin_weakmap.go

+14-33
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
package goja
22

3-
import "sync"
4-
53
type weakMap struct {
6-
// need to synchronise access to the data map because it may be accessed
7-
// from the finalizer goroutine
8-
sync.Mutex
94
data map[uint64]Value
105
}
116

@@ -26,57 +21,43 @@ func (wmo *weakMapObject) init() {
2621
}
2722

2823
func (wm *weakMap) removeId(id uint64) {
29-
wm.Lock()
3024
delete(wm.data, id)
31-
wm.Unlock()
3225
}
3326

3427
func (wm *weakMap) set(key *Object, value Value) {
35-
refs := key.getWeakCollRefs()
36-
wm.Lock()
37-
wm.data[refs.id()] = value
38-
wm.Unlock()
39-
refs.add(wm)
28+
ref := key.getWeakRef()
29+
wm.data[ref.id] = value
30+
key.runtime.addWeakKey(ref.id, wm)
4031
}
4132

4233
func (wm *weakMap) get(key *Object) Value {
43-
refs := key.weakColls
44-
if refs == nil {
34+
ref := key.weakRef
35+
if ref == nil {
4536
return nil
4637
}
47-
wm.Lock()
48-
ret := wm.data[refs.id()]
49-
wm.Unlock()
38+
ret := wm.data[ref.id]
5039
return ret
5140
}
5241

5342
func (wm *weakMap) remove(key *Object) bool {
54-
refs := key.weakColls
55-
if refs == nil {
43+
ref := key.weakRef
44+
if ref == nil {
5645
return false
5746
}
58-
id := refs.id()
59-
wm.Lock()
60-
_, exists := wm.data[id]
61-
if exists {
62-
delete(wm.data, id)
63-
}
64-
wm.Unlock()
47+
_, exists := wm.data[ref.id]
6548
if exists {
66-
refs.remove(wm)
49+
delete(wm.data, ref.id)
50+
key.runtime.removeWeakKey(ref.id, wm)
6751
}
6852
return exists
6953
}
7054

7155
func (wm *weakMap) has(key *Object) bool {
72-
refs := key.weakColls
73-
if refs == nil {
56+
ref := key.weakRef
57+
if ref == nil {
7458
return false
7559
}
76-
id := refs.id()
77-
wm.Lock()
78-
_, exists := wm.data[id]
79-
wm.Unlock()
60+
_, exists := wm.data[ref.id]
8061
return exists
8162
}
8263

builtin_weakmap_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@ func TestWeakMapExpiry(t *testing.T) {
2424
}
2525
runtime.GC()
2626
runtime.GC()
27+
vm.RunString("true") // this will trigger dead keys removal
2728
wmo := vm.Get("m").ToObject(vm).self.(*weakMapObject)
28-
wmo.m.Lock()
2929
l := len(wmo.m.data)
30-
wmo.m.Unlock()
3130
if l > 0 {
3231
t.Fatal("Object has not been removed")
3332
}

builtin_weakset.go

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

3-
import "sync"
4-
53
type weakSet struct {
6-
// need to synchronise access to the data map because it may be accessed
7-
// from the finalizer goroutine
8-
sync.Mutex
94
data map[uint64]struct{}
105
}
116

@@ -26,43 +21,34 @@ func (ws *weakSetObject) init() {
2621
}
2722

2823
func (ws *weakSet) removeId(id uint64) {
29-
ws.Lock()
3024
delete(ws.data, id)
31-
ws.Unlock()
3225
}
3326

3427
func (ws *weakSet) add(o *Object) {
35-
refs := o.getWeakCollRefs()
36-
ws.Lock()
37-
ws.data[refs.id()] = struct{}{}
38-
ws.Unlock()
39-
refs.add(ws)
28+
ref := o.getWeakRef()
29+
ws.data[ref.id] = struct{}{}
30+
o.runtime.addWeakKey(ref.id, ws)
4031
}
4132

4233
func (ws *weakSet) remove(o *Object) bool {
43-
if o.weakColls == nil {
34+
ref := o.weakRef
35+
if ref == nil {
4436
return false
4537
}
46-
id := o.weakColls.id()
47-
ws.Lock()
48-
_, exists := ws.data[id]
49-
if exists {
50-
delete(ws.data, id)
51-
}
52-
ws.Unlock()
38+
_, exists := ws.data[ref.id]
5339
if exists {
54-
o.weakColls.remove(ws)
40+
delete(ws.data, ref.id)
41+
o.runtime.removeWeakKey(ref.id, ws)
5542
}
5643
return exists
5744
}
5845

5946
func (ws *weakSet) has(o *Object) bool {
60-
if o.weakColls == nil {
47+
ref := o.weakRef
48+
if ref == nil {
6149
return false
6250
}
63-
ws.Lock()
64-
_, exists := ws.data[o.weakColls.id()]
65-
ws.Unlock()
51+
_, exists := ws.data[ref.id]
6652
return exists
6753
}
6854

builtin_weakset_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ func TestWeakSetExpiry(t *testing.T) {
3737
}
3838
runtime.GC()
3939
runtime.GC()
40+
vm.RunString("true") // this will trigger dead keys removal
4041
wso := vm.Get("s").ToObject(vm).self.(*weakSetObject)
41-
wso.s.Lock()
4242
l := len(wso.s.data)
43-
wso.s.Unlock()
4443
if l > 0 {
4544
t.Fatal("Object has not been removed")
4645
}

object.go

+33-18
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"reflect"
77
"runtime"
88
"sort"
9+
"sync"
910

1011
"github.com/dop251/goja/unistring"
1112
)
@@ -84,25 +85,35 @@ func (r *weakCollections) remove(c weakCollection) {
8485
}
8586
}
8687

87-
func finalizeObjectWeakRefs(r *weakCollections) {
88-
id := r.id()
89-
for _, c := range r.colls {
90-
c.removeId(id)
91-
}
92-
r.colls = nil
88+
func finalizeObjectWeakRefs(r *objectWeakRef) {
89+
r.tracker.add(r.id)
90+
}
91+
92+
type weakRefTracker struct {
93+
sync.Mutex
94+
list []uint64
95+
}
96+
97+
func (t *weakRefTracker) add(id uint64) {
98+
t.Lock()
99+
t.list = append(t.list, id)
100+
t.Unlock()
101+
}
102+
103+
// An object that gets finalized when the corresponding *Object is garbage-collected.
104+
// It must be ensured that neither the *Object, nor the *Runtime is reachable from this struct,
105+
// otherwise it will create a circular reference with a Finalizer which will make it not garbage-collectable.
106+
type objectWeakRef struct {
107+
id uint64
108+
tracker *weakRefTracker
93109
}
94110

95111
type Object struct {
96112
id uint64
97113
runtime *Runtime
98114
self objectImpl
99115

100-
// Contains references to all weak collections that contain this Object.
101-
// weakColls has a finalizer that removes the Object's id from all weak collections.
102-
// The id is the weakColls pointer value converted to uintptr.
103-
// Note, cannot set the finalizer on the *Object itself because it's a part of a
104-
// reference cycle.
105-
weakColls *weakCollections
116+
weakRef *objectWeakRef
106117
}
107118

108119
type iterNextFunc func() (propIterItem, iterNextFunc)
@@ -1399,15 +1410,19 @@ func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool)
13991410
}
14001411
}
14011412

1402-
func (o *Object) getWeakCollRefs() *weakCollections {
1403-
if o.weakColls == nil {
1404-
o.weakColls = &weakCollections{
1405-
objId: o.getId(),
1413+
func (o *Object) getWeakRef() *objectWeakRef {
1414+
if o.weakRef == nil {
1415+
if o.runtime.weakRefTracker == nil {
1416+
o.runtime.weakRefTracker = &weakRefTracker{}
1417+
}
1418+
o.weakRef = &objectWeakRef{
1419+
id: o.getId(),
1420+
tracker: o.runtime.weakRefTracker,
14061421
}
1407-
runtime.SetFinalizer(o.weakColls, finalizeObjectWeakRefs)
1422+
runtime.SetFinalizer(o.weakRef, finalizeObjectWeakRefs)
14081423
}
14091424

1410-
return o.weakColls
1425+
return o.weakRef
14111426
}
14121427

14131428
func (o *Object) getId() uint64 {

runtime.go

+69-1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ type Runtime struct {
170170
vm *vm
171171
hash *maphash.Hash
172172
idSeq uint64
173+
174+
// Contains a list of ids of finalized weak keys so that the runtime could pick it up and remove from
175+
// all weak collections using the weakKeys map. The runtime picks it up either when the topmost function
176+
// returns (i.e. the callstack becomes empty) or every 10000 'ticks' (vm instructions).
177+
// It is implemented this way to avoid circular references which at the time of writing (go 1.15) causes
178+
// the whole structure to become not garbage-collectable.
179+
weakRefTracker *weakRefTracker
180+
181+
// Contains a list of weak collections that contain the key with the id.
182+
weakKeys map[uint64]*weakCollections
173183
}
174184

175185
type StackFrame struct {
@@ -1195,6 +1205,7 @@ func (r *Runtime) RunProgram(p *Program) (result Value, err error) {
11951205
r.vm.clearStack()
11961206
} else {
11971207
r.vm.stack = nil
1208+
r.leave()
11981209
}
11991210
return
12001211
}
@@ -1966,7 +1977,11 @@ func AssertFunction(v Value) (Callable, bool) {
19661977
if ex != nil {
19671978
err = ex
19681979
}
1969-
obj.runtime.vm.clearStack()
1980+
vm := obj.runtime.vm
1981+
vm.clearStack()
1982+
if len(vm.callStack) == 0 {
1983+
obj.runtime.leave()
1984+
}
19701985
return
19711986
}, true
19721987
}
@@ -2184,6 +2199,59 @@ func (r *Runtime) getHash() *maphash.Hash {
21842199
return r.hash
21852200
}
21862201

2202+
func (r *Runtime) addWeakKey(id uint64, coll weakCollection) {
2203+
keys := r.weakKeys
2204+
if keys == nil {
2205+
keys = make(map[uint64]*weakCollections)
2206+
r.weakKeys = keys
2207+
}
2208+
colls := keys[id]
2209+
if colls == nil {
2210+
colls = &weakCollections{
2211+
objId: id,
2212+
}
2213+
keys[id] = colls
2214+
}
2215+
colls.add(coll)
2216+
}
2217+
2218+
func (r *Runtime) removeWeakKey(id uint64, coll weakCollection) {
2219+
keys := r.weakKeys
2220+
if colls := keys[id]; colls != nil {
2221+
colls.remove(coll)
2222+
if len(colls.colls) == 0 {
2223+
delete(keys, id)
2224+
}
2225+
}
2226+
}
2227+
2228+
// this gets inlined so a CALL is avoided on a critical path
2229+
func (r *Runtime) removeDeadKeys() {
2230+
if r.weakRefTracker != nil {
2231+
r.doRemoveDeadKeys()
2232+
}
2233+
}
2234+
2235+
func (r *Runtime) doRemoveDeadKeys() {
2236+
r.weakRefTracker.Lock()
2237+
list := r.weakRefTracker.list
2238+
r.weakRefTracker.list = nil
2239+
r.weakRefTracker.Unlock()
2240+
for _, id := range list {
2241+
if colls := r.weakKeys[id]; colls != nil {
2242+
for _, coll := range colls.colls {
2243+
coll.removeId(id)
2244+
}
2245+
delete(r.weakKeys, id)
2246+
}
2247+
}
2248+
}
2249+
2250+
// called when the top level function returns (i.e. control is passed outside the Runtime).
2251+
func (r *Runtime) leave() {
2252+
r.removeDeadKeys()
2253+
}
2254+
21872255
func nilSafe(v Value) Value {
21882256
if v != nil {
21892257
return v

vm.go

+1
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ func (vm *vm) run() {
308308
ticks++
309309
if ticks > 10000 {
310310
runtime.Gosched()
311+
vm.r.removeDeadKeys()
311312
ticks = 0
312313
}
313314
}

0 commit comments

Comments
 (0)