|
10 | 10 | #include "JsArgToArrayConverter.h" |
11 | 11 | #include "ArgConverter.h" |
12 | 12 | #include "v8-profiler.h" |
| 13 | +#include <cmath> |
13 | 14 | #include <iostream> |
| 15 | +#include <optional> |
14 | 16 | #include <sstream> |
15 | 17 | #include <fstream> |
16 | 18 | #include <cstdio> |
@@ -1166,6 +1168,18 @@ int ClampWorkerPriority(Local<Context> context, Local<Value> value) { |
1166 | 1168 | throw NativeScriptException(isolate, error, message); |
1167 | 1169 | } |
1168 | 1170 |
|
| 1171 | +[[noreturn]] void ThrowWorkerOptionRangeError(Isolate *isolate, const std::string &message) { |
| 1172 | + Local<Value> error = Exception::RangeError(ArgConverter::ConvertToV8String(isolate, message)); |
| 1173 | + throw NativeScriptException(isolate, error, message); |
| 1174 | +} |
| 1175 | + |
| 1176 | +#ifndef V8_HAS_JS_DISPATCH_TABLE_RESERVATION_PARAM |
| 1177 | +[[noreturn]] void ThrowWorkerOptionError(Isolate *isolate, const std::string &message) { |
| 1178 | + Local<Value> error = Exception::Error(ArgConverter::ConvertToV8String(isolate, message)); |
| 1179 | + throw NativeScriptException(isolate, error, message); |
| 1180 | +} |
| 1181 | +#endif |
| 1182 | + |
1169 | 1183 | // Reads `key` from `object`. A false return means the getter threw: the |
1170 | 1184 | // exception is already pending on the isolate and construction must stop |
1171 | 1185 | // without running anything else on it. |
@@ -1250,6 +1264,123 @@ bool GetWorkerThreadPriority(Isolate *isolate, Local<Context> context, |
1250 | 1264 | kWorkerPriorityNames + "."); |
1251 | 1265 | } |
1252 | 1266 |
|
| 1267 | +constexpr double kBytesPerMegabyte = 1024 * 1024; |
| 1268 | +// Bounds the double-to-size_t conversion below; v8 clamps heap sizes far under |
| 1269 | +// this on every device, so nothing real is excluded. |
| 1270 | +constexpr double kMaxLimitMegabytes = 1024.0 * 1024.0; |
| 1271 | + |
| 1272 | +// v8 needs the reservation to be a whole number of table segments and no larger |
| 1273 | +// than its compile-time maximum; whole megabytes satisfy the first on every |
| 1274 | +// platform's segment size, and 256 is the maximum. |
| 1275 | +constexpr double kMaxJsDispatchTableSizeMb = 256; |
| 1276 | + |
| 1277 | +#ifdef V8_HAS_JS_DISPATCH_TABLE_RESERVATION_PARAM |
| 1278 | +// Every isolate otherwise reserves 256 MB of address space for its JS dispatch |
| 1279 | +// table; 64 MB still holds four million dispatch entries, far more than a |
| 1280 | +// worker allocates. Only workers get the smaller reservation - the main |
| 1281 | +// isolate keeps v8's default. |
| 1282 | +constexpr size_t kDefaultWorkerJsDispatchTableBytes = 64 * 1024 * 1024; |
| 1283 | +#endif |
| 1284 | + |
| 1285 | +// Reads one megabyte-valued `resourceLimits` key into `megabytes`, leaving it |
| 1286 | +// empty when the key is absent (v8's own default stays in place). Returns false |
| 1287 | +// when the getter threw (see ReadWorkerOption). A present value must be a |
| 1288 | +// finite number worth at least one byte and at most kMaxLimitMegabytes. |
| 1289 | +bool ReadMegabyteLimit(Isolate *isolate, Local<Context> context, Local<Object> resourceLimits, |
| 1290 | + const char *key, std::optional<double> &megabytes) { |
| 1291 | + Local<Value> value; |
| 1292 | + if (!ReadWorkerOption(isolate, context, resourceLimits, key, value)) { |
| 1293 | + return false; |
| 1294 | + } |
| 1295 | + if (value->IsUndefined()) { |
| 1296 | + return true; |
| 1297 | + } |
| 1298 | + |
| 1299 | + std::string name = std::string("resourceLimits.") + key; |
| 1300 | + if (!value->IsNumber()) { |
| 1301 | + ThrowWorkerOptionTypeError(isolate, "Worker option \"" + name + "\" must be a number."); |
| 1302 | + } |
| 1303 | + |
| 1304 | + double parsed = value.As<Number>()->Value(); |
| 1305 | + if (!std::isfinite(parsed) || parsed * kBytesPerMegabyte < 1 || parsed > kMaxLimitMegabytes) { |
| 1306 | + ThrowWorkerOptionRangeError(isolate, |
| 1307 | + "Worker option \"" + name + |
| 1308 | + "\" must be a finite number of megabytes worth at " |
| 1309 | + "least one byte and at most 1048576."); |
| 1310 | + } |
| 1311 | + |
| 1312 | + megabytes = parsed; |
| 1313 | + return true; |
| 1314 | +} |
| 1315 | + |
| 1316 | +/* |
| 1317 | + * Node's `resourceLimits` shape. Unknown keys are ignored, so the options Node |
| 1318 | + * has and this runtime cannot honor (codeRangeSizeMb, stackSizeMb) stay |
| 1319 | + * harmless to pass. Returns false when a getter threw (see ReadWorkerOption). |
| 1320 | + */ |
| 1321 | +bool ParseWorkerResourceLimits(Isolate *isolate, Local<Context> context, |
| 1322 | + const v8::FunctionCallbackInfo<v8::Value> &args, |
| 1323 | + IsolateLimits &limits) { |
| 1324 | + if (args.Length() < 2 || !args[1]->IsObject()) { |
| 1325 | + return true; |
| 1326 | + } |
| 1327 | + |
| 1328 | + Local<Value> value; |
| 1329 | + if (!ReadWorkerOption(isolate, context, args[1].As<Object>(), "resourceLimits", value)) { |
| 1330 | + return false; |
| 1331 | + } |
| 1332 | + if (value->IsNullOrUndefined()) { |
| 1333 | + return true; |
| 1334 | + } |
| 1335 | + if (!value->IsObject()) { |
| 1336 | + ThrowWorkerOptionTypeError(isolate, "Worker option \"resourceLimits\" must be an object."); |
| 1337 | + } |
| 1338 | + Local<Object> resourceLimits = value.As<Object>(); |
| 1339 | + |
| 1340 | + std::optional<double> megabytes; |
| 1341 | + |
| 1342 | + if (!ReadMegabyteLimit(isolate, context, resourceLimits, "maxOldGenerationSizeMb", megabytes)) { |
| 1343 | + return false; |
| 1344 | + } |
| 1345 | + if (megabytes) { |
| 1346 | + limits.maxOldGenerationSizeBytes = |
| 1347 | + static_cast<size_t>(*megabytes * kBytesPerMegabyte); |
| 1348 | + } |
| 1349 | + |
| 1350 | + megabytes.reset(); |
| 1351 | + if (!ReadMegabyteLimit(isolate, context, resourceLimits, "maxYoungGenerationSizeMb", |
| 1352 | + megabytes)) { |
| 1353 | + return false; |
| 1354 | + } |
| 1355 | + if (megabytes) { |
| 1356 | + limits.maxYoungGenerationSizeBytes = |
| 1357 | + static_cast<size_t>(*megabytes * kBytesPerMegabyte); |
| 1358 | + } |
| 1359 | + |
| 1360 | + megabytes.reset(); |
| 1361 | + if (!ReadMegabyteLimit(isolate, context, resourceLimits, "jsDispatchTableSizeMb", megabytes)) { |
| 1362 | + return false; |
| 1363 | + } |
| 1364 | + if (megabytes) { |
| 1365 | + if (*megabytes != std::floor(*megabytes) || *megabytes < 1 || |
| 1366 | + *megabytes > kMaxJsDispatchTableSizeMb) { |
| 1367 | + ThrowWorkerOptionRangeError( |
| 1368 | + isolate, "Worker option \"resourceLimits.jsDispatchTableSizeMb\" must be a " |
| 1369 | + "whole number of megabytes between 1 and 256."); |
| 1370 | + } |
| 1371 | +#ifdef V8_HAS_JS_DISPATCH_TABLE_RESERVATION_PARAM |
| 1372 | + limits.jsDispatchTableReservationBytes = |
| 1373 | + static_cast<size_t>(*megabytes) * static_cast<size_t>(kBytesPerMegabyte); |
| 1374 | +#else |
| 1375 | + ThrowWorkerOptionError(isolate, |
| 1376 | + "Worker option \"resourceLimits.jsDispatchTableSizeMb\" requires a " |
| 1377 | + "v8 build with a configurable JS dispatch table."); |
| 1378 | +#endif |
| 1379 | + } |
| 1380 | + |
| 1381 | + return true; |
| 1382 | +} |
| 1383 | + |
1253 | 1384 | } // namespace |
1254 | 1385 |
|
1255 | 1386 | void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo<v8::Value> &args) { |
@@ -1307,10 +1438,18 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo<v8::Valu |
1307 | 1438 | } |
1308 | 1439 |
|
1309 | 1440 | int priority; |
1310 | | - if (!GetWorkerThreadPriority(isolate, context, args, priority)) { |
| 1441 | + IsolateLimits resourceLimits; |
| 1442 | + if (!GetWorkerThreadPriority(isolate, context, args, priority) || |
| 1443 | + !ParseWorkerResourceLimits(isolate, context, args, resourceLimits)) { |
1311 | 1444 | return; |
1312 | 1445 | } |
1313 | 1446 |
|
| 1447 | +#ifdef V8_HAS_JS_DISPATCH_TABLE_RESERVATION_PARAM |
| 1448 | + if (!resourceLimits.jsDispatchTableReservationBytes.has_value()) { |
| 1449 | + resourceLimits.jsDispatchTableReservationBytes = kDefaultWorkerJsDispatchTableBytes; |
| 1450 | + } |
| 1451 | +#endif |
| 1452 | + |
1314 | 1453 | // An http(s) entry has no filesystem form to validate or to resolve |
1315 | 1454 | // against the caller's directory: it is already absolute, and the |
1316 | 1455 | // module loader's HTTP branch fetches it on the worker's own thread |
@@ -1386,8 +1525,8 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo<v8::Valu |
1386 | 1525 | // here on the main thread where class loading is safe. |
1387 | 1526 | WorkerWrapper::EnsureJniCached(); |
1388 | 1527 |
|
1389 | | - auto wrapper = std::make_shared<WorkerWrapper>(isolate, workerId, entryPath, |
1390 | | - currentDir, priority, thiz); |
| 1528 | + auto wrapper = std::make_shared<WorkerWrapper>(isolate, workerId, entryPath, currentDir, |
| 1529 | + priority, std::move(resourceLimits), thiz); |
1391 | 1530 | WorkerWrapper::Insert(workerId, wrapper); |
1392 | 1531 |
|
1393 | 1532 | DEBUG_WRITE("Called Worker constructor id=%d", workerId); |
|
0 commit comments