Skip to content

Commit

Permalink
feat: add mem usage estimation for map objects
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabri3l committed Jun 11, 2024
1 parent 761212a commit c8c0371
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 17 deletions.
77 changes: 60 additions & 17 deletions builtin_map.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package goja

import (
"math"
"reflect"
)

Expand Down Expand Up @@ -94,6 +95,40 @@ func (mo *mapObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objec
return nil
}

// estimateMemUsage helps calculating mem usage for large objects.
// It will sample the object and use those samples to estimate the
// mem usage.
func (mo *mapObject) estimateMemUsage(ctx *MemUsageContext) (estimate uint64, err error) {
var samplesVisited, memUsage uint64
totalItems := mo.m.size
if totalItems == 0 {
return memUsage, nil
}
sampleSize := int(math.Floor(float64(totalItems) * SampleRate))

i := 0
for item := mo.m.iterFirst; item != nil && i < totalItems; item = item.iterNext {
if i%sampleSize != 0 {
continue
}
samplesVisited += 1
inc, err := item.key.MemUsage(ctx)
memUsage += inc
if err != nil {
return computeMemUsageEstimate(memUsage, samplesVisited, totalItems), err
}

inc, err = item.value.MemUsage(ctx)
memUsage += inc
if err != nil {
return computeMemUsageEstimate(memUsage, samplesVisited, totalItems), err
}
i += 1
}

return computeMemUsageEstimate(memUsage, samplesVisited, totalItems), nil
}

func (mo *mapObject) MemUsage(ctx *MemUsageContext) (memUsage uint64, err error) {
if mo == nil || ctx.IsObjVisited(mo) {
return SizeEmptyStruct, nil
Expand All @@ -109,28 +144,36 @@ func (mo *mapObject) MemUsage(ctx *MemUsageContext) (memUsage uint64, err error)
return memUsage, err
}

for _, entry := range mo.m.hashTable {
if entry == nil {
continue
if ctx.ObjectPropsLenExceedsThreshold(mo.m.size) {
inc, err := mo.estimateMemUsage(ctx)
memUsage += inc
if err != nil {
return memUsage, err
}
} else {
for _, entry := range mo.m.hashTable {
if entry == nil {
continue
}

if entry.key != nil {
inc, err := entry.key.MemUsage(ctx)
memUsage += inc
if err != nil {
return memUsage, err
if entry.key != nil {
inc, err := entry.key.MemUsage(ctx)
memUsage += inc
if err != nil {
return memUsage, err
}
}
}

if entry.value != nil {
inc, err := entry.value.MemUsage(ctx)
memUsage += inc
if err != nil {
return memUsage, err
if entry.value != nil {
inc, err := entry.value.MemUsage(ctx)
memUsage += inc
if err != nil {
return memUsage, err
}
}
if exceeded := ctx.MemUsageLimitExceeded(memUsage); exceeded {
return memUsage, nil
}
}
if exceeded := ctx.MemUsageLimitExceeded(memUsage); exceeded {
return memUsage, nil
}
}

Expand Down
47 changes: 47 additions & 0 deletions builtin_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package goja
import (
"fmt"
"hash/maphash"
"math"
"testing"
)

Expand Down Expand Up @@ -243,8 +244,40 @@ func BenchmarkMapDeleteJS(b *testing.B) {
}
}

func createOrderedMap(vm *Runtime, size int) *orderedMap {
ht := make(map[uint64]*mapEntry, 0)
for i := 0; i < size; i += 1 {
value := vm.ToValue("value")
// This is leveraging the fact that we sample only 10% of the data.
// We intentionally set the non-sampled items to something different
// so that we can show in our test that we are correctly using
// the samples to estimate mem usage and nothing else.
if i%(int(math.Floor(float64(size)*SampleRate))) == 1 {
value = vm.ToValue("verylongstring")
}

ht[uint64(i)] = &mapEntry{
key: vm.ToValue("key"),
value: value,
}
// These iter items are necessary for testing the mem usage
// estimation since that's who we iterate through the map.
if i > 0 {
ht[uint64(i)].iterPrev = ht[uint64(i-1)]
ht[uint64(i-1)].iterNext = ht[uint64(i)]
}
}
return &orderedMap{
size: size,
iterFirst: ht[uint64(0)],
iterLast: ht[uint64(size-1)],
hashTable: ht,
}
}

func TestMapObjectMemUsage(t *testing.T) {
vm := New()

tests := []struct {
name string
mu *MemUsageContext
Expand Down Expand Up @@ -309,6 +342,20 @@ func TestMapObjectMemUsage(t *testing.T) {
(5+SizeString)*3,
errExpected: nil,
},
{
name: "mem above estimate threshold and within memory limit returns correct mem usage",
mu: NewMemUsageContext(vm, 88, 100, 50, 5, TestNativeMemUsageChecker{}),
mo: &mapObject{
m: createOrderedMap(vm, 20),
},
// baseObject
expectedMem: SizeEmptyStruct +
// len(key) + overhead (we reach the limit after 3)
(3+SizeString)*20 +
// len(value) + overhead (we reach the limit after 3)
(5+SizeString)*20,
errExpected: nil,
},
}

for _, tc := range tests {
Expand Down

0 comments on commit c8c0371

Please sign in to comment.