Skip to content

Commit bc9e413

Browse files
addaleaxtargos
authored andcommitted
worker: add stack size resource limit option
Add `stackSizeMb` to the `resourceLimit` option group. Refs: #31593 (comment) PR-URL: #33085 Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
1 parent c6d632a commit bc9e413

6 files changed

+61
-20
lines changed

doc/api/worker_threads.md

+4
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ added:
174174
* `maxYoungGenerationSizeMb` {number}
175175
* `maxOldGenerationSizeMb` {number}
176176
* `codeRangeSizeMb` {number}
177+
* `stackSizeMb` {number}
177178

178179
Provides the set of JS engine resource constraints inside this Worker thread.
179180
If the `resourceLimits` option was passed to the [`Worker`][] constructor,
@@ -584,6 +585,8 @@ changes:
584585
recently created objects.
585586
* `codeRangeSizeMb` {number} The size of a pre-allocated memory range
586587
used for generated code.
588+
* `stackSizeMb` {number} The default maximum stack size for the thread.
589+
Small values may lead to unusable Worker instances. **Default:** `4`.
587590

588591
### Event: `'error'`
589592
<!-- YAML
@@ -679,6 +682,7 @@ added:
679682
* `maxYoungGenerationSizeMb` {number}
680683
* `maxOldGenerationSizeMb` {number}
681684
* `codeRangeSizeMb` {number}
685+
* `stackSizeMb` {number}
682686

683687
Provides the set of JS engine resource constraints for this Worker thread.
684688
If the `resourceLimits` option was passed to the [`Worker`][] constructor,

lib/internal/worker.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const {
5454
kMaxYoungGenerationSizeMb,
5555
kMaxOldGenerationSizeMb,
5656
kCodeRangeSizeMb,
57+
kStackSizeMb,
5758
kTotalResourceLimitCount
5859
} = internalBinding('worker');
5960

@@ -379,14 +380,17 @@ function parseResourceLimits(obj) {
379380
ret[kMaxYoungGenerationSizeMb] = obj.maxYoungGenerationSizeMb;
380381
if (typeof obj.codeRangeSizeMb === 'number')
381382
ret[kCodeRangeSizeMb] = obj.codeRangeSizeMb;
383+
if (typeof obj.stackSizeMb === 'number')
384+
ret[kStackSizeMb] = obj.stackSizeMb;
382385
return ret;
383386
}
384387

385388
function makeResourceLimits(float64arr) {
386389
return {
387390
maxYoungGenerationSizeMb: float64arr[kMaxYoungGenerationSizeMb],
388391
maxOldGenerationSizeMb: float64arr[kMaxOldGenerationSizeMb],
389-
codeRangeSizeMb: float64arr[kCodeRangeSizeMb]
392+
codeRangeSizeMb: float64arr[kCodeRangeSizeMb],
393+
stackSizeMb: float64arr[kStackSizeMb]
390394
};
391395
}
392396

src/node_worker.cc

+16-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ using v8::Value;
3939
namespace node {
4040
namespace worker {
4141

42+
constexpr double kMB = 1024 * 1024;
43+
4244
Worker::Worker(Environment* env,
4345
Local<Object> wrap,
4446
const std::string& url,
@@ -93,8 +95,6 @@ bool Worker::is_stopped() const {
9395
void Worker::UpdateResourceConstraints(ResourceConstraints* constraints) {
9496
constraints->set_stack_limit(reinterpret_cast<uint32_t*>(stack_base_));
9597

96-
constexpr double kMB = 1024 * 1024;
97-
9898
if (resource_limits_[kMaxYoungGenerationSizeMb] > 0) {
9999
constraints->set_max_young_generation_size_in_bytes(
100100
resource_limits_[kMaxYoungGenerationSizeMb] * kMB);
@@ -589,9 +589,20 @@ void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
589589

590590
w->stopped_ = false;
591591

592+
if (w->resource_limits_[kStackSizeMb] > 0) {
593+
if (w->resource_limits_[kStackSizeMb] * kMB < kStackBufferSize) {
594+
w->resource_limits_[kStackSizeMb] = kStackBufferSize / kMB;
595+
w->stack_size_ = kStackBufferSize;
596+
} else {
597+
w->stack_size_ = w->resource_limits_[kStackSizeMb] * kMB;
598+
}
599+
} else {
600+
w->resource_limits_[kStackSizeMb] = w->stack_size_ / kMB;
601+
}
602+
592603
uv_thread_options_t thread_options;
593604
thread_options.flags = UV_THREAD_HAS_STACK_SIZE;
594-
thread_options.stack_size = kStackSize;
605+
thread_options.stack_size = w->stack_size_;
595606
int ret = uv_thread_create_ex(&w->tid_, &thread_options, [](void* arg) {
596607
// XXX: This could become a std::unique_ptr, but that makes at least
597608
// gcc 6.3 detect undefined behaviour when there shouldn't be any.
@@ -601,7 +612,7 @@ void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
601612

602613
// Leave a few kilobytes just to make sure we're within limits and have
603614
// some space to do work in C++ land.
604-
w->stack_base_ = stack_top - (kStackSize - kStackBufferSize);
615+
w->stack_base_ = stack_top - (w->stack_size_ - kStackBufferSize);
605616

606617
w->Run();
607618

@@ -834,6 +845,7 @@ void InitWorker(Local<Object> target,
834845
NODE_DEFINE_CONSTANT(target, kMaxYoungGenerationSizeMb);
835846
NODE_DEFINE_CONSTANT(target, kMaxOldGenerationSizeMb);
836847
NODE_DEFINE_CONSTANT(target, kCodeRangeSizeMb);
848+
NODE_DEFINE_CONSTANT(target, kStackSizeMb);
837849
NODE_DEFINE_CONSTANT(target, kTotalResourceLimitCount);
838850
}
839851

src/node_worker.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum ResourceLimits {
1616
kMaxYoungGenerationSizeMb,
1717
kMaxOldGenerationSizeMb,
1818
kCodeRangeSizeMb,
19+
kStackSizeMb,
1920
kTotalResourceLimitCount
2021
};
2122

@@ -95,7 +96,7 @@ class Worker : public AsyncWrap {
9596
void UpdateResourceConstraints(v8::ResourceConstraints* constraints);
9697

9798
// Full size of the thread's stack.
98-
static constexpr size_t kStackSize = 4 * 1024 * 1024;
99+
size_t stack_size_ = 4 * 1024 * 1024;
99100
// Stack buffer size that is not available to the JS engine.
100101
static constexpr size_t kStackBufferSize = 192 * 1024;
101102

test/parallel/test-worker-resource-limits.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const testResourceLimits = {
1212
maxOldGenerationSizeMb: 16,
1313
maxYoungGenerationSizeMb: 4,
1414
codeRangeSizeMb: 16,
15+
stackSizeMb: 1,
1516
};
1617

1718
// Do not use isMainThread so that this test itself can be run inside a Worker.

test/parallel/test-worker-stack-overflow-stack-size.js

+33-14
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const { Worker } = require('worker_threads');
88
// Verify that Workers don't care about --stack-size, as they have their own
99
// fixed and known stack sizes.
1010

11-
async function runWorker() {
11+
async function runWorker(options = {}) {
1212
const empiricalStackDepth = new Uint32Array(new SharedArrayBuffer(4));
1313
const worker = new Worker(`
1414
const { workerData: { empiricalStackDepth } } = require('worker_threads');
@@ -18,26 +18,45 @@ async function runWorker() {
1818
}
1919
f();`, {
2020
eval: true,
21-
workerData: { empiricalStackDepth }
21+
workerData: { empiricalStackDepth },
22+
...options
2223
});
2324

2425
const [ error ] = await once(worker, 'error');
2526

26-
common.expectsError({
27-
constructor: RangeError,
28-
message: 'Maximum call stack size exceeded'
29-
})(error);
27+
if (!options.skipErrorCheck) {
28+
common.expectsError({
29+
constructor: RangeError,
30+
message: 'Maximum call stack size exceeded'
31+
})(error);
32+
}
3033

3134
return empiricalStackDepth[0];
3235
}
3336

3437
(async function() {
35-
v8.setFlagsFromString('--stack-size=500');
36-
const w1stack = await runWorker();
37-
v8.setFlagsFromString('--stack-size=1000');
38-
const w2stack = await runWorker();
39-
// Make sure the two stack sizes are within 10 % of each other, i.e. not
40-
// affected by the different `--stack-size` settings.
41-
assert(Math.max(w1stack, w2stack) / Math.min(w1stack, w2stack) < 1.1,
42-
`w1stack = ${w1stack}, w2stack ${w2stack} are too far apart`);
38+
{
39+
v8.setFlagsFromString('--stack-size=500');
40+
const w1stack = await runWorker();
41+
v8.setFlagsFromString('--stack-size=1000');
42+
const w2stack = await runWorker();
43+
// Make sure the two stack sizes are within 10 % of each other, i.e. not
44+
// affected by the different `--stack-size` settings.
45+
assert(Math.max(w1stack, w2stack) / Math.min(w1stack, w2stack) < 1.1,
46+
`w1stack = ${w1stack}, w2stack = ${w2stack} are too far apart`);
47+
}
48+
49+
{
50+
const w1stack = await runWorker({ resourceLimits: { stackSizeMb: 0.5 } });
51+
const w2stack = await runWorker({ resourceLimits: { stackSizeMb: 1.0 } });
52+
// Make sure the two stack sizes are at least 40 % apart from each other,
53+
// i.e. affected by the different `stackSizeMb` settings.
54+
assert(w2stack > w1stack * 1.4,
55+
`w1stack = ${w1stack}, w2stack = ${w2stack} are too close`);
56+
}
57+
58+
// Test that various low stack sizes result in an 'error' event.
59+
for (const stackSizeMb of [ 0.001, 0.01, 0.1, 0.2, 0.3, 0.5 ]) {
60+
await runWorker({ resourceLimits: { stackSizeMb }, skipErrorCheck: true });
61+
}
4362
})().then(common.mustCall());

0 commit comments

Comments
 (0)