Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BAAS-24348: estimate mem usage for gomap and goslice when over thresholds #110

Merged
merged 9 commits into from
Jan 18, 2024
12 changes: 8 additions & 4 deletions array.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@
}

func toIdx(v valueInt) uint32 {
if v >= 0 && v < math.MaxUint32 {

Check failure on line 562 in array.go

View workflow job for this annotation

GitHub Actions / test (1.16.x, ubuntu-latest, 386)

constant 4294967295 overflows valueInt
return uint32(v)
}
return math.MaxUint32
Expand All @@ -576,11 +576,15 @@
// usage of the whole array
func (a *arrayObject) estimateMemUsage(ctx *MemUsageContext) (estimate uint64, newEstimate uint64, err error) {
var samplesVisited, memUsage, newMemUsage uint64
sampleSize := len(a.values) / 10
arrayLen := len(a.values)
if arrayLen == 0 {
return memUsage, newMemUsage, nil
}
sampleSize := arrayLen / 10

// grabbing one sample every "sampleSize" to provide consistent
// memory usage across function executions
for i := 0; i < len(a.values); i += sampleSize {
for i := 0; i < arrayLen; i += sampleSize {
if a.values[i] == nil {
continue
}
Expand All @@ -590,8 +594,8 @@
memUsage += inc
newMemUsage += newInc
// average * number of a.values
estimate = uint64((float32(memUsage) / float32(samplesVisited)) * float32(len(a.values)))
newEstimate = uint64((float32(newMemUsage) / float32(samplesVisited)) * float32(len(a.values)))
estimate = uint64((float32(memUsage) / float32(samplesVisited)) * float32(arrayLen))
newEstimate = uint64((float32(newMemUsage) / float32(samplesVisited)) * float32(arrayLen))
if err != nil {
return estimate, newEstimate, err
}
Expand Down
12 changes: 12 additions & 0 deletions array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ func TestArrayObjectMemUsage(t *testing.T) {
(4+SizeString)*4,
errExpected: nil,
},
{
name: "empty array with negative threshold",
mu: NewMemUsageContext(vm, 88, 100, -1, 50, TestNativeMemUsageChecker{}),
ao: &arrayObject{
values: []Value{},
},
// array overhead + array baseObject + values slice overhead
expectedMem: SizeEmptyStruct + SizeEmptyStruct + SizeEmptySlice,
// array overhead + array baseObject + values slice overhead
expectedNewMem: SizeEmptyStruct + SizeEmptyStruct + SizeEmptySlice,
errExpected: nil,
},
{
name: "array limit function undefined throws error",
mu: &MemUsageContext{
Expand Down
3 changes: 3 additions & 0 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -1916,6 +1916,9 @@ func computeMemUsageEstimate(memUsage, samplesVisited uint64, totalProps int) ui
func (o *baseObject) estimateMemUsage(ctx *MemUsageContext) (estimate uint64, newEstimate uint64, err error) {
var samplesVisited, memUsage, newMemUsage uint64
totalProps := len(o.propNames)
if totalProps == 0 {
return memUsage, newMemUsage, nil
}
sampleSize := totalProps / 10

// grabbing one sample every "sampleSize" to provide consistent
Expand Down
68 changes: 58 additions & 10 deletions object_gomap.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,71 @@ func (o *objectGoMapSimple) equal(other objectImpl) bool {
return false
}

// estimateMemUsage helps calculating mem usage for large objects.
// It will sample the object and use those samples to estimate the
// mem usage.
func (o *objectGoMapSimple) estimateMemUsage(ctx *MemUsageContext) (estimate uint64, newEstimate uint64, err error) {
var samplesVisited, memUsage, newMemUsage uint64
counter := 0
totalProps := len(o.data)
if totalProps == 0 {
return memUsage, newMemUsage, nil
}
sampleSize := totalProps / 10

// grabbing one sample every "sampleSize" to provide consistent
// memory usage across function executions
for key := range o.data {
counter++
if counter%sampleSize == 0 {
Gabri3l marked this conversation as resolved.
Show resolved Hide resolved
memUsage += uint64(len(key)) + SizeString
newMemUsage += uint64(len(key)) + SizeString
v := o._getStr(key)
if v == nil {
continue
}

inc, newInc, err := v.MemUsage(ctx)
samplesVisited += 1
memUsage += inc
newMemUsage += newInc
if err != nil {
return computeMemUsageEstimate(memUsage, samplesVisited, totalProps), computeMemUsageEstimate(newMemUsage, samplesVisited, totalProps), err
}
}
}

return computeMemUsageEstimate(memUsage, samplesVisited, totalProps), computeMemUsageEstimate(newMemUsage, samplesVisited, totalProps), nil
}

func (o *objectGoMapSimple) MemUsage(ctx *MemUsageContext) (uint64, uint64, error) {
mem, newMem, err := o.baseObject.MemUsage(ctx)
memUsage, newMemUsage, err := o.baseObject.MemUsage(ctx)
if err != nil {
return 0, 0, err
}

if ctx.ObjectPropsLenExceedsThreshold(len(o.data)) {
inc, newInc, err := o.estimateMemUsage(ctx)
memUsage += inc
newMemUsage += newInc
if err != nil {
return memUsage, newMemUsage, err
}
return memUsage, newMemUsage, nil
}

for key := range o.data {
mem += uint64(len(key)) + SizeString
newMem += uint64(len(key)) + SizeString
memValue, newMemValue, err := o._getStr(key).MemUsage(ctx)
mem += memValue
newMem += newMemValue
memUsage += uint64(len(key)) + SizeString
newMemUsage += uint64(len(key)) + SizeString
incr, newIncr, err := o._getStr(key).MemUsage(ctx)
memUsage += incr
newMemUsage += newIncr
if err != nil {
return mem, newMem, err
return memUsage, newMemUsage, err
}
if exceeded := ctx.MemUsageLimitExceeded(mem); exceeded {
return mem, newMem, nil
if exceeded := ctx.MemUsageLimitExceeded(memUsage); exceeded {
return memUsage, newMemUsage, nil
}
}
return mem, newMem, nil
return memUsage, newMemUsage, nil
}
78 changes: 65 additions & 13 deletions object_gomap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,13 @@ func TestGoMapMemUsage(t *testing.T) {
}

tests := []struct {
name string
val *objectGoMapSimple
memLimit uint64
expectedMem uint64
expectedNewMem uint64
errExpected error
name string
val *objectGoMapSimple
memLimit uint64
estimateThreshold int
expectedMem uint64
expectedNewMem uint64
errExpected error
}{
{
name: "should account for each key value pair given a non-empty object",
Expand All @@ -366,7 +367,8 @@ func TestGoMapMemUsage(t *testing.T) {
"test1": valueInt(99),
},
},
memLimit: 100,
memLimit: 100,
estimateThreshold: 100,
// baseObject overhead + len("testN") with string overhead + value
expectedMem: SizeEmptyStruct + ((5+SizeString)+SizeInt)*2,
// baseObject overhead + len("testN") with string overhead + value
Expand All @@ -384,7 +386,8 @@ func TestGoMapMemUsage(t *testing.T) {
"test1": 99,
},
},
memLimit: 100,
memLimit: 100,
estimateThreshold: 100,
// baseObject overhead + len("testN") with string overhead + value
expectedMem: SizeEmptyStruct + ((5+SizeString)+SizeInt)*2,
// baseObject overhead + len("testN") with string overhead + value
Expand All @@ -401,7 +404,8 @@ func TestGoMapMemUsage(t *testing.T) {
"test": nil,
},
},
memLimit: 100,
memLimit: 100,
estimateThreshold: 100,
// overhead + len("test") with string overhead + null
expectedMem: SizeEmptyStruct + (4 + SizeString) + SizeEmptyStruct,
// overhead + len("test") with string overhead + null
Expand All @@ -418,7 +422,8 @@ func TestGoMapMemUsage(t *testing.T) {
"test": nestedMap,
},
},
memLimit: 100,
memLimit: 100,
estimateThreshold: 100,
// overhead + len("testN") with string overhead + (Object prototype with overhead + values with string overhead)
expectedMem: SizeEmptyStruct + (4 + SizeString) + nestedMapMemUsage,
// overhead + len("testN") with string overhead + (Object prototype with overhead + values with string overhead)
Expand All @@ -435,7 +440,8 @@ func TestGoMapMemUsage(t *testing.T) {
"test": &nestedMap,
},
},
memLimit: 100,
memLimit: 100,
estimateThreshold: 100,
// overhead + len("testN") with string overhead + nested overhead
expectedMem: SizeEmptyStruct + (4 + SizeString) + SizeEmptyStruct,
// overhead + len("testN") with string overhead + nested overhead
Expand All @@ -457,18 +463,64 @@ func TestGoMapMemUsage(t *testing.T) {
"test5": valueInt(99),
},
},
memLimit: 0,
memLimit: 0,
estimateThreshold: 100,
// baseObject overhead + len("testN") with string overhead + value
expectedMem: SizeEmptyStruct + ((5 + SizeString) + SizeInt),
// baseObject overhead + len("testN") with string overhead + value
expectedNewMem: SizeEmptyStruct + ((5 + SizeString) + SizeInt),
errExpected: nil,
},
{
name: "should estimate mem usage when exceeding object props len threshold",
val: &objectGoMapSimple{
baseObject: baseObject{
val: &Object{runtime: vm},
},
data: map[string]interface{}{
"test0": valueInt(99),
"test1": valueInt(99),
"test2": valueInt(99),
"test3": valueInt(99),
"test4": valueInt(99),
"test5": valueInt(99),
"test6": valueInt(99),
"test7": valueInt(99),
"test8": valueInt(99),
"test9": valueInt(99),
"testa": valueInt(99),
"testb": valueInt(99),
},
},
memLimit: 0,
estimateThreshold: 10,
// baseObject overhead + len("testN") with string overhead + value
expectedMem: SizeEmptyStruct + ((5+SizeString)+SizeInt)*12,
// baseObject overhead + len("testN") with string overhead + value
expectedNewMem: SizeEmptyStruct + ((5+SizeString)+SizeInt)*12,
errExpected: nil,
},
{
name: "should estimate mem usage given empty object nad negative threshold",
val: &objectGoMapSimple{
baseObject: baseObject{
val: &Object{runtime: vm},
},
data: map[string]interface{}{},
},
memLimit: 0,
estimateThreshold: -1,
// baseObject overhead
expectedMem: SizeEmptyStruct,
// baseObject overhead
expectedNewMem: SizeEmptyStruct,
errExpected: nil,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
total, newTotal, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, tc.memLimit, 100, 100, nil))
total, newTotal, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, tc.memLimit, 100, tc.estimateThreshold, nil))
if err != tc.errExpected {
t.Fatalf("Unexpected error. Actual: %v Expected: %v", err, tc.errExpected)
}
Expand Down
66 changes: 57 additions & 9 deletions object_goslice.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,24 +350,72 @@ func (o *objectGoSlice) swap(i int, j int) {
(*o.data)[i], (*o.data)[j] = (*o.data)[j], (*o.data)[i]
}

func (o *objectGoSlice) estimateMemUsage(ctx *MemUsageContext) (estimate uint64, newEstimate uint64, err error) {
var samplesVisited, memUsage, newMemUsage uint64
counter := 0
sliceLen := len(*o.data)
if sliceLen == 0 {
return memUsage, newMemUsage, nil
}
sampleSize := sliceLen / 10

// grabbing one sample every "sampleSize" to provide consistent
// memory usage across function executions
for _, datum := range *o.data {
counter++
if counter%sampleSize == 0 {
Gabri3l marked this conversation as resolved.
Show resolved Hide resolved
if datum == nil {
continue
}

inc, newInc, err := o.val.runtime.ToValue(datum).MemUsage(ctx)
samplesVisited += 1
memUsage += inc
newMemUsage += newInc
// average * number of a.values
estimate = uint64((float32(memUsage) / float32(samplesVisited)) * float32(sliceLen))
newEstimate = uint64((float32(newMemUsage) / float32(samplesVisited)) * float32(sliceLen))
if err != nil {
return estimate, newEstimate, err
}
if exceeded := ctx.MemUsageLimitExceeded(estimate); exceeded {
return estimate, newEstimate, nil
}
}
}

return estimate, newEstimate, nil
}

func (o *objectGoSlice) MemUsage(ctx *MemUsageContext) (uint64, uint64, error) {
mem, newMem, err := o.baseObject.MemUsage(ctx)
memUsage, newMemUsage, err := o.baseObject.MemUsage(ctx)
if err != nil {
return 0, 0, err
}
if o.data == nil {
return mem, newMem, nil
return memUsage, newMemUsage, nil
}

if ctx.ArrayLenExceedsThreshold(len(*o.data)) {
inc, newInc, err := o.estimateMemUsage(ctx)
memUsage += inc
newMemUsage += newInc
if err != nil {
return memUsage, newMemUsage, err
}
return memUsage, newMemUsage, nil
}

for _, datum := range *o.data {
memValue, newMemValue, err := o.val.runtime.ToValue(datum).MemUsage(ctx)
mem += memValue
newMem += newMemValue
inc, newInc, err := o.val.runtime.ToValue(datum).MemUsage(ctx)
memUsage += inc
newMemUsage += newInc
if err != nil {
return mem, newMem, err
return memUsage, newMemUsage, err
}
if exceeded := ctx.MemUsageLimitExceeded(mem); exceeded {
return mem, newMem, nil
if exceeded := ctx.MemUsageLimitExceeded(memUsage); exceeded {
return memUsage, newMemUsage, nil
}
}
return mem, newMem, nil
return memUsage, newMemUsage, nil
}
Loading
Loading