Skip to content

Commit 9173b09

Browse files
committed
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 b0c1aca commit 9173b09

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: v12.16.0
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,
@@ -625,6 +626,8 @@ changes:
625626
recently created objects.
626627
* `codeRangeSizeMb` {number} The size of a pre-allocated memory range
627628
used for generated code.
629+
* `stackSizeMb` {number} The default maximum stack size for the thread.
630+
Small values may lead to unusable Worker instances. **Default:** `4`.
628631

629632
### Event: `'error'`
630633
<!-- YAML
@@ -718,6 +721,7 @@ added: v12.16.0
718721
* `maxYoungGenerationSizeMb` {number}
719722
* `maxOldGenerationSizeMb` {number}
720723
* `codeRangeSizeMb` {number}
724+
* `stackSizeMb` {number}
721725

722726
Provides the set of JS engine resource constraints for this Worker thread.
723727
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

@@ -385,14 +386,17 @@ function parseResourceLimits(obj) {
385386
ret[kMaxYoungGenerationSizeMb] = obj.maxYoungGenerationSizeMb;
386387
if (typeof obj.codeRangeSizeMb === 'number')
387388
ret[kCodeRangeSizeMb] = obj.codeRangeSizeMb;
389+
if (typeof obj.stackSizeMb === 'number')
390+
ret[kStackSizeMb] = obj.stackSizeMb;
388391
return ret;
389392
}
390393

391394
function makeResourceLimits(float64arr) {
392395
return {
393396
maxYoungGenerationSizeMb: float64arr[kMaxYoungGenerationSizeMb],
394397
maxOldGenerationSizeMb: float64arr[kMaxOldGenerationSizeMb],
395-
codeRangeSizeMb: float64arr[kCodeRangeSizeMb]
398+
codeRangeSizeMb: float64arr[kCodeRangeSizeMb],
399+
stackSizeMb: float64arr[kStackSizeMb]
396400
};
397401
}
398402

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,
@@ -98,8 +100,6 @@ std::shared_ptr<ArrayBufferAllocator> Worker::array_buffer_allocator() {
98100
void Worker::UpdateResourceConstraints(ResourceConstraints* constraints) {
99101
constraints->set_stack_limit(reinterpret_cast<uint32_t*>(stack_base_));
100102

101-
constexpr double kMB = 1024 * 1024;
102-
103103
if (resource_limits_[kMaxYoungGenerationSizeMb] > 0) {
104104
constraints->set_max_young_generation_size_in_bytes(
105105
resource_limits_[kMaxYoungGenerationSizeMb] * kMB);
@@ -595,9 +595,20 @@ void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
595595

596596
w->stopped_ = false;
597597

598+
if (w->resource_limits_[kStackSizeMb] > 0) {
599+
if (w->resource_limits_[kStackSizeMb] * kMB < kStackBufferSize) {
600+
w->resource_limits_[kStackSizeMb] = kStackBufferSize / kMB;
601+
w->stack_size_ = kStackBufferSize;
602+
} else {
603+
w->stack_size_ = w->resource_limits_[kStackSizeMb] * kMB;
604+
}
605+
} else {
606+
w->resource_limits_[kStackSizeMb] = w->stack_size_ / kMB;
607+
}
608+
598609
uv_thread_options_t thread_options;
599610
thread_options.flags = UV_THREAD_HAS_STACK_SIZE;
600-
thread_options.stack_size = kStackSize;
611+
thread_options.stack_size = w->stack_size_;
601612
int ret = uv_thread_create_ex(&w->tid_, &thread_options, [](void* arg) {
602613
// XXX: This could become a std::unique_ptr, but that makes at least
603614
// gcc 6.3 detect undefined behaviour when there shouldn't be any.
@@ -607,7 +618,7 @@ void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
607618

608619
// Leave a few kilobytes just to make sure we're within limits and have
609620
// some space to do work in C++ land.
610-
w->stack_base_ = stack_top - (kStackSize - kStackBufferSize);
621+
w->stack_base_ = stack_top - (w->stack_size_ - kStackBufferSize);
611622

612623
w->Run();
613624

@@ -837,6 +848,7 @@ void InitWorker(Local<Object> target,
837848
NODE_DEFINE_CONSTANT(target, kMaxYoungGenerationSizeMb);
838849
NODE_DEFINE_CONSTANT(target, kMaxOldGenerationSizeMb);
839850
NODE_DEFINE_CONSTANT(target, kCodeRangeSizeMb);
851+
NODE_DEFINE_CONSTANT(target, kStackSizeMb);
840852
NODE_DEFINE_CONSTANT(target, kTotalResourceLimitCount);
841853
}
842854

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

@@ -97,7 +98,7 @@ class Worker : public AsyncWrap {
9798
void UpdateResourceConstraints(v8::ResourceConstraints* constraints);
9899

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

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)