[STAL-1960] Fix performance issues introduced by creating new v8 Contexts #416
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
What problem are you trying to solve?
TL;DR fixing some performance regressions I introduced.
With #398, we implemented
scoped_execute
, which runs a script within av8::Context
that is a "blank slate" across executions, so no script can mutate the state in a way that persists to another execution.I did this by creating a new
v8::Context
for every execution, with the justification that the overall performance hit was acceptable.However, I made some mistakes in that profiling, and upon re-visiting, it looks like the impact is significant (~15% performance penalty).
Before (122 seconds
v8 spends a lot of time creating the new contexts (~11% of observed frames).
What is your solution?
We need two things:
Technical Details
We now re-use the same
v8::Context
across executions. To achieve an equivalent level of encapsulation as creating a new context, we employ two strategies:Freeze the global object and set its prototype
In JavaScript, calling
Object.freeze(/* obj */)
on an object prevents new properties from being added, existing properties from being modified in any way, and the object's prototype from being changed (reference). We freeze the v8 context's default global object to satisfy #1. However, to satisfy #2, we can't set our bridge variables on the global object directly (because a frozen object cannot be mutated by either JS or Rust). Instead, we set our own persistent object as the prototype of thev8::Context
's global object. When a script accesses, e.g. a ddsa bridge variable name, it will walk the prototype chain and resolve to a value on our persistent object, allowing us to achieve #2.Wrap rule code in an anonymous function
An anonymous function provides encapsulation by creating a separate scope for all code within it.
For example, a function declaration is effectively an assignment to the scope's most immediate global object:
Because we've frozen
globalThis
, a standard function declaration won't work. Instead, we have to enter a different scope with an anonymous function:This prevents local variable declarations from applying to/mutating the v8 context we're re-using.
"use strict";
We set the script to run in strict mode. This is desirable because it makes code that attempts to write to an unwritable object (e.g. the frozen
globalThis
) throw an error (instead of silently fail).After (107 seconds
Alternatives considered
What the reviewer should know