I'm running a very simple javascript script in V8, embedded in a C++ program. The script is just a number literal: '12'. However, when I run this script many times in a loop, my application will consume several gigabytes of memory after a 20-30 seconds. I'm using V8 7.5.160, built under windows 10 with Visual Studio 2017.
The reason I was running this code was to get a feel for the function call overhead when executing javascript from C++. I'm investigating using V8 as a scripting engine for an application that will call many small scripts at a very high rate. But the memory usage has me worried.
I've looked for ways to trigger the garbage collector manually. But as far as I understand this should not be necessary (and I couldn't find how to do it). But I'm more interested in if I can prevent this memory usage in the first place.
I used the example code from the V8 embedding documentation (hello-world.cc), with a different script, running the script in a loop and timing the time it takes to do the calls:
// V8 setup code, unchanged from the V8 embedding hello-world.cc.
// My script:
v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, "12", v8::NewStringType::kNormal).ToLocalChecked();
// Compile script:
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
// Then this code in two nested loops, where the inner loop times the time it takes.
// The outer loop runs 20 times. The inner loop runs 100.000.000 times.
// Inside the inner loop there is only this line of code:
script->Run(context).ToLocalChecked();
I was expecting this code not to cause such high memory usage. Or at least, I would have expected the garbage collector to keep the memory usage low.
I'd be grateful for insights into what causes the memory usage for this script, and ways to prevent this from happening.
SOLVED:
The solution provided by jmrk(thanks!) solved my issue.
I split my inner loop that runs 100.000.000 times into two loops; an outer loop with 100.000 iterations and an inner loop with 1000 iterations. Before this inner loop I do v8::HandleScope temp_scope(isolate);
and now there is no more extreme memory usage. The application now stays at 2.3MB used.
The overall time per script run has not gone up either. Instead, it has slightly decreased from 85ns to 82ns.
Try using short-lived HandleScope
s around your invocations:
for (int i = 0; i < 20; i++) {
/* Record start time */
for (int j = 0; j < 1000; j++) {
v8::HandleScope temp_scope(isolate);
for (int k = 0; k < 100000; k++) {
script->Run(context).ToLocalChecked();
}
}
/* Record end time */
}
The background is that .ToLocalChecked()
creates a new v8::Local
, whose contents (=the result of the script invocation) is kept alive as long as the current HandleScope
is around. So by creating many new handles to many new objects all inside one HandleScope
, you're effectively disabling garbage collection.
The tradeoff is that HandleScope
creation/destruction has a (small) performance cost, but recreating them frequently makes more memory eligible for garbage collection (and also slightly speeds up garbage collection itself). As a rule of thumb, I'd aim for about 1,000 to 1,000,000 handles per HandleScope
.