Skip to content

Commit 3dade40

Browse files
authored
feat(worker): accept platform options under ios, deprecate iosPriority (#470)
* feat(worker): accept platform options under ios, deprecate iosPriority Worker options now carry iOS-specific settings in an `ios` namespace object: new Worker("./w.js", { ios: { priority: "userInitiated" } }) `ios.priority` is validated strictly — a non-object `ios`, a non-string priority or an unrecognized priority name throws a TypeError — while unknown keys inside `ios` are ignored so later options can be added without breaking older runtimes. `iosPriority` stays supported with its lenient behavior and logs a one-time deprecation warning; `ios.priority` takes precedence when both are given. The quality of service is carried as `std::optional<int>` so an explicit "default" is distinguishable from no option at all. * feat(worker): treat ios: null as an absent ios option * test(worker): give low quality-of-service workers time to boot on a loaded host * test(worker): stop waiting on a background-class thread to boot an isolate * fix(worker): stop construction when an option getter throws
1 parent cee3713 commit 3dade40

6 files changed

Lines changed: 245 additions & 21 deletions

File tree

‎NativeScript/runtime/DataWrapper.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <functional>
77
#include <mutex>
8+
#include <optional>
89
#include <thread>
910

1011
#include "Common.h"
@@ -548,7 +549,8 @@ class WorkerWrapper : public BaseDataWrapper {
548549
void DestroyInspector();
549550

550551
void Start(std::shared_ptr<v8::Persistent<v8::Value>> poWorker,
551-
std::function<v8::Isolate*()> func, int qualityOfService = -1);
552+
std::function<v8::Isolate*()> func,
553+
std::optional<int> qualityOfService = std::nullopt);
552554
void CallOnErrorHandlers(v8::TryCatch& tc);
553555
// Reports a rejected entry-evaluation promise. A rejection carries a reason
554556
// rather than a TryCatch, so it cannot go through CallOnErrorHandlers, but it

‎NativeScript/runtime/Worker.mm‎

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "Worker.h"
22
#include <pthread.h>
33
#include <functional>
4+
#include <mutex>
5+
#include <optional>
46
#include "Caches.h"
57
#include "Constants.h"
68
#include "Helpers.h"
@@ -17,6 +19,96 @@
1719

1820
std::vector<std::string> Worker::GlobalFunctions = {"postMessage", "close"};
1921

22+
namespace {
23+
24+
// The five names below are the whole public priority surface; anything else is
25+
// a caller error under `ios.priority` and ignored under the deprecated
26+
// `iosPriority`.
27+
bool MapPriorityName(const std::string& name, int& qos) {
28+
if (name == "userInteractive") {
29+
qos = NSQualityOfServiceUserInteractive;
30+
} else if (name == "userInitiated") {
31+
qos = NSQualityOfServiceUserInitiated;
32+
} else if (name == "default") {
33+
qos = NSQualityOfServiceDefault;
34+
} else if (name == "utility") {
35+
qos = NSQualityOfServiceUtility;
36+
} else if (name == "background") {
37+
qos = NSQualityOfServiceBackground;
38+
} else {
39+
return false;
40+
}
41+
return true;
42+
}
43+
44+
// Carries a real TypeError instance so `catch (e) { e instanceof TypeError }`
45+
// holds in JS; the constructor's catch block rethrows it unchanged.
46+
[[noreturn]] void ThrowOptionTypeError(Isolate* isolate, const std::string& message) {
47+
Local<Value> error = Exception::TypeError(tns::ToV8String(isolate, message));
48+
throw NativeScriptException(isolate, error, message);
49+
}
50+
51+
// Reads `key` from `object`. A false return means the getter threw: the
52+
// exception is already pending on the isolate and construction must stop
53+
// without running anything else on it.
54+
bool ReadOption(Isolate* isolate, Local<Context> context, Local<Object> object, const char* key,
55+
Local<Value>& out) {
56+
return object->Get(context, tns::ToV8String(isolate, key)).ToLocal(&out);
57+
}
58+
59+
// Fills `qos` with the quality of service the caller asked for, or leaves it
60+
// empty for the operation queue's own default. Returns false when a getter
61+
// threw (see ReadOption).
62+
bool ParseQualityOfService(Isolate* isolate, Local<Context> context, Local<Object> options,
63+
std::optional<int>& qos) {
64+
Local<Value> iosVal;
65+
if (!ReadOption(isolate, context, options, "ios", iosVal)) {
66+
return false;
67+
}
68+
if (!iosVal->IsNullOrUndefined()) {
69+
if (!iosVal->IsObject()) {
70+
ThrowOptionTypeError(isolate, "Worker option \"ios\" must be an object.");
71+
}
72+
73+
Local<Value> priorityVal;
74+
if (!ReadOption(isolate, context, iosVal.As<Object>(), "priority", priorityVal)) {
75+
return false;
76+
}
77+
if (!priorityVal->IsUndefined()) {
78+
int mapped;
79+
if (!IsString(priorityVal) || !MapPriorityName(ToString(isolate, priorityVal), mapped)) {
80+
ThrowOptionTypeError(isolate,
81+
"Worker option \"ios.priority\" must be one of \"userInteractive\", "
82+
"\"userInitiated\", \"default\", \"utility\" or \"background\".");
83+
}
84+
qos = mapped;
85+
}
86+
}
87+
88+
Local<Value> legacyVal;
89+
if (!ReadOption(isolate, context, options, "iosPriority", legacyVal)) {
90+
return false;
91+
}
92+
if (!legacyVal->IsUndefined()) {
93+
static std::once_flag warnedDeprecated;
94+
std::call_once(warnedDeprecated, []() {
95+
Log(@"NativeScript: the Worker option \"iosPriority\" is deprecated. Use "
96+
@"\"ios\": { \"priority\": ... } instead.");
97+
});
98+
99+
int mapped;
100+
// Lenient by contract: an unusable legacy value is ignored, never fatal.
101+
if (!qos.has_value() && IsString(legacyVal) &&
102+
MapPriorityName(ToString(isolate, legacyVal), mapped)) {
103+
qos = mapped;
104+
}
105+
}
106+
107+
return true;
108+
}
109+
110+
} // namespace
111+
20112
void Worker::Init(Isolate* isolate, Local<ObjectTemplate> globalTemplate) {
21113
Worker::Init(isolate, globalTemplate, Caches::Get(isolate)->isWorker);
22114
}
@@ -148,24 +240,10 @@ throw NativeScriptException(
148240
}
149241
}
150242

151-
int qos = -1;
243+
std::optional<int> qos;
152244
if (info.Length() >= 2 && info[1]->IsObject()) {
153-
Local<Object> options = info[1].As<Object>();
154-
Local<Value> iosPriorityVal;
155-
if (options->Get(context, tns::ToV8String(isolate, "iosPriority")).ToLocal(&iosPriorityVal) &&
156-
IsString(iosPriorityVal)) {
157-
std::string priority = ToString(isolate, iosPriorityVal);
158-
if (priority == "userInteractive") {
159-
qos = NSQualityOfServiceUserInteractive;
160-
} else if (priority == "userInitiated") {
161-
qos = NSQualityOfServiceUserInitiated;
162-
} else if (priority == "default") {
163-
qos = NSQualityOfServiceDefault;
164-
} else if (priority == "utility") {
165-
qos = NSQualityOfServiceUtility;
166-
} else if (priority == "background") {
167-
qos = NSQualityOfServiceBackground;
168-
}
245+
if (!ParseQualityOfService(isolate, context, info[1].As<Object>(), qos)) {
246+
return;
169247
}
170248
}
171249

‎NativeScript/runtime/WorkerWrapper.mm‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,16 @@ static void PostToRuntimeLoop(Runtime* runtime, std::function<void()> fn, bool a
7373
}
7474

7575
void WorkerWrapper::Start(std::shared_ptr<Persistent<Value>> poWorker,
76-
std::function<Isolate*()> func, int qualityOfService) {
76+
std::function<Isolate*()> func, std::optional<int> qualityOfService) {
7777
this->poWorker_ = poWorker;
7878
this->workerId_ = nextId_.fetch_add(1, std::memory_order_relaxed) + 1;
7979

8080
NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
8181
this->BackgroundLooper(func);
8282
}];
8383

84-
if (qualityOfService >= 0) {
85-
op.qualityOfService = static_cast<NSQualityOfService>(qualityOfService);
84+
if (qualityOfService.has_value()) {
85+
op.qualityOfService = static_cast<NSQualityOfService>(*qualityOfService);
8686
}
8787

8888
[workers_ addOperation:op];
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
describe("Worker platform options", function () {
2+
var entry = "./workerOptions/qosWorker.js";
3+
4+
// Jasmine arms a spec's async timeout before calling it, so the interval
5+
// has to be raised ahead of the spec, not inside it. A utility thread boots
6+
// a whole isolate under throttled CPU and I/O; on a contended host that has
7+
// taken well over 10 s.
8+
var originalTimeout;
9+
beforeEach(function () {
10+
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
11+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000;
12+
});
13+
afterEach(function () {
14+
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
15+
});
16+
17+
var reportQos = function (options, done, check) {
18+
var worker = options === undefined ? new Worker(entry) : new Worker(entry, options);
19+
var settled = false;
20+
var finish = function () {
21+
if (settled) {
22+
return;
23+
}
24+
settled = true;
25+
worker.terminate();
26+
done();
27+
};
28+
// A throw inside either handler must still settle the spec and
29+
// terminate the worker; Jasmine only guards the spec body itself.
30+
worker.onmessage = function (msg) {
31+
try {
32+
check(msg.data.qos);
33+
} finally {
34+
finish();
35+
}
36+
};
37+
worker.onerror = function (e) {
38+
try {
39+
expect(String(e && e.message ? e.message : e)).toBe("<no worker error>");
40+
} finally {
41+
finish();
42+
}
43+
};
44+
};
45+
46+
// Background is deliberately absent: the system defines that class as work
47+
// that may take minutes, and on a loaded host a background thread has not
48+
// finished booting an isolate within two minutes. It is covered below
49+
// without waiting on it.
50+
var priorities = [
51+
["userInteractive", NSQualityOfService.UserInteractive],
52+
["userInitiated", NSQualityOfService.UserInitiated],
53+
["default", NSQualityOfService.Default],
54+
["utility", NSQualityOfService.Utility]
55+
];
56+
57+
priorities.forEach(function (pair) {
58+
it("runs the worker thread at " + pair[0] + " quality of service", function (done) {
59+
reportQos({ ios: { priority: pair[0] } }, done, function (qos) {
60+
expect(qos).toBe(pair[1]);
61+
});
62+
});
63+
});
64+
65+
it("accepts background priority", function () {
66+
var worker;
67+
expect(function () {
68+
worker = new Worker(entry, { ios: { priority: "background" } });
69+
}).not.toThrow();
70+
worker.terminate();
71+
});
72+
73+
it("still honors the deprecated iosPriority option", function (done) {
74+
reportQos({ iosPriority: "utility" }, done, function (qos) {
75+
expect(qos).toBe(NSQualityOfService.Utility);
76+
});
77+
});
78+
79+
it("prefers ios.priority over iosPriority when both are given", function (done) {
80+
reportQos({ ios: { priority: "userInteractive" }, iosPriority: "background" }, done, function (qos) {
81+
expect(qos).toBe(NSQualityOfService.UserInteractive);
82+
});
83+
});
84+
85+
it("ignores unknown keys inside ios", function (done) {
86+
reportQos({ ios: { priority: "utility", somethingElse: 42 } }, done, function (qos) {
87+
expect(qos).toBe(NSQualityOfService.Utility);
88+
});
89+
});
90+
91+
it("starts a worker given no options at all", function (done) {
92+
reportQos(undefined, done, function (qos) {
93+
expect(typeof qos).toBe("number");
94+
});
95+
});
96+
97+
it("treats ios: null like an absent ios", function (done) {
98+
reportQos({ ios: null, iosPriority: "utility" }, done, function (qos) {
99+
expect(qos).toBe(NSQualityOfService.Utility);
100+
});
101+
});
102+
103+
it("propagates the error thrown by an option getter", function () {
104+
var boom = new Error("boom");
105+
var options = new Proxy({}, {
106+
get: function (target, key) {
107+
if (key === "ios") {
108+
throw boom;
109+
}
110+
return undefined;
111+
}
112+
});
113+
var thrown;
114+
try {
115+
new Worker(entry, options);
116+
} catch (e) {
117+
thrown = e;
118+
}
119+
expect(thrown).toBe(boom);
120+
});
121+
122+
it("throws a TypeError when ios is not an object", function () {
123+
expect(function () {
124+
new Worker(entry, { ios: 42 });
125+
}).toThrowError(TypeError, /"ios"/);
126+
});
127+
128+
it("throws a TypeError for an unknown ios.priority", function () {
129+
expect(function () {
130+
new Worker(entry, { ios: { priority: "highest" } });
131+
}).toThrowError(TypeError, /"ios\.priority"/);
132+
});
133+
134+
it("throws a TypeError for a non-string ios.priority", function () {
135+
expect(function () {
136+
new Worker(entry, { ios: { priority: 3 } });
137+
}).toThrowError(TypeError, /"ios\.priority"/);
138+
});
139+
});

‎TestRunner/app/tests/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ require("./ApiTests");
131131
require("./NsRuntimeTests");
132132
require("./GCFinalizerTests");
133133
require("./WorkerConcurrentStartupTests");
134+
require("./WorkerOptionsTests");
134135
require("./DeclarationConflicts");
135136
//
136137
require("./Promises");
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Entry for WorkerOptionsTests: reports the quality of service the runtime
2+
// gave this worker's thread, which is the only observable effect of the
3+
// `ios.priority` option.
4+
postMessage({ qos: NSThread.currentThread.qualityOfService });

0 commit comments

Comments
 (0)