From 65dd408c46a1475fcf1f05710758b5db3bb555bb Mon Sep 17 00:00:00 2001 From: Andrea Rahmanan <50110630+arahmanan@users.noreply.github.com> Date: Mon, 21 Dec 2020 17:35:07 -0500 Subject: [PATCH] REALMC-7452 implement memory usage (#2) * REALMC-7452 implement memory usage --- array.go | 24 +- array_sparse.go | 28 ++- builtin_map.go | 42 ++++ compiler.go | 17 ++ date.go | 11 +- func.go | 30 ++- mem_context.go | 29 +-- memory_test.go | 655 ++++++++++++++++++++++++++++++++---------------- object.go | 80 +++--- object_lazy.go | 13 +- proxy.go | 38 +++ runtime.go | 22 +- string.go | 10 +- typedarrays.go | 64 +++++ value.go | 66 +++-- vm.go | 42 +++- 16 files changed, 810 insertions(+), 361 deletions(-) diff --git a/array.go b/array.go index 7adc5369..929c0141 100644 --- a/array.go +++ b/array.go @@ -634,24 +634,34 @@ func strToGoIdx(s unistring.String) int { } func (a *arrayObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - total := SizeEmpty + if a == nil || ctx.IsObjVisited(a) { + return SizeEmpty, nil + } + ctx.VisitObj(a) - // inc, err := a.baseObject.MemUsage(ctx) - // total += inc - // if err != nil { - // return total, err - // } - inc, err := a.lengthProp.MemUsage(ctx) + total := SizeEmpty + inc, err := a.baseObject.MemUsage(ctx) total += inc if err != nil { return total, err } + + if err := ctx.Descend(); err != nil { + return total, err + } + for _, v := range a.values { + if v == nil { + continue + } + inc, err := v.MemUsage(ctx) total += inc if err != nil { return total, err } } + + ctx.Ascend() return total, nil } diff --git a/array_sparse.go b/array_sparse.go index 90dfd94a..62189aff 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -453,5 +453,31 @@ func (a *sparseArrayObject) exportType() reflect.Type { } func (a *sparseArrayObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - return SizeEmpty, nil + if a == nil || ctx.IsObjVisited(a) { + return SizeEmpty, nil + } + ctx.VisitObj(a) + + if err := ctx.Descend(); err != nil { + return SizeEmpty, err + } + + total := SizeEmpty + for _, item := range a.items { + // Add the size of the index + total += SizeInt32 + if item.value != nil { + inc, err := item.value.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + } + + ctx.Ascend() + + inc, err := a.baseObject.MemUsage(ctx) + total += inc + return total, err } diff --git a/builtin_map.go b/builtin_map.go index 2c23d19a..d6682b13 100644 --- a/builtin_map.go +++ b/builtin_map.go @@ -40,6 +40,48 @@ func (mo *mapObject) init() { mo.m = newOrderedMap(mo.val.runtime.getHash()) } +func (mo *mapObject) MemUsage(ctx *MemUsageContext) (uint64, error) { + if mo == nil || ctx.IsObjVisited(mo) { + return SizeEmpty, nil + } + ctx.VisitObj(mo) + + if err := ctx.Descend(); err != nil { + return 0, err + } + + total, err := mo.baseObject.MemUsage(ctx) + if err != nil { + return total, err + } + + for _, entry := range mo.m.hashTable { + if entry == nil { + continue + } + + if entry.key != nil { + inc, err := entry.key.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if entry.value != nil { + inc, err := entry.value.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + } + + ctx.Ascend() + + return total, nil +} + func (r *Runtime) mapProto_clear(call FunctionCall) Value { thisObj := r.toObject(call.This) mo, ok := thisObj.self.(*mapObject) diff --git a/compiler.go b/compiler.go index 9375ef4d..89195381 100644 --- a/compiler.go +++ b/compiler.go @@ -172,6 +172,23 @@ func (p *Program) sourceOffset(pc int) int { return 0 } +func (p *Program) MemUsage(ctx *MemUsageContext) (uint64, error) { + total := uint64(0) + for _, val := range p.values { + if val == nil { + continue + } + + inc, err := val.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + return total, nil +} + func (s *scope) isFunction() bool { if !s.lexical { return s.outer != nil diff --git a/date.go b/date.go index 891b22ca..dd2872e2 100644 --- a/date.go +++ b/date.go @@ -140,5 +140,14 @@ func (d *dateObject) timeUTC() time.Time { } func (d *dateObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - return SizeEmpty, nil + if d == nil || ctx.IsObjVisited(d) { + return SizeEmpty, nil + } + ctx.VisitObj(d) + + // start with the size of msec + total := SizeNumber + inc, err := d.baseObject.MemUsage(ctx) + total += inc + return total, err } diff --git a/func.go b/func.go index 20e7eda9..fdfaa855 100644 --- a/func.go +++ b/func.go @@ -288,24 +288,32 @@ func (f *boundFuncObject) hasInstance(v Value) bool { } func (f *nativeFuncObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - if ctx.IsObjVisited(f) { - return 0, nil + if f == nil || ctx.IsObjVisited(f) { + return SizeEmpty, nil } ctx.VisitObj(f) - total := SizeEmpty - for _, k := range f.propNames { - prop := f.getOwnPropStr(k) - inc, err := prop.MemUsage(ctx) + return f.baseFuncObject.MemUsage(ctx) +} + +func (f *funcObject) MemUsage(ctx *MemUsageContext) (uint64, error) { + if f == nil || ctx.IsObjVisited(f) { + return SizeEmpty, nil + } + ctx.VisitObj(f) + + total, baseObjectErr := f.baseObject.MemUsage(ctx) + if baseObjectErr != nil { + return total, baseObjectErr + } + + if f.stash != nil { + inc, err := f.stash.MemUsage(ctx) total += inc if err != nil { return total, err } - } - return total, nil -} -func (f *funcObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - return f.baseObject.MemUsage(ctx) + return total, nil } diff --git a/mem_context.go b/mem_context.go index 8dd57484..2d1f397d 100644 --- a/mem_context.go +++ b/mem_context.go @@ -2,14 +2,11 @@ package goja import ( "errors" - "hash/maphash" ) type visitTracker struct { objsVisited map[objectImpl]bool stashesVisited map[*stash]bool - valsVisited map[uint64]bool - h *maphash.Hash } func (vt visitTracker) IsObjVisited(obj objectImpl) bool { @@ -21,29 +18,11 @@ func (vt visitTracker) VisitObj(obj objectImpl) { vt.objsVisited[obj] = true } -func (vt visitTracker) IsValVisited(obj Value) bool { - if obj == nil { - return true - } - _, ok := vt.valsVisited[obj.hash(vt.h)] - return ok -} - -func (vt visitTracker) VisitVal(obj Value) { - vt.valsVisited[obj.hash(vt.h)] = true -} - func (vt visitTracker) IsStashVisited(stash *stash) bool { _, ok := vt.stashesVisited[stash] return ok } -// func (vt visitTracker) IsStackVisited(stash valueStack) bool { -// _, ok := vt.stashesVisited[stash] -// fmt.Println("visited :check:") -// return ok -// } - func (vt visitTracker) VisitStash(stash *stash) { vt.stashesVisited[stash] = true } @@ -109,7 +88,6 @@ func (self *stash) MemUsage(ctx *MemUsageContext) (uint64, error) { } type MemUsageContext struct { - vm *Runtime visitTracker *depthTracker NativeMemUsageChecker @@ -117,8 +95,7 @@ type MemUsageContext struct { func NewMemUsageContext(vm *Runtime, maxDepth int, nativeChecker NativeMemUsageChecker) *MemUsageContext { return &MemUsageContext{ - vm: vm, - visitTracker: visitTracker{objsVisited: map[objectImpl]bool{}, valsVisited: map[uint64]bool{}, stashesVisited: map[*stash]bool{}, h: &maphash.Hash{}}, + visitTracker: visitTracker{objsVisited: map[objectImpl]bool{}, stashesVisited: map[*stash]bool{}}, depthTracker: &depthTracker{curDepth: 0, maxDepth: maxDepth}, NativeMemUsageChecker: nativeChecker, } @@ -127,3 +104,7 @@ func NewMemUsageContext(vm *Runtime, maxDepth int, nativeChecker NativeMemUsageC var ( ErrMaxDepth = errors.New("reached max depth") ) + +type MemUsageReporter interface { + MemUsage(ctx *MemUsageContext) (uint64, error) +} diff --git a/memory_test.go b/memory_test.go index fcad6a44..4a2f2a22 100644 --- a/memory_test.go +++ b/memory_test.go @@ -1,212 +1,447 @@ package goja -// import ( -// "fmt" -// "testing" -// ) - -// type NoopNativeMemUsageChecker struct { -// } - -// func (nnmu NoopNativeMemUsageChecker) NativeMemUsage(goNativeValue interface{}) (uint64, bool) { -// return 0, false -// } - -// func TestMemCheck(t *testing.T) { -// // This is the sum of property names allocated at each new (empty) scope -// var functionOverhead uint64 = 91 -// /* -// └──────Property "arguments" -// └─────────Property "length" -// └─────────done: "length" (6) size 8 (total = 14) -// └─────────Property "callee" -// └────────────Property "name" -// └────────────done: "name" (4) size 0 (total = 4) -// └────────────Property "length" -// └────────────done: "length" (6) size 8 (total = 14) -// └────────────Property "prototype" -// └───────────────Property "constructor" -// └───────────────done: "constructor" (11) size 0 (total = 11) -// └────────────done: "prototype" (9) size 19 (total = 28) -// └─────────done: "callee" (6) size 54 (total = 60) -// └──────done: "arguments" (9) size 82 (total = 91) -// */ - -// for _, tc := range []struct { -// description string -// script string -// expectedSizeDiff uint64 -// }{ -// { -// "number", -// `x = [] -// checkMem() -// x.push(0) -// checkMem()`, -// SizeNumber, -// }, -// { -// "boolean", -// `x = [] -// checkMem() -// x.push(true) -// checkMem()`, -// SizeBool, -// }, -// { -// "null", -// `x = [] -// checkMem() -// x.push(null) -// checkMem()`, -// SizeEmpty, -// }, -// { -// "undefined", -// `x = [] -// checkMem() -// x.push(undefined) -// checkMem()`, -// SizeEmpty, -// }, -// { -// "string", -// `x = [] -// checkMem() -// x.push("12345") -// checkMem()`, -// 5, -// }, -// { -// "string with multi-byte characters", -// `x = [] -// checkMem() -// x.push("\u2318") -// checkMem()`, -// 3 + 1, // "x" property name, plus single char with 3-byte width -// }, -// { -// "nested objects", -// `y = [] -// checkMem() -// y.push({"a":10, "b":"1234", "c":{}}) -// checkMem()`, -// SizeEmpty + -// (1 + SizeNumber) + // "a" and number -// (1 + 4) + // "b" and string -// (1 + SizeEmpty) + // "c" (an object) -// (1), //"0" attribute for y -// }, -// { -// "array of numbers", -// `y = [] -// var i = 0; -// checkMem(); -// y.push([]); -// for(i=0;i<20;i++){ -// y[0].push(i); -// }; -// checkMem()`, -// // Array overhead, -// // size of property names, -// // size of property values, -// // plus "length" attribute for the array. -// // plus the length of the "0" property of the outermost array. -// SizeEmpty + (10 + 10*2) + 20*SizeNumber + 6 + SizeNumber + 1, -// }, -// { -// "overhead of a single new scope", -// `checkMem(); -// (function(){ -// checkMem(); -// })();`, // over -// functionOverhead, -// }, -// { -// "overhead of each scope is equivalent regardless of depth", -// `checkMem(); -// (function(){ -// (function(){ -// (function(){ -// (function(){ -// (function(){ -// (function(){ -// checkMem(); -// })(); -// })(); -// })(); -// })(); -// })(); -// })();`, -// functionOverhead * 6, -// }, -// {"values attached to lexical scope in a function", -// `checkMem(); -// (function(){ -// var zzzx = 10; -// checkMem(); -// })();`, -// // function overhead plus the number value of the "zzzx" property and its string name -// functionOverhead + SizeNumber + 4, -// }, -// { -// "cyclical data structure", -// // cyclical data structure does not recurse infinitely -// // and does not artificially inflate mem count. The only change in mem -// // between the two checks is for the new property names for "y" and "x". -// `var zzza = {} -// var zzzb = {} -// checkMem(); -// zzza.y = zzzb -// zzzb.x = zzza -// checkMem()`, -// 2, // "x" and "y" property names -// }, -// } { -// t.Run(fmt.Sprintf(tc.description), func(t *testing.T) { -// memChecks := []uint64{} -// vm := New() -// vm.Set("checkMem", func(call FunctionCall) Value { - -// mem, err := vm.MemUsage(NewMemUsageContext(vm, 100, NoopNativeMemUsageChecker{})) -// if err != nil { -// t.Fatal(err) -// } -// memChecks = append(memChecks, mem) -// return UndefinedValue() -// }) -// _, err := vm.RunString(tc.script) -// if err != nil { -// t.Fatal(err) -// } -// if len(memChecks) < 2 { -// t.Fatalf("expected at least two entries in mem check function, but got %d", len(memChecks)) -// } -// fmt.Println("mem diff!!", memChecks[len(memChecks)-1], " ", memChecks[0]) -// memDiff := memChecks[len(memChecks)-1] - memChecks[0] -// if memDiff != tc.expectedSizeDiff { -// t.Fatalf("expected memory change to equal %d but got %d instead", tc.expectedSizeDiff, memDiff) -// } -// }) -// } - -// t.Log("Check max depth error condition") -// vm := New() -// _, err := vm.RunString(` -// var x = {"a": {"b": {"c":{"d":1}}}} -// `) -// if err != nil { -// t.Fatal(err) -// } - -// _, err = vm.MemUsage(NewMemUsageContext(vm, 3, NoopNativeMemUsageChecker{})) -// if err != ErrMaxDepth { -// t.Fatalf("expected mem check to hit depth limit error, but got nil %v", err) -// } - -// _, err = vm.MemUsage(NewMemUsageContext(vm, 4, NoopNativeMemUsageChecker{})) -// if err != nil { -// t.Fatalf("expected to NOT hit mem check hit depth limit error, but got %v", err) -// } - -// } +import ( + "fmt" + "testing" +) + +const testNativeValueMemUsage = 100 + +type TestNativeValue struct { +} + +type TestNativeMemUsageChecker struct { +} + +func (muc TestNativeMemUsageChecker) NativeMemUsage(value interface{}) (uint64, bool) { + switch value.(type) { + case TestNativeValue: + return testNativeValueMemUsage, true + default: + return 0, false + } +} + +func TestMemCheck(t *testing.T) { + // This is the sum of property names allocated at each new (empty) scope + var emptyFunctionScopeOverhead uint64 = 8 + + for _, tc := range []struct { + description string + script string + expectedSizeDiff uint64 + }{ + { + "number", + `x = [] + x.push(0) + checkMem() + x.push(0) + checkMem()`, + SizeNumber, + }, + { + "boolean", + `x = [] + x.push(true) + checkMem() + x.push(true) + checkMem()`, + SizeBool, + }, + { + "null", + `x = [] + x.push(null) + checkMem() + x.push(null) + checkMem()`, + SizeEmpty, + }, + { + "undefined", + `x = [] + x.push(undefined) + checkMem() + x.push(undefined) + checkMem()`, + SizeEmpty, + }, + { + "string", + `x = [] + x.push("12345") + checkMem() + x.push("12345") + checkMem()`, + 5, + }, + { + "string with multi-byte characters", + `x = [] + x.push("\u2318") + checkMem() + x.push("\u2318") + checkMem()`, + 3, // single char with 3-byte width + }, + { + "nested objects", + `y = [] + y.push(null) + checkMem() + y.push({"a":10, "b":"1234", "c":{}}) + checkMem()`, + SizeEmpty + SizeEmpty + // outer object + reference to its prototype + (1 + SizeNumber) + // "a" and number + (1 + 4) + // "b" and string + (1 + SizeEmpty + SizeEmpty), // "c" (object + prototype reference) + }, + { + "array of numbers", + `y = [] + var i = 0; + y.push([]); + checkMem(); + for(i=0;i<20;i++){ + y[0].push(i); + }; + checkMem()`, + // Array overhead, + // size of property values, + SizeEmpty + 20*SizeNumber, + }, + { + "overhead of a single new scope", + `checkMem(); + (function(){ + checkMem(); + })();`, // over + emptyFunctionScopeOverhead, + }, + { + "previous function scopes should not affect the current memory", + `checkMem(); + (function(){ + })(); + checkMem();`, + 0, + }, + { + "overhead of each scope is equivalent regardless of depth", + `checkMem(); + (function(){ + (function(){ + (function(){ + (function(){ + (function(){ + (function(){ + checkMem(); + })(); + })(); + })(); + })(); + })(); + })();`, + emptyFunctionScopeOverhead * 6, + }, + {"values attached to lexical scope in a function", + `checkMem(); + (function(){ + var zzzx = 10; + checkMem(); + })();`, + // function overhead plus the number value of the "zzzx" property and its string name + // functionOverhead + SizeNumber + 4, + emptyFunctionScopeOverhead + SizeNumber, + }, + { + "cyclical data structure", + // cyclical data structure does not recurse infinitely + // and does not artificially inflate mem count. The only change in mem + // between the two checks is for the new property names for "y" and "x". + `var zzza = {} + var zzzb = {} + checkMem(); + zzza.y = zzzb + zzzb.x = zzza + checkMem()`, + 2 + SizeEmpty + SizeEmpty, // "x" and "y" property names + references to each object + }, + { + "sparse array (arrayObject)", + `x = [] + x[1] = "abcd"; + checkMem() + x[10] = "abc"; + checkMem()`, + 3, + }, + { + "sparse array (sparseArrayObject)", + `x = [] + x[5000] = "abcd"; + checkMem() + x[5001] = "abc"; + checkMem()`, + SizeInt32 + 3, + }, + { + "array with non-numeric keys", + `x = [] + x["a"] = 3; + checkMem() + x[2] = "abc"; + x["c"] = 3; + checkMem() + `, + // len("abc") + len("a") + SizeNumber + 3 + 1 + SizeNumber, + }, + { + "reference to array", + `x = [] + x[1] = "abcd"; + x[10] = "abc"; + checkMem() + y = x; + checkMem()`, + // len("y") + reference to array + 1 + SizeEmpty, + }, + { + "reference to sparse array", + `x = [] + x[5000] = "abcb"; + x[5001] = "abc"; + checkMem() + y = x; + // len("y") + reference to array + checkMem()`, + 1 + SizeEmpty, + }, + { + "Date object", + ` + d1 = new Date(); + checkMem(); + d2 = new Date(); + checkMem() + `, + // len("d2") + size of msec + reference to visited base object + base object prototype reference + 2 + SizeNumber + SizeEmpty + SizeEmpty, + }, + { + "Empty object", + ` + checkMem(); + o = {} + checkMem() + `, + // len("o") + object's starting size + reference to prototype + 1 + SizeEmpty + SizeNumber, + }, + { + "Map", + `var m = new Map(); + m.set("a", 1); + checkMem(); + m.set("abc", {"a":10, "b":"1234"}); + checkMem();`, + 3 + // "abc" + SizeEmpty + SizeEmpty + // outer object + reference to its prototype + (1 + SizeNumber) + // "a" and number + (1 + 4), // "b" and string + }, + { + "Proxy", + `var target = { + message1: "hello", + message2: "everyone" + }; + + var handler = { + get: function(target, prop, receiver) { + return "world"; + } + }; + var proxy1 = new Proxy(target, handler); + + checkMem(); + proxy2 = new Proxy(target, handler); + checkMem(); + `, + 6 + // "proxy2" + SizeEmpty + // proxy overhead + SizeEmpty + SizeEmpty + // base object + prototype + SizeEmpty + // target object reference + SizeEmpty, // handler object reference + }, + { + "String", + `str1 = new String("hi") + + checkMem(); + str2 = new String("hello") + checkMem(); + `, + 4 + // "str2" + 5 + // "hello" + SizeEmpty + SizeEmpty + // base object + prototype + 6 + SizeNumber, // "length" + number + }, + { + "Typed array", + `var ta = new Uint8Array(1); + checkMem(); + ta2 = new Uint8Array([1, 2, 3, 4]), + checkMem(); + `, + 3 + // "ta2" + SizeEmpty + // typed array overhead + SizeEmpty + SizeEmpty + // base object + prototype + 4 + SizeEmpty + SizeEmpty + // array buffer data + base object + prototype + SizeEmpty, // default constructor + }, + { + "ArrayBuffer", + `var buffer = new ArrayBuffer(16); + + checkMem(); + buffer2 = new ArrayBuffer(16); + checkMem();`, + 7 + // "buffer2" + 16 + // data size + SizeEmpty + SizeEmpty, // base object + prototype + }, + { + "DataView", + `var buffer = new ArrayBuffer(16); + var view = new DataView(buffer, 0); + var buffer2 = new ArrayBuffer(16); + + checkMem(); + view2 = new DataView(buffer2, 0); + checkMem();`, + 5 + // "view2" + SizeEmpty + // DataView overhead + SizeEmpty + SizeEmpty + // base object + prototype + SizeEmpty, // array buffer reference + }, + { + "Number", + `num1 = new Number("1") + + checkMem(); + num2 = new Number("2") + checkMem();`, + 4 + // "num2" + SizeNumber + + SizeEmpty + SizeEmpty, // base object + prototype + }, + { + "stash", + `checkMem(); + try { + throw new Error("abc"); + } catch(e) { + checkMem(); + } + `, + 7 + 3 + // Error "message" field + len("abc") + 4 + 5 + // Error "name" field + len("Error") + SizeEmpty + SizeEmpty, // base object + prototype + }, + { + "Native value", + `checkMem(); + nv = new MyNativeVal() + checkMem();`, + testNativeValueMemUsage + 2, + }, + } { + t.Run(fmt.Sprintf(tc.description), func(t *testing.T) { + memChecks := []uint64{} + vm := New() + vm.Set("checkMem", func(call FunctionCall) Value { + mem, err := vm.MemUsage(NewMemUsageContext(vm, 100, TestNativeMemUsageChecker{})) + if err != nil { + t.Fatal(err) + } + memChecks = append(memChecks, mem) + return UndefinedValue() + }) + + nc := vm.CreateNativeClass("MyNativeVal", func(call FunctionCall) interface{} { + return TestNativeValue{} + }, nil, nil) + + vm.Set("MyNativeVal", nc.Function) + + _, err := vm.RunString(tc.script) + if err != nil { + t.Fatal(err) + } + if len(memChecks) < 2 { + t.Fatalf("expected at least two entries in mem check function, but got %d", len(memChecks)) + } + + memDiff := memChecks[len(memChecks)-1] - memChecks[0] + if memDiff != tc.expectedSizeDiff { + t.Fatalf("expected memory change to equal %d but got %d instead", tc.expectedSizeDiff, memDiff) + } + }) + } +} + +func TestMemMaxDepth(t *testing.T) { + for _, tc := range []struct { + description string + script string + expectedDepth int + }{ + { + "nested objects", + `var x = {"1": {"2": {"3": {"4": {"5": {"6": "abc"}}}}}}`, + 6, + }, + { + "array", + `var x = [] + x[1] = {"1": {"2": {"3": {"4": {"5": {"6": "abc"}}}}}};`, + 7, + }, + { + "sparse array (sparseArrayObject)", + `var x = [] + x[5000] = {"1": {"2": {"3": {"4": {"5": {"6": "abc"}}}}}};`, + 7, + }, + { + "Map", + `var abc = new Map() + abc.set("obj", {"1": {"2": {"3": {"4": {"5": {"6": "abc"}}}}}});`, + 7, + }, + } { + t.Run(fmt.Sprintf(tc.description), func(t *testing.T) { + vm := New() + _, err := vm.RunString(tc.script) + if err != nil { + t.Fatal(err) + } + + // All global variables are contained in the Runtime's globalObject field, which causes + // them to be one level deeper + _, err = vm.MemUsage(NewMemUsageContext(vm, tc.expectedDepth, TestNativeMemUsageChecker{})) + if err != ErrMaxDepth { + t.Fatalf("expected mem check to hit depth limit error, but got nil %v", err) + } + + _, err = vm.MemUsage(NewMemUsageContext(vm, tc.expectedDepth+1, TestNativeMemUsageChecker{})) + if err != nil { + t.Fatalf("expected to NOT hit mem check hit depth limit error, but got %v", err) + } + }) + } +} diff --git a/object.go b/object.go index d709fd62..f8f350fc 100644 --- a/object.go +++ b/object.go @@ -1577,63 +1577,63 @@ func (ctx *objectExportCtx) putTyped(key objectImpl, typ reflect.Type, value int } func (o *baseObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - if ctx.IsObjVisited(o) { - return 0, nil + if o == nil || ctx.IsObjVisited(o) { + return SizeEmpty, nil } ctx.VisitObj(o) + + if err := ctx.Descend(); err != nil { + return 0, err + } + total := SizeEmpty - // if o.val != nil { - // inc, err := o.val.MemUsage(ctx) - // total += inc - // if err != nil { - // return total, err - // } - // } - - for k, v := range o.values { - total += uint64(len(k)) - // v := o.val.self.getStr(name.string(), nil) - // if v == nil { - // continue - // } + + for _, k := range o.propNames { + v := o.values[k] + if v == nil { + continue + } inc, err := v.MemUsage(ctx) total += inc + total += uint64(len(k)) + if err != nil { return total, err } } - // for k, val := range o.values { - // total += uint64(len(k)) - // if val == nil { - // continue - // } - // inc, err := val.MemUsage(ctx) - // total += inc - // // count size of property name towards total object size. - // if err != nil { - // return total, err - // } - // } + + if o.prototype != nil { + inc, err := o.prototype.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + ctx.Ascend() + return total, nil } func (self *primitiveValueObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - total := SizeEmpty - // self.mu.RLock() - // defer self.mu.RUnlock() - for k, v := range self.values { - // if val, ok := v.(Value); ok { - inc, err := v.MemUsage(ctx) + if self == nil || ctx.IsObjVisited(self) { + return SizeEmpty, nil + } + ctx.VisitObj(self) + + total := uint64(0) + + if self.pValue != nil { + inc, err := self.pValue.MemUsage(ctx) total += inc - total += uint64(len(k)) // count size of property name towards total object size. if err != nil { return total, err } - // } else { - // // most likely a propertyGetSet. ignore for now. - // } } - return total, nil - // return SizeEmpty, nil + + inc, baseErr := self.baseObject.MemUsage(ctx) + total += inc + return total, baseErr + } diff --git a/object_lazy.go b/object_lazy.go index a1213f77..c5e71f0a 100644 --- a/object_lazy.go +++ b/object_lazy.go @@ -296,20 +296,13 @@ func (o *lazyObject) swap(i, j int64) { } func (o *lazyObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - if o.val == nil { + if o.val == nil || ctx.IsObjVisited(o) { return SizeEmpty, nil } - if ctx.IsObjVisited(o.val.self) { - return 0, nil - } ctx.VisitObj(o) - total := uint64(0) + total := uint64(SizeEmpty) inc, err := o.val.MemUsage(ctx) total += inc - if err != nil { - return total, err - } - - return total, nil + return total, err } diff --git a/proxy.go b/proxy.go index a181a9bd..509a6f77 100644 --- a/proxy.go +++ b/proxy.go @@ -805,3 +805,41 @@ func (p *proxyObject) revoke() { p.handler = nil p.target = nil } + +func (p *proxyObject) MemUsage(ctx *MemUsageContext) (uint64, error) { + if p == nil || ctx.IsObjVisited(p) { + return SizeEmpty, nil + } + ctx.VisitObj(p) + + if err := ctx.Descend(); err != nil { + return 0, err + } + + total := SizeEmpty + inc, baseObjetErr := p.baseObject.MemUsage(ctx) + total += inc + if baseObjetErr != nil { + return total, baseObjetErr + } + + if p.target != nil { + inc, err := p.target.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if p.handler != nil { + inc, err := p.handler.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + ctx.Ascend() + + return total, nil +} diff --git a/runtime.go b/runtime.go index 1f22e96b..0e3824c0 100644 --- a/runtime.go +++ b/runtime.go @@ -431,28 +431,28 @@ func (r *Runtime) MemUsage(ctx *MemUsageContext) (uint64, error) { total := uint64(0) if r.globalObject != nil { - inc, err := r.globalObject.self.MemUsage(ctx) + inc, err := r.globalObject.MemUsage(ctx) total += inc - fmt.Println("inc, total", inc, total) if err != nil { return total, err } } - if r.vm.stack != nil { - inc, err := r.vm.stack.MemUsage(ctx) + for idx := range r.vm.callStack { + inc, err := r.vm.callStack[idx].MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if r.vm.stash != nil { + inc, err := r.vm.stash.MemUsage(ctx) total += inc if err != nil { return total, err } } - // if r.vm.stash != nil { - // inc, err := r.vm.stash.MemUsage(ctx) - // total += inc - // if err != nil { - // return total, err - // } - // } return total, nil } diff --git a/string.go b/string.go index 9690fb2d..dc27d2b2 100644 --- a/string.go +++ b/string.go @@ -345,5 +345,13 @@ func (s *stringObject) hasOwnPropertyIdx(idx valueInt) bool { } func (s *stringObject) MemUsage(ctx *MemUsageContext) (uint64, error) { - return uint64(s.length), nil + if s == nil || ctx.IsObjVisited(s) { + return SizeEmpty, nil + } + ctx.VisitObj(s) + + total := uint64(s.length) + inc, err := s.baseObject.MemUsage(ctx) + total += inc + return total, err } diff --git a/typedarrays.go b/typedarrays.go index 874eca2b..d93d34b4 100644 --- a/typedarrays.go +++ b/typedarrays.go @@ -109,6 +109,10 @@ func (a ArrayBuffer) Detached() bool { return a.buf.detached } +func (a ArrayBuffer) MemUsage(ctx *MemUsageContext) (uint64, error) { + return a.buf.MemUsage(ctx) +} + func (r *Runtime) NewArrayBuffer(data []byte) ArrayBuffer { buf := r._newArrayBuffer(r.global.ArrayBufferPrototype, nil) buf.data = data @@ -641,6 +645,34 @@ func (a *typedArrayObject) enumerateUnfiltered() iterNextFunc { }).next } +func (a *typedArrayObject) MemUsage(ctx *MemUsageContext) (uint64, error) { + if a == nil || ctx.IsObjVisited(a) { + return SizeEmpty, nil + } + ctx.VisitObj(a) + + total := SizeEmpty + if a.viewedArrayBuf != nil { + inc, err := a.viewedArrayBuf.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if a.defaultCtor != nil { + inc, err := a.defaultCtor.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + inc, err := a.baseObject.MemUsage(ctx) + total += inc + return total, err +} + func (r *Runtime) _newTypedArrayObject(buf *arrayBufferObject, offset, length, elemSize int, defCtor *Object, arr typedArray, proto *Object) *typedArrayObject { o := &Object{runtime: r} a := &typedArrayObject{ @@ -719,6 +751,26 @@ func (o *dataViewObject) getIdxAndByteOrder(idxVal, littleEndianVal Value, size return getIdx, bo } +func (o *dataViewObject) MemUsage(ctx *MemUsageContext) (uint64, error) { + if o == nil || ctx.IsObjVisited(o) { + return SizeEmpty, nil + } + ctx.VisitObj(o) + + total := SizeEmpty + if o.viewedArrayBuf != nil { + inc, err := o.viewedArrayBuf.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + inc, err := o.baseObject.MemUsage(ctx) + total += inc + return total, err +} + func (o *arrayBufferObject) ensureNotDetached() { if o.detached { panic(o.val.runtime.NewTypeError("ArrayBuffer is detached")) @@ -854,6 +906,18 @@ func (o *arrayBufferObject) export(*objectExportCtx) interface{} { } } +func (o *arrayBufferObject) MemUsage(ctx *MemUsageContext) (uint64, error) { + if o == nil || ctx.IsObjVisited(o) { + return SizeEmpty, nil + } + ctx.VisitObj(o) + + total := uint64(len(o.data)) + inc, err := o.baseObject.MemUsage(ctx) + total += inc + return total, err +} + func (r *Runtime) _newArrayBuffer(proto *Object, o *Object) *arrayBufferObject { if o == nil { o = &Object{runtime: r} diff --git a/value.go b/value.go index 92dca038..5ae12a47 100644 --- a/value.go +++ b/value.go @@ -726,8 +726,7 @@ func (p *valueProperty) hash(*maphash.Hash) uint64 { } func (p *valueProperty) MemUsage(ctx *MemUsageContext) (uint64, error) { - total := SizeEmpty - total += uint64(len(p.String())) // count size of property name towards total object size. + total := uint64(0) if p.value != nil { inc, err := p.value.MemUsage(ctx) total += inc @@ -736,6 +735,22 @@ func (p *valueProperty) MemUsage(ctx *MemUsageContext) (uint64, error) { } } + if p.getterFunc != nil { + inc, err := p.getterFunc.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if p.setterFunc != nil { + inc, err := p.setterFunc.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + return total, nil } @@ -1069,9 +1084,17 @@ func (o *Object) hash(*maphash.Hash) uint64 { } func (o *Object) MemUsage(ctx *MemUsageContext) (uint64, error) { - if o.__wrapped != nil { + if o == nil || o.self == nil { return SizeEmpty, nil } + + if o.__wrapped != nil { + nativeMem, ok := ctx.NativeMemUsage(o.__wrapped) + if ok { + return nativeMem, nil + } + } + switch x := o.self.(type) { case *objectGoReflect: return SizeEmpty, nil @@ -1083,42 +1106,15 @@ func (o *Object) MemUsage(ctx *MemUsageContext) (uint64, error) { return SizeEmpty, nil case *objectGoSliceReflect: return SizeEmpty, nil - case *arrayObject: - return x.MemUsage(ctx) - case *nativeFuncObject: - return x.MemUsage(ctx) - case *baseObject: - if o.self == nil { - return 0, nil - } - err := ctx.Descend() - if err != nil { - return 0, err - } - total, err := o.self.MemUsage(ctx) - ctx.Ascend() - return total, err - case *lazyObject: - if o.self == nil { - return 0, nil - } - total, err := o.self.MemUsage(ctx) - return total, err - case *primitiveValueObject: - if o.self == nil { - return 0, nil - } - total, err := o.self.MemUsage(ctx) - return total, err - case *stringObject: - if x.val == nil { + default: + r, ok := x.(MemUsageReporter) + if !ok { return 0, nil } - return x.MemUsage(ctx) - default: + return r.MemUsage(ctx) } - return 0, nil } + func (o *Object) ToInt() int { return o.self.toPrimitiveNumber().ToNumber().ToInt() } diff --git a/vm.go b/vm.go index 7d4395cf..5f02350f 100644 --- a/vm.go +++ b/vm.go @@ -40,6 +40,36 @@ type vmContext struct { mu sync.RWMutex } +func (vc *vmContext) MemUsage(ctx *MemUsageContext) (uint64, error) { + total := SizeEmpty + + if vc.newTarget != nil { + inc, err := vc.newTarget.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if vc.stash != nil { + inc, err := vc.stash.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + if vc.prg != nil { + inc, err := vc.prg.MemUsage(ctx) + total += inc + if err != nil { + return total, err + } + } + + return total, nil +} + type iterStackItem struct { val Value f iterNextFunc @@ -2633,20 +2663,12 @@ func (jmp iterNext) exec(vm *vm) { func (stack valueStack) MemUsage(ctx *MemUsageContext) (uint64, error) { total := uint64(0) for _, self := range stack { - if self != nil && self.ExportType() != nil && self.ExportType().Kind() == reflect.Func { - return 0, nil + if self == nil { + continue } - // obj := self.baseObject(ctx.vm) - // if ctx.IsValVisited(self) { - // fmt.Printf("self is visited %T %+v\n", self, self) - // return 0, nil - // } - // ctx.VisitVal(self) inc, err := self.MemUsage(ctx) - fmt.Printf("self is not visited, incrementing by %v %T %+v\n", inc, self, self) total += inc - fmt.Println("this brings total to: ", total) if err != nil { return total, err }