Skip to content

Commit 602b38c

Browse files
#96 Improve takeSnapshot
1 parent ad3c867 commit 602b38c

File tree

10 files changed

+127
-36
lines changed

10 files changed

+127
-36
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.5.0] - 2024-09-17
9+
10+
### Added
11+
12+
- `restartWorker` option to `takeSnapshot`
13+
- a promise return value to `takeSnapshot` settled to a created file path
14+
15+
### Changed
16+
17+
- `takeSnapshot` will no longer create a heap snapshot when there is not enough memory
18+
819
## [2.4.0] - 2021-12-03
920

1021
### Added

Readme.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,27 @@ Run CPU Profiler and save result on main process directory
7474

7575
<a name="WorkerNodes+takeSnapshot"></a>
7676

77-
### workerNodes.takeSnapshot() ⇒ <code>void</code>
77+
### workerNodes.takeSnapshot() ⇒ <code>Promise</code>
7878
Take Heap Snapshot and save result on main process directory
7979

80+
The operation will fail when there is not enough memory to create a snapshot.
81+
8082
**Kind**: instance method of [<code>WorkerNodes</code>](#WorkerNodes)
8183
<a name="WorkerNodes+getUsedWorkers"></a>
8284

85+
| Param | Type |
86+
|---------| --- |
87+
| options | <code>TakeSnapshotOptions</code> |
88+
89+
### options.restartWorker : <code>Boolean</code>
90+
91+
Orders a worker that was used to create a snapshot,
92+
to be immediately restarted.
93+
It's recommended to use this option,
94+
because V8 might persist the snapshot in memory until exit.
95+
96+
**Default**: <code>false</code>
97+
8398
### workerNodes.getUsedWorkers() ⇒ <code>Array.&lt;Worker&gt;</code>
8499
Return list with used workers in pool
85100

@@ -296,4 +311,4 @@ Unless required by applicable law or agreed to in writing, software
296311
distributed under the License is distributed on an "AS IS" BASIS,
297312
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
298313
See the License for the specific language governing permissions and
299-
limitations under the License.
314+
limitations under the License.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const v8 = require("v8");
2+
3+
module.exports = function mockHeapStatistics () {
4+
v8.getHeapStatistics = () => ({
5+
heap_size_limit: 100,
6+
used_heap_size: 55,
7+
})
8+
};

e2e/v8-profilers.base.js

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,41 @@ module.exports = function describe(workerType) {
1212
await workerNodes.call('hello!');
1313

1414
// when
15-
workerNodes.takeSnapshot();
16-
const getHeapSnapshotFilename = workerType === "thread" ?
17-
() => fs.readdirSync(process.cwd()).find(name => name.includes('.heapsnapshot') && name.includes(`-${process.pid}-`)) :
18-
() => fs.readdirSync(process.cwd()).find(name => name.includes('.heapsnapshot') && !name.includes(`-${process.pid}-`));
19-
await eventually(() => getHeapSnapshotFilename() !== undefined);
15+
const filePath = await workerNodes.takeSnapshot();
2016

21-
const result = getHeapSnapshotFilename();
22-
t.truthy(result);
23-
t.true(result.length > 0)
24-
fs.unlinkSync(result);
17+
t.regex(filePath, /^HeapSnapshot-\d+-\d+\.heapsnapshot$/);
18+
t.true(fs.existsSync(filePath))
19+
fs.unlinkSync(filePath);
20+
});
21+
22+
test.serial(`should restart worker after taking heap snapshot when restartWorker option was set`, async (t) => {
23+
// given
24+
const workerNodes = new WorkerNodes(fixture('echo-function-async'), { lazyStart: true, workerType });
25+
await workerNodes.ready();
26+
await workerNodes.call('hello!');
27+
28+
const workersBefore = workerNodes.getUsedWorkers();
29+
30+
// when
31+
const filePath = await workerNodes.takeSnapshot({ restartWorker: true });
32+
fs.unlinkSync(filePath);
33+
34+
// Waiting for a worker to restart
35+
await new Promise((resolve) => setTimeout(resolve, 500));
36+
37+
const workersAfter = workerNodes.getUsedWorkers();
38+
t.true(workersBefore.length === workersAfter.length);
39+
t.true(workersBefore[0] !== workersAfter[0]);
40+
});
41+
42+
test.serial(`should let takeSnapshot throw an error when there is not enough heap`, async (t) => {
43+
const workerNodes = new WorkerNodes(fixture('mock-heap-statistics'), { lazyStart: true, workerType });
44+
await workerNodes.ready();
45+
await workerNodes.call();
46+
47+
await t.throwsAsync(workerNodes.takeSnapshot(), {
48+
message: 'Not enough memory to perform heap snapshot'
49+
})
2550
});
2651

2752
test(`should generate heap profiler result file`, async (t) => {
@@ -34,8 +59,8 @@ module.exports = function describe(workerType) {
3459

3560
await workerNodes.call('hello!');
3661

37-
const getCpuProfileFilename = workerType === "thread" ?
38-
() => fs.readdirSync(process.cwd()).find(name => name.includes('.cpuprofile') && name.includes(`-${process.pid}-`)) :
62+
const getCpuProfileFilename = workerType === "thread" ?
63+
() => fs.readdirSync(process.cwd()).find(name => name.includes('.cpuprofile') && name.includes(`-${process.pid}-`)) :
3964
() => fs.readdirSync(process.cwd()).find(name => name.includes('.cpuprofile') && !name.includes(`-${process.pid}-`));
4065

4166
await eventually(() => getCpuProfileFilename() !== undefined);
@@ -46,4 +71,4 @@ module.exports = function describe(workerType) {
4671
t.true(result.length > 0)
4772
fs.unlinkSync(result);
4873
});
49-
}
74+
}

index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ interface Options {
1313
workerType?: "thread" | "process";
1414
}
1515

16+
interface TakeSnapshotOptions {
17+
restartWorker?: boolean;
18+
}
19+
1620
interface WorkerNodesInstance {
1721
call: CallProperty;
1822
ready: () => Promise<WorkerNodesInstance>;
1923
terminate: () => Promise<WorkerNodesInstance>;
2024
profiler: (duration?: number) => void;
21-
takeSnapshot: () => void;
25+
takeSnapshot: (options?: TakeSnapshotOptions) => void;
2226
getUsedWorkers: () => Array<Worker>;
2327
}
2428

lib/pool.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -355,17 +355,19 @@ class WorkerNodes extends EventEmitter {
355355

356356
/**
357357
* Take Heap Snapshot and save result on main process directory
358-
*
359-
* @returns {void}
358+
* @param {TakeSnapshotOptions} options
359+
* @returns {Promise<string>}
360360
*/
361-
takeSnapshot() {
361+
takeSnapshot(options = {}) {
362362
const worker = this.pickWorker();
363363

364364
if (worker) {
365-
worker.takeSnapshot();
365+
return worker.takeSnapshot(options);
366366
} else {
367-
// There might not be availble worker, let it start.
368-
setTimeout(() => this.takeSnapshot(), 500);
367+
return new Promise((resolve) => {
368+
// There might not be available worker, let it start.
369+
setTimeout(() => resolve(this.takeSnapshot()), 500);
370+
})
369371
}
370372
}
371373

lib/util/get-heap-snapshot.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
const v8 = require('v8');
2-
const fs = require('fs');
32

4-
const getHeapSnapshot = (callback) => {
5-
const stream = v8.getHeapSnapshot();
6-
const file = fs.createWriteStream(`HeapSnapshot-${process.pid}-${Date.now()}.heapsnapshot`);
7-
8-
stream.on('data', (chunk) => file.write(chunk));
3+
const hasEnoughMemory = () => {
4+
const heapStats = v8.getHeapStatistics();
5+
return heapStats.heap_size_limit >= heapStats.used_heap_size * 2;
6+
}
97

10-
stream.on('end', () => {
11-
if (callback) { callback('heap snapshot done'); }
12-
});
8+
const getHeapSnapshot = (callback) => {
9+
if (hasEnoughMemory()) {
10+
const filePath = v8.writeHeapSnapshot(`HeapSnapshot-${process.pid}-${Date.now()}.heapsnapshot`);
11+
callback(filePath);
12+
} else {
13+
callback(undefined, {
14+
type: 'Error',
15+
message: 'Not enough memory to perform heap snapshot'
16+
});
17+
}
1318
}
1419

15-
module.exports = getHeapSnapshot;
20+
module.exports = getHeapSnapshot;

lib/util/promise-with-resolvers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const promiseWithResolvers = () => {
2+
let resolve, reject
3+
const promise = new Promise((res, rej) => {
4+
resolve = res
5+
reject = rej
6+
})
7+
return { promise, resolve, reject }
8+
}
9+
10+
module.exports = promiseWithResolvers;

lib/worker.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const EventEmitter = require('events');
33
const WorkerProcess = require('./worker/process');
44
const Sequence = require('./util/sequence');
55
const messages = require('./worker/message');
6+
const promiseWithResolvers = require("./util/promise-with-resolvers");
67

78
const ProcessRequest = messages.Request;
89
const ProcessResponse = messages.Response;
@@ -133,17 +134,26 @@ class Worker extends EventEmitter {
133134
}
134135

135136
/**
136-
*
137+
* @param {TakeSnapshotOptions} options
138+
* @returns {Promise<string>}
137139
*/
138-
takeSnapshot() {
140+
takeSnapshot(options) {
141+
const { promise, resolve, reject } = promiseWithResolvers();
142+
139143
const cmd = 'takeSnapshot';
140144
this.calls.set(cmd, {
141145
timer: null,
142-
reject: () => {},
143-
resolve: () => {},
146+
reject,
147+
resolve
144148
});
145149

146150
this.process.handle({ cmd });
151+
152+
if (options.restartWorker) {
153+
promise.finally(() => this.stop());
154+
}
155+
156+
return promise;
147157
}
148158
}
149159

lib/worker/child-loader.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ function handleHeapSnapshot(requestData) {
101101
const request = new Request(requestData);
102102
const response = Response.from(request);
103103

104-
getHeapSnapshot((result) => {
104+
getHeapSnapshot((result, error) => {
105105
response.callId = 'takeSnapshot';
106106
response.setResult(result);
107+
response.error = error;
107108
sendMessageToParent(response);
108109
});
109110
}

0 commit comments

Comments
 (0)