-
Notifications
You must be signed in to change notification settings - Fork 22
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-22214: capture memory used by objectGoMapSimple and objectGoSlice #88
Conversation
value.go
Outdated
case *objectGoMapSimple: | ||
return SizeEmpty, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] The fact that no test is failing worries me as this is a "breaking" change. Can we add tests even just around this MemUsage
function? You can look at toValue
to see how it's used or leverage that directly in the test body.
I think this will require to add a MemUsage
function to objectGoMapSimple
. But in order to calculate it you would call the MemUsage
function from the embedded baseObject
+ you have to calculate it for the data
field.
The catch here is that you have to loop through key/values and calculate the memory usage for each pair. Keys mem usage is calculated as uint64(len(k)) + SizeString
, but SizeString
is not available yet and it's part of my PR (you can just copy paste it here, it's a constant value anyway). For the values it's probably easier to convert them to a value (call ToValue) and then use the MemUsage
on them (I think you can just call o._getStr(k)
and use the return value to call MemUsage
on it.
Let me know if you have any questions!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Roger, Hmmm okay, yeah it appeared to be incrementing the fields when I ran this in the debugger against a VM test, but it also didn't fail the test. I'm now wondering if the MemUsage is already managing to take into account what's contained in the map... I'll write some tests as I investigate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran the debugger for the BAAS test TestMemUsageBSON
found here. I noticed it looks like if we add this we will also start tracking the memory used by our context
, console
, etc... For example:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't sure if this would be an unexpected change and whether we want to capture it or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wanted to confirm if you are correct, for my example test here, it looks like the default memory usage implementation for this object is substantially larger than if we implemented it ourselves. evg result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The much higher usage is normal, if you look at some of tests in memory_test.go you see that we do a diff of mem usages there because of the common objects that get initialized with a VM (and other init stuff). You can get an idea of it by looking at the toValue function where the init() function gets called on the objectGoMapSimple
struct.
In this case to get a clear understanding of the mem usage for it you can look at my PR here to see how to test this out. I'd recommend instantiating the struct in the test, something like:
m := &objectGoMapSimple{
baseObject: baseObject{
val: &Object{runtime: vm},
},
data: origMap,
}
and then possibly call the MemUsage(memCtx)
function on it. This way you isolate it from anything around it and make sure your calculations are correct. If we end up lazy loading the objects we inject to the VM (console, services, ...) then all is good, otherwise we will need to subtract our objects mem usage from the overall calculation but this would be done in baas
. We will have to check with the function engine improvements project to make sure. Let me know if that doesn't make sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You won the lottery and get to write another MemUsage function! Thank you for looking into this and navigating Goja!
object_gomap_test.go
Outdated
{ | ||
name: "should account for nested parsed map", | ||
threshold: 100, | ||
val: &objectGoMapSimple{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: map[string]interface{}{ | ||
"test": &objectGoMapSimple{ // <- this is treated as a objectGoMapReflect | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: map[string]interface{}{ | ||
"subTest1": valueInt(99), | ||
"subTest2": valueInt(99), | ||
"subTest3": valueInt(99), | ||
"subTest4": valueInt(99), | ||
}, | ||
}, | ||
}, | ||
}, | ||
// overhead + len("test") + reflectedMap | ||
expectedMem: SizeEmptyStruct + 4 + SizeEmptyStruct, | ||
// overhead + len("test") with string overhead + reflectedMap | ||
expectedNewMem: SizeEmptyStruct + 4 + SizeString + SizeEmptyStruct, | ||
errExpected: nil, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Q] is this test case even possible/valid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, this type is only generated when as a goja user I call ToValue on a map[string]interface{} but the type is otherwise not exposed.
Forgot to add more to this, this test is still valid but only if instead of using a reference to objectGoMapSimple we use a reference to a map[string]interfeace{} that's more realistic to what a user could do. That's where it would get converted to objectGoMapReflect. If you look at the ToValue function, there's a switch case at the bottom that resolves to create a objectGoMapReflect item.
I think from the look of it that the memory usage of the entity pointed to should still be computed because the ToValue method extracts the element pointed to and instantiates an objectSomethingSomething based on its type. This will use memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK if you only want to focus on goMapSimple and goSlice with this PR and we can do the rest in a dedicated ticket.
object_gomap_test.go
Outdated
{ | ||
name: "should account for nested key value pairs", | ||
threshold: 100, | ||
val: &objectGoMapSimple{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: map[string]interface{}{ | ||
"test": map[string]interface{}{ // is this ever valid/should we test this situation? | ||
"subTest1": valueInt(99), | ||
"subTest2": valueInt(99), | ||
"subTest3": valueInt(99), | ||
"subTest4": valueInt(99), | ||
}, | ||
}, | ||
}, | ||
// overhead + len("test") + VM runtime base object + values | ||
expectedMem: SizeEmptyStruct + 4 + 2204 + ((8 + SizeInt) * 4), | ||
// overhead + len("testN") with string overhead + VM runtime base object with overhead + values with string overhead | ||
expectedNewMem: SizeEmptyStruct + (4 + SizeString) + 4716 + ((8 + SizeString + SizeInt) * 4), | ||
errExpected: nil, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Q] Same as above, is this test case even possible/valid?
Also just want to point out that when nested there is a significant size difference? Is this because there is like a duplicate vm/runtime instance. I tried following down the debug breakpoint rabbit hole, but definitely gets confusing with all the recursive calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a valid test case and I'm looking to see why there's so much additional memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! The problem here is that when we loop through the "test" map, we rely on our MemUsage
function to convert it to a value (_getStr
calls ToValue
), but if you look closely:
// ...other stuff inside ToValue
case map[string]interface{}:
if i == nil {
return _null
}
obj := &Object{runtime: r}
m := &objectGoMapSimple{
baseObject: baseObject{
val: obj,
extensible: true,
},
data: i,
}
obj.self = m
m.init() // <-- uh oh
return obj
There's a call to init
which then sets the prototype to o.val.runtime.global.ObjectPrototype
which is pretty large. That said this is totally fine, I'll review the rest and I think we should keep this test because this is a plausible scenario for a user to run into. We don't see this much in other tests because most of them are for goja types which do not need a conversion with ToValue
.
object_goslice_test.go
Outdated
{ | ||
name: "should account for objectGoSlice", // treated as a objectGoSliceReflect | ||
threshold: 100, | ||
val: &objectGoSlice{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: &[]interface{}{ | ||
&objectGoSlice{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: &[]interface{}{ | ||
valueInt(99), | ||
valueInt(99), | ||
valueInt(99), | ||
valueInt(99), | ||
}, | ||
}, | ||
}, | ||
}, | ||
// overhead + nested overhead | ||
expectedMem: SizeEmptyStruct + SizeEmptyStruct, | ||
// overhead + nested overhead | ||
expectedNewMem: SizeEmptyStruct + SizeEmptyStruct, | ||
errExpected: nil, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Q] Similar to the map Q of nested pre-parsed objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also similar and I would use a pointer to a slice of interfaces rather than the internal objectGoSlice type. Again this test is correct but if we use a pointer it better represent what a user could do (where the user is us in baas eheh)
object_goslice_test.go
Outdated
{ | ||
name: "should account for nested slices", | ||
threshold: 100, | ||
val: &objectGoSlice{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: &[]interface{}{ | ||
[]interface{}{ | ||
valueInt(99), | ||
valueInt(99), | ||
valueInt(99), | ||
valueInt(99), | ||
}, | ||
}, | ||
}, | ||
// overhead + (value + len("length") + "length".value + prototype + ints) | ||
expectedMem: SizeEmptyStruct + (SizeEmptyStruct + 6 + SizeEmptyStruct + (SizeEmptyStruct + SizeEmptyStruct) + SizeNumber*4), | ||
// overhead + (value + len("length") with string overhead + "length".value + prototype + ints) | ||
expectedNewMem: SizeEmptyStruct + (SizeEmptyStruct + (6 + SizeString) + SizeEmptyStruct + (SizeEmptyStruct + SizeEmptyStruct) + SizeNumber*4), | ||
errExpected: nil, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Q] Similar Q to the nested maps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is valid test case for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is a possible scenario but I would only go one layer down like you did here.
@Gabri3l mind leaving a comment where you are at with investigating the answer to my questions? |
Adding a few more people to the PR as I'm catching up with a few things. I'll try and dive into this as soon as I can. |
// the baseObject is quite large when ToValue is called due to the functions on the object | ||
// calculating ahead of time for test case | ||
nestedMapAsObject := vm.ToValue(nestedMap) | ||
nestedMapMemUsage, nestedMapNewMemUsage, err := nestedMapAsObject.MemUsage(vmCtx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nice!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for tackling this, it's more challenging than what it appeared. Left a few comments to get the ball rolling!
object_gomap_test.go
Outdated
{ | ||
name: "should account for nested reflect object", | ||
threshold: 100, | ||
val: &objectGoMapSimple{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: map[string]interface{}{ | ||
"test": &objectGoMapSimple{ | ||
baseObject: baseObject{ | ||
val: &Object{runtime: vm}, | ||
}, | ||
data: map[string]interface{}{ | ||
"subTest1": valueInt(99), | ||
"subTest2": valueInt(99), | ||
"subTest3": valueInt(99), | ||
"subTest4": valueInt(99), | ||
}, | ||
}, | ||
}, | ||
}, | ||
// overhead + len("test") + reflectObject | ||
expectedMem: SizeEmptyStruct + 4 + SizeEmptyStruct, | ||
// overhead + len("test") with string overhead + reflectObject | ||
expectedNewMem: SizeEmptyStruct + 4 + SizeString + SizeEmptyStruct, | ||
errExpected: nil, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[minor] Kind of like I mentioned in the answers to your questions I would convert this to have "test": &nestedMap
object_goslice_test.go
Outdated
|
||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
total, newTotal, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, 100, 100, tc.threshold, nil)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[minor] IF you feel adventurous and want to add estimates to the objectGoSlice
mem usage functions then you would use the threshold on the array and not on the object props like you did here. Otherwise you can just remove the threshold field in the test struct altogether and hardcode it.
total, newTotal, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, 100, 100, tc.threshold, nil)) | |
total, newTotal, err := tc.val.MemUsage(NewMemUsageContext(vm, 100, 100, tc.threshold, 100, nil)) |
return 0, 0, err | ||
} | ||
for _, datum := range *o.data { | ||
memValue, newMemValue, err := o.val.runtime.ToValue(datum).MemUsage(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[q] this could be an expensive operation for large arrays. Should we estimate the usage instead? Example here. Same question for the objectGoMapSimple
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nathan just filed https://jira.mongodb.org/browse/BAAS-24348 which I will pick up pretty much right away!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd still like for the two unresolved changes to be made, then we're good to go!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work digging into this, thank you so much!
No description provided.