docs/design/transport-js-builtins.md
The abstraction of 'Transportable' lies in the center of Napa.js to efficiently share objects between JavaScript VMs (Napa workers). Except JavaScript primitive types, an object needs to implement 'Transportable' interface to make it transportable. It means JavaScript standard built-in objects are not transportable unless wrappers or equivalent implementations for them are implemented by extending 'Transportable' interface. The development cost for these objects is not trivial, and new abstraction layer (wrappers or equivalent implementations) will create barriers for users to learn and adopt these new things. Moreover, developers also need to deal with the interaction between JavaScript standards objects and those wrappers or equivalent implementations.
The incentive of this design is to provide a solution to make JavaScript standard built-in objects transportable with requirements listed in the Goals section.
At the first stage, we will focus on an efficient solution to share data between Napa workers. Basically, it is about making SharedArrayBuffer / TypedArray / DataView transportable.
Make Javascript standard built-in objects transportable with
The below example shows how SharedArrayBuffer object is transported across multiple Napa workers. It will print the TypedArray 'ta' created from a SharedArrayBuffer, with all its elements set to 100 from different Napa workers.
var napa = require("napajs");
var zone = napa.zone.create('zone', { workers: 4 });
function foo(sab, i) {
var ta = new Uint8Array(sab);
ta[i] = 100;
return i;
}
function run() {
var promises = [];
var sab = new SharedArrayBuffer(4);
for (var i = 0; i < 4; i++) {
promises[i] = zone.execute(foo, [sab, i]);
}
return Promise.all(promises).then(values => {
var ta = new Uint8Array(sab);
console.log(ta);
});
}
run();
Here we just give a high-level description of the solution. Its API will go to docs/api/transport.
V8 provides its value-serialization mechanism by ValueSerializer and ValueDeserializer, which is compatible with the HTML structured clone algorithm. It is a horizontal solution to serialize / deserialize JavaScript objects. ValueSerializer::Delegate and ValueDeserializer::Delegate are their inner class. They work as base classes from which developers can deprive to customize some special handling of external / shared resources, like memory used by a SharedArrayBuffer object.
napa::v8_extensions::ExternalizedContents
napa::v8_extensions::SerializedData
BuiltInObjectTransporter
Currently, Napa relies on Transportable API and a registered constructor to make an object transportable. In marshallTransform, when a JavaScript object is detected to have a registered constructor, it will go with Napa way to marshall this object with the help of a TransportContext object, otherwise a non-transportable error is thrown.
Instead of throwing an error when no registered constructor is detected, the BuiltInObjectTransporter can help handle this object. We can use a whitelist of object types to restrict this solution to those verified types at first.
export function marshallTransform(jsValue: any, context: transportable.TransportContext): any {
if (jsValue != null && typeof jsValue === 'object' && !Array.isArray(jsValue)) {
let constructorName = Object.getPrototypeOf(jsValue).constructor.name;
if (constructorName !== 'Object') {
if (typeof jsValue['cid'] === 'function') {
return <transportable.Transportable>(jsValue).marshall(context);
} else if (_builtInTypeWhitelist.has(constructorName)) {
let serializedData = builtinObjectTransporter.serializeValue(jsValue);
if (serializedData) {
return { _serialized : serializedData };
} else {
throw new Error(`Failed to serialize object with type of \"${constructorName}\".`);
}
} else {
throw new Error(`Object type \"${constructorName}\" is not transportable.`);
}
}
}
return jsValue;
}
function unmarshallTransform(payload: any, context: transportable.TransportContext): any {
if (payload != null && payload._cid !== undefined) {
let cid = payload._cid;
if (cid === 'function') {
return functionTransporter.load(payload.hash);
}
let subClass = _registry.get(cid);
if (subClass == null) {
throw new Error(`Unrecognized Constructor ID (cid) "${cid}". Please ensure @cid is applied on the class or transport.register is called on the class.`);
}
let object = new subClass();
object.unmarshall(payload, context);
return object;
} else if (payload.hasOwnProperty('_serialized')) {
return builtinObjectTransporter.deserializeValue(payload['_serialized']);
}
return payload;
}
When a SAB participates transportation among Napa workers, its life cycle will be extended till the last reference this SAB. The reference of a SAB could be:
The life cycle extension during transportation is achieved through the ExternalizedContents SharedPtrWrap of the SAB.
When a SAB is transported for the first time, it will be externalized and its ExternalizedContents will be stored in its SerializedData. At the same time, the SharedPtrWrap of the ExternalizedContents will be set to the '_externalized' property of the original SAB.
When a SAB is transported for the second time or later, it will skip externalization and find its ExternalizedContents from its '_externalized' property, and store it to its SerializedData.
When a Napa worker tries to restore a transported SAB, it will find the pre-stored ExternalizedContents, and create a SharedPtrWrap for it, then set it to the to-be-restored SAB.
The life cycle of the SharedArrayBuffer is extended by the SharedPtrWrap of its ExternalizedContents.
The above solution is based on the serialization / deserialization mechanism of V8. It may have the following constraints.