[SCP-759] Fix profiler shutdown when isolate is terminated#215
Conversation
BenchmarksBenchmark execution time: 2025-06-10 09:16:55 Comparing candidate commit 17343f3 in PR branch Found 2 performance improvements and 0 performance regressions! Performance is the same for 88 metrics, 30 unstable metrics. scenario:profiler-light-load-no-wall-profiler-22
scenario:profiler-light-load-with-wall-profiler-24
|
2da68aa to
488b05f
Compare
Overall package sizeSelf size: 1.58 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | source-map | 0.7.4 | 226 kB | 226 kB | | pprof-format | 2.1.0 | 111.69 kB | 111.69 kB | | p-limit | 3.1.0 | 7.75 kB | 13.78 kB | | delay | 5.0.0 | 11.17 kB | 11.17 kB | | node-gyp-build | 3.9.0 | 8.81 kB | 8.81 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
87e6b98 to
f9a54c2
Compare
| it('should not crash when worker is terminated', async function () { | ||
| this.timeout(20000); | ||
| const nruns = 5; | ||
| const concurrentWorkers = 100; | ||
| for (let i = 0; i < nruns; i++) { | ||
| const workers = []; | ||
| for (let j = 0; j < concurrentWorkers; j++) { | ||
| const worker = new Worker('./out/test/worker2.js') | ||
| worker.postMessage('hello') | ||
|
|
||
| worker.on('message', () => { | ||
| worker.terminate() | ||
| }) | ||
|
|
||
| workers.push(new Promise<void>((resolve, reject) => { | ||
| worker.on('exit', (exitCode) => { | ||
| if (exitCode === 1) { | ||
| resolve() | ||
| } else { | ||
| reject(new Error('Worker exited with code 0')) | ||
| } | ||
| }) | ||
| })) | ||
| } | ||
| await Promise.all(workers) | ||
| } | ||
| }); |
There was a problem hiding this comment.
a03101a to
10b3b89
Compare
When an isolate is terminated abruptly (eg. by Worker.terminate()), the profiler is not stopped and not removed from the isolate->profiler map. This can lead to a crash. This commit adds a cleanup hook with `node::AddEnvironmentCleanupHook` that is called when the isolate is destroyed. The cleanup hook stops the profiler and removes it from the profiler map.
10b3b89 to
ab2ddb2
Compare
szegedi
left a comment
There was a problem hiding this comment.
Overall looks great! Going to approve as-is but I wouldn't mind if you considered my suggestion for that if (isolate != nullptr) block in Dispose.
| cpuProfiler_ = nullptr; | ||
|
|
||
| g_profilers.RemoveProfiler(isolate, this); | ||
| if (removeFromMap) { |
There was a problem hiding this comment.
You could have one big if (isolate != nullptr) block here, and move both this and the if (collectAsyncId_) logic (and the cleanup hook removal) below into it. I know they're no-ops when isolate is null, but I think it'd read better as in "here's what is executed when isolate is known".
There was a problem hiding this comment.
I removed the call to Dispose from destructor
There was a problem hiding this comment.
Well, that's one way to simplify it 😁. I like it. I did feel our disposal and destruction story was a bit overcomplicated, so this is 👍.
There was a problem hiding this comment.
Dispose call in destructor felt like a desperate attempt to clean up, now that we have the environment cleanup hook, this should not be needed anymore (and I am not even sure in which cases, other than profiler stop, the destructor is called).
| } | ||
|
|
||
| std::string WallProfiler::StartInternal() { | ||
| v8::ProfilerId WallProfiler::StartInternal() { |
|
|
||
| WallProfiler::~WallProfiler() { | ||
| Dispose(nullptr); | ||
| Dispose(nullptr, true); |
There was a problem hiding this comment.
isolate == nullptr will make g_profilers.RemoveProfiler(isolate, this); in Dispose a no-op anyway so you might as well be passing false. On the other hand, if we do the transformation in Dispose I suggested then it'd get short-circuited by the nullptr anyway so it doesn't matter what's the value here.
There was a problem hiding this comment.
I think we should remove this call to Dispose, the only case when it's not a no-op is when dispose is called on a running profiler, and that should be an error.
| return V8toEpochOffset; | ||
| } | ||
|
|
||
| void WallProfiler::CleanupHook(void* data) { |
There was a problem hiding this comment.
Making this a private static method on WallProfiler actually isn't a bad idea. I should probably do an equivalent for GC callbacks so I can make them private.
| it('should not crash when worker is terminated', async function () { | ||
| this.timeout(30000); | ||
| const nruns = 5; | ||
| const concurrentWorkers = 20; | ||
| for (let i = 0; i < nruns; i++) { | ||
| const workers = []; | ||
| for (let j = 0; j < concurrentWorkers; j++) { | ||
| const worker = new Worker('./out/test/worker2.js'); | ||
| worker.postMessage('hello'); | ||
|
|
||
| worker.on('message', () => { | ||
| worker.terminate(); | ||
| }); | ||
|
|
||
| workers.push( | ||
| new Promise<void>((resolve, reject) => { | ||
| worker.on('exit', exitCode => { | ||
| if (exitCode === 1) { | ||
| resolve(); | ||
| } else { | ||
| reject(new Error('Worker exited with code 0')); | ||
| } | ||
| }); | ||
| }) | ||
| ); | ||
| } | ||
| await Promise.all(workers); | ||
| } | ||
| }); |
There was a problem hiding this comment.
* Fix profiler shutdown when isolate is terminated When an isolate is terminated abruptly (eg. by Worker.terminate()), the profiler is not stopped and not removed from the isolate->profiler map. This can lead to a crash. This commit adds a cleanup hook with `node::AddEnvironmentCleanupHook` that is called when the isolate is destroyed. The cleanup hook stops the profiler and removes it from the profiler map.
* Fix profiler shutdown when isolate is terminated When an isolate is terminated abruptly (eg. by Worker.terminate()), the profiler is not stopped and not removed from the isolate->profiler map. This can lead to a crash. This commit adds a cleanup hook with `node::AddEnvironmentCleanupHook` that is called when the isolate is destroyed. The cleanup hook stops the profiler and removes it from the profiler map.
What does this PR do?:
When an isolate is terminated abruptly (eg. by Worker.terminate()), the profiler is not stopped and not removed from the isolate->profiler map. This can lead to a crash.
This commit adds a cleanup hook with
node::AddEnvironmentCleanupHookthat is called when the isolate is destroyed.The cleanup hook stops the profiler and removes it from the profiler map.