Name: tasklets
Owner: GoogleChromeLabs
Description: null
Created: 2017-05-22 22:02:53.0
Updated: 2018-05-16 16:27:48.0
Pushed: 2017-10-28 18:32:16.0
Homepage: null
Size: 90
Language: JavaScript
GitHub Committers
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
Note: The tasklets proposal spawned the Comlink library. It?s an RPC library that extracts the ergonomics feature from tasklets.
Most modern development platforms favor a multi-threaded approach by default. Typically, the split for work is:
iOS and Android native platforms, for example, restrict (by default) the usage of any APIs not critical to UI manipulation on the main thread.
The web has support for this model via WebWorkers
. However:
postMessage()
is clunky and difficult to useAs a result, worker adoption has been minimal at best and the default model remains to put all work on the main thread. In order to encourage developers to move work off the main thread, we propose a more ergonomic solution with Tasklets.
Note: APIs described below are just strawperson proposals. We think they're pretty cool but
there's always room for improvement.
TL;DR:
etcher.js
rt async function fetchDataObject() {
nst resp = await fetch(/*...*/);
nst json = await resp.json();
turn doSomeExpensiveProcessing(json);
s
pp.js
t fetcher = await tasklets.addModule('fetcher.js');
t json = await fetcher.fetchDataObject();
..
Today, many uses of WebWorkers follow a structure similar to:
t worker = new Worker('worker.js');
er.postMessage({'cmd': 'fetch', 'url': 'example.com'});
s
orker.js
.addEventListener('message', (evt) => {
itch (evt.data.cmd) {
case 'fetch':
performFetch(evt.data.url);
break;
default:
throw Error(`Invalid command: ${evt.data.cmd}`);
A switch statement in the worker then typically routes messages to the correct API. The Tasklets API exposes this behavior natively, by allowing a class within one context to expose methods to other contexts.
The code below shows a basic example of the Tasklets API.
peaker.js
rt class Speaker {
yHello(message) {
return `Hello ${message}`;
rt function add(a, b) {
turn a + b;
s
t module = await tasklets.addModule('speaker.js');
t speaker = await new module.Speaker();
ole.log(await speaker.sayHello('world!')); // Logs "Hello world!".
ole.log(await module.add(2, 3)); // Logs '5'.
A few things are happening here, so let's step through them individually.
t module = await tasklets.addModule('speaker.js');
This loads the module into the tasklet's JavaScript global scope. This is similar to invoking
new Worker('speaker.js')
. Also similar to WebWorkers, the tasklet would be around for the lifetime
of the page.
However, when this module is loaded, the browser will look into the script you imported and find all
of the exported classes and functions. In the above example we only exported the Speaker
class and the add
function.
addModule
returns a “namespace” object, for which the browser creates “proxy” constructors and
functions:
le.Speaker.toString() == 'function Speaker() { [native code] }';
le.Speaker.prototype.sayHello.toString() == 'function sayHello() { [native code] }';
<span>Note: `[native code]`</span>
It doesn't have to be `[native code]` above, this is just to give people the idea that this class on the main thread side, is synthesized by the browser.
All of the functions now return promises, for example:
t p = speaker.sayHello('world!');
All arguments and return values go through the structured clone algorithm, which means that functions can only accept certain kinds of objects, for example:
ker.sayHello(document.body); // Causes a DOMException as HTMLBodyElement cannot be cloned.
As for transferrables, we think that every parameter and return value should be transferred by default, e.g:
t arr = new Int8Array(100);
someFunction(arr);
rr has now been transferred, you can't access it.
If web developers need copying behavior instead, they are able to make a copy in the call, e.g:
someFunction(new Int8Array(arr));
We'll quickly go through some more detailed cases here. We haven't fully formed everything here yet.
We believe that all asynchronous APIs which are exposed in workers should be exposed in the
TaskWorkletGlobalScope
(that means Sync XHR for example would not be exposed). Additionally
Atomics.wait
would throw a TypeError
.
We want this characteristic as we'd like to potentially run multiple tasklets in the same thread. Some implementations have a high overhead per thread, but a smaller cost per JavaScript environment.
We think it's very compelling to have classes inside the TaskWorkletGlobalScope
to be able to
extend from EventTarget
, for example:
pi.js
rt class FetchManager extends EventTarget {
rformFetch(url) {
const response = await fetch(url);
if (response.get('Content-Type').startsWith('image')) {
this.dispatchEvent('image-fetched');
}
s
t api = await tasklets.addModule('api.js');
t fetchManager = await new api.FetchManager();
hManager.addEventListener('image-fetched', () => {
maybe update some UI?
hManager.performFetch('cats.png');
The data provided with the event is structured cloned, similar to arguments and return values.
This is a very early stage proposal, so it has a few problems that we'll need to sort out.
It will undoubtedly be useful to return instances of objects created in the tasklet. The complete async nature of the proxies, however, make reasoning harder and handling a bit awkward.
alendarTasklet.js
rt class Calendar {
nstructor(credentials) { /* ... */ }
xtEvents(limit = 10) {
/* ... */
return arrayOfCalendarEntries;
nerateShareLink(id) { /* ... */ }
... */
rt class CalendarEntry {
nstructor(calender) { /* ... */ }
t id() { /* ... */ }
... */
s
ain.js
t {Calendar} = await tasklets.addModule('calendarTasklet.js');
t myCalendar = await new Calendar(myCredentials);
t events = await myCalendar.nextEvents();
ts.map(event => myCalender.generateShareLink(event.id)); // !!!
The last line is potentially problematic. event.id
has been promisified. This line would create a
lot of message passing under the hood: Every invocation of the map()
callback would have to wait
for event.id
to resolve just pass a message back to the tasklet to invoke generateShareLink
.
This can be solved by the author by architecting their tasklet appropriately. A method like
myCalender.generateShareLinks(events)
for example would be much more efficient.
Consider this tasklet code:
rt {A} from 'a.js';
rt class B extends A {}
What gets exported?
¯\_(?)_/¯
We aren't sure. Options:
Object
prototype.B
, but not A
)static get exportedProperties() { return [/*...*/]; }
)