From dc629db4686ebb8f7beaf9f17415db8d03f42b17 Mon Sep 17 00:00:00 2001 From: ishabi Date: Wed, 8 Jul 2026 17:06:49 +0200 Subject: [PATCH 1/2] restore original heap size --- bindings/profilers/heap.cc | 3 ++ ts/test/oom-restore-heap-limit.ts | 85 +++++++++++++++++++++++++++++++ ts/test/test-heap-profiler.ts | 30 +++++++++++ 3 files changed, 118 insertions(+) create mode 100644 ts/test/oom-restore-heap-limit.ts diff --git a/bindings/profilers/heap.cc b/bindings/profilers/heap.cc index f7aaf774..104d1355 100644 --- a/bindings/profilers/heap.cc +++ b/bindings/profilers/heap.cc @@ -95,6 +95,9 @@ struct HeapProfilerState { } if (isolate) { isolate->AddNearHeapLimitCallback(&NearHeapLimit, nullptr); + // Restore 90% of the original heap limit when possible. + constexpr double kHeapLimitRestoreThreshold = 0.90; + isolate->AutomaticallyRestoreInitialHeapLimit(kHeapLimitRestoreThreshold); callbackInstalled = true; } } diff --git a/ts/test/oom-restore-heap-limit.ts b/ts/test/oom-restore-heap-limit.ts new file mode 100644 index 00000000..c053a03c --- /dev/null +++ b/ts/test/oom-restore-heap-limit.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as v8 from 'v8'; + +import {heap} from '../src/index'; + +const MB = 1024 * 1024; +const LIMIT_TOLERANCE = 16 * MB; +const CHUNK_SIZE = 4 * MB; +const gc = (global as typeof globalThis & {gc?: () => void}).gc; + +function heapLimit() { + return v8.getHeapStatistics().heap_size_limit; +} + +function allocateChunk() { + const chunk = new Array(CHUNK_SIZE / 8); + for (let i = 0; i < chunk.length; i++) { + chunk[i] = i + 0.1; + } + return chunk; +} + +async function main() { + if (!gc) { + throw new Error('This test must be run with --expose-gc.'); + } + + heap.start(MB, 64); + try { + heap.monitorOutOfMemory(64 * MB, 1, false); + + const initialLimit = heapLimit(); + const retained: number[][] = []; + + while ( + heapLimit() <= initialLimit + LIMIT_TOLERANCE && + retained.length < 64 + ) { + retained.push(allocateChunk()); + } + + const increasedLimit = heapLimit(); + if (increasedLimit <= initialLimit + LIMIT_TOLERANCE) { + throw new Error(`Heap limit did not increase from ${initialLimit}.`); + } + + retained.length = 0; + for (let i = 0; i < 100; i++) { + gc(); + if (heapLimit() <= initialLimit + LIMIT_TOLERANCE) { + return; + } + await new Promise(resolve => setTimeout(resolve, 20)); + } + + throw new Error( + `Heap limit increased to ${increasedLimit} but did not restore to ` + + `${initialLimit}.`, + ); + } finally { + heap.stop(); + } +} + +main().catch(err => { + console.error(err.stack || err.message); + process.exitCode = 1; +}); diff --git a/ts/test/test-heap-profiler.ts b/ts/test/test-heap-profiler.ts index 34d58ebe..2bb2c3e4 100644 --- a/ts/test/test-heap-profiler.ts +++ b/ts/test/test-heap-profiler.ts @@ -331,6 +331,36 @@ describe('HeapProfiler', () => { }); describe('OOMMonitoring', () => { + it('should restore heap limit after v8 recovers from OOM', async function () { + this.timeout(30000); + + const proc = fork(path.join(__dirname, 'oom-restore-heap-limit.js'), { + execArgv: ['--expose-gc', '--max-old-space-size=64'], + silent: true, + }); + let output = ''; + + proc.stdout?.on('data', chunk => { + output += chunk; + }); + proc.stderr?.on('data', chunk => { + output += chunk; + }); + + await new Promise((resolve, reject) => { + proc.on('error', reject); + proc.on('exit', code => { + if (code === 0) { + resolve(); + } else { + reject( + new Error(`oom-restore-heap-limit exited with ${code}\n${output}`), + ); + } + }); + }); + }); + it('should call external process upon OOM', async function () { // this test is very slow on some configs (asan/valgrind) this.timeout(20000); From 76c11a963c455e62c7ee79737d8e08d1b294d2f7 Mon Sep 17 00:00:00 2001 From: ishabi Date: Thu, 9 Jul 2026 13:27:19 +0200 Subject: [PATCH 2/2] clarify AutomaticallyRestoreInitialHeapLimit usage comment --- bindings/profilers/heap.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bindings/profilers/heap.cc b/bindings/profilers/heap.cc index 104d1355..e4ee8cf3 100644 --- a/bindings/profilers/heap.cc +++ b/bindings/profilers/heap.cc @@ -95,7 +95,9 @@ struct HeapProfilerState { } if (isolate) { isolate->AddNearHeapLimitCallback(&NearHeapLimit, nullptr); - // Restore 90% of the original heap limit when possible. + // Restore the original heap limit once live old-generation usage falls + // below 90% of the original limit. The threshold controls when V8 + // restores the limit, not the restored limit size. constexpr double kHeapLimitRestoreThreshold = 0.90; isolate->AutomaticallyRestoreInitialHeapLimit(kHeapLimitRestoreThreshold); callbackInstalled = true;