「Memory Leak Regression Testing With V8 Node.js Part 2 - Finalizer-Primarily Based Testing」の版間の差分
(ページの作成:「<br>Within the previous weblog publish, I talked about how Node.js used memory usage measurement to check in opposition to memory leaks. Generally that’s adequate to su…」) |
(相違点なし)
|
2025年11月11日 (火) 20:58時点における最新版
Within the previous weblog publish, I talked about how Node.js used memory usage measurement to check in opposition to memory leaks. Generally that’s adequate to supply legitimate tests. Sometimes we would like the test to be extra precises and deal with the standing of specific objects. This can be quite tricky with what’s obtainable to interact with V8’s garbage collector. One common technique utilized by Node.js core check suites relies on the native v8::PersistentBase::SetWeak() API to invoke a "finalizer" when the observed object is rubbish collected. 3. At course of exit, if the callback set in 1 sees that the finalizer has not been invoked for MemoryWave Official sufficient times, the item is taken into account leaking. The onGC() helper was introduced earlier than the FinalizationRegistry API became accessible to JavaScript. It basically serves the same goal as FinalizationRegistry and invokes the ongc() callback for the first argument as a finializer. It's implemented with Node.js’s destroy async hook which is in flip implemented with the v8::PersistentBase::SetWeak() API mentioend before.
The FinalizationRegistry API (part of the WeakRef proposal) has been shipped since V8 8.4. This roughly serves the same purpose as the onGC() helper described above, but the callbacks are invoked via a mechanism completely different from that of the weak callback’s. Compared to weak callbacks, the invocation of finalization registry callbacks normally happens later and is less predictable. That is by-design to offer JS engines extra leeway within the scheduling of the callback and avoid hurting performance. Technically the JS engine doesn't even should invoke the callback (the identical can also be mentioned about weak callbacks, but they are less advanced anyway). Finalizers are tricky business and it's best to avoid them. They can be invoked at unexpected times, or not in any respect… The proposed specification allows conforming implementations to skip calling finalization callbacks for any reason or no reason. In observe though, the callback would solely be called for ninety nine times by the point the exit event is emitted - a minimum of when i examined it domestically.
As I’ve analyzed in one other weblog put up, the false positives of Jest’s --deteck-leaks (which relies on FinalizatioRegistry) showed that you cannot use gc() to ensure finalization registry callbacks to be called for each object ever registered when they are garbage collected, even if you happen to go so far as running gc() for 10 times asynchronously, Memory Wave as a result of that’s not what they are designed for in the primary place. Finally, this depends upon the regression that you're testing in opposition to. If the leak reproduces reliably with each repeated operation that you're testing, one non-leaking pattern could already give you 90% confidence that you’ve fastened it and it’s not regressing once more. In fact, you would possibly desire a 100% confidence and verify this with every sample, however given that observing finalization with a garbage collector can already give you false positives by design, a much less precise test with much less false positives is better than a more precise check with extra false positives.
As I’ve talked about in the other blog publish, a easy gc() is normally not sufficient to scrub up as many objects and invoke as many callbacks as potential, because it’s merely not designed for that. Operating it a number of times or conserving the thread working for a bit (in Node.js, using setImmediate() to maintain the occasion loop alive) can generally give V8 sufficient nudges to run your finalizers for unreachable objects (which was what Jest’s --detect-leaks did), however sometimes these methods are nonetheless not enough. In that case, if you count on the finalizers to let you know whether or not your object can be collected or not, and consider the absence of finalizer invocations to be a sign of leaks, then you're going to have false positives. There is one other caveat with gc() - if the graph being checked includes newly compiled functions/scripts, and you're assuming that V8 can collect them when they are not reachable by users (which does occur normally), then the usage of gc() can chew you in the again because a compelled GC induced by gc() alone can forestall them from being rubbish collected.
That’s intentional, as a result of gc() is a V8 internal API that only caters to V8’s own testing wants, which incorporates this conduct. That said, generally it’s still inevitable for the regression checks to pressure the garbage assortment someway. Is there a extra dependable different to gc()? Nicely, one hack used by some of Node.js’s checks as well as a later fix to Jest’s --detect-leaks is to take a heap snapshot to perform some some sort of final-resort garbage assortment. By design, MemoryWave Official a heap snapshot in supposed to seize what’s alive on the heap as accurately as possible, so taking it urges V8 to begin the rubbish collection with some extra operations to run as many finalizers as it may. The heap snapshot generation course of additionally clears the compilation cache, which might help clearing scripts that would not be in any other case collected if the GC is forced by gc(). This helper takes an object manufacturing facility fn(), and run it as much as maxCount times. Ideally the heap dimension restrict ought to even be set to a smaller value to give V8 some sense of emergency to wash the constructed objects up because the allocation occurs. If the FinalizationRegistry callback for any objects returned from fn() gets called during the process, we know that a minimum of some of these objects are collectable underneath memory pressure, then we're confident enough about disproving the leak and cease there. To provide V8 further nudges to invoke the finalizer, we’ll also take the heap snapshot at a specified frequency.