Skip to content
This repository has been archived by the owner on Jun 13, 2024. It is now read-only.

Introduction

Daiyi Peng edited this page Oct 21, 2017 · 4 revisions

This article introduces concepts that are essential to understand how Napa.js works. You may want to read this post on its value proposition.

Zone

In Napa.js, all works related to multi-threading are around the concept of Zone, which is the basic unit to define policies and execute JavaScript code. A process may contain multiple zones, each consists of multiple JavaScript Workers.

Architecture

Within a zone, all workers are symmetrical: they load the same code, serve broadcast and execute requests in an indistinguishable manner. Basically, you cannot ask a zone to execute code on a specific worker. Workers across different zones are asymmetrical: they may load different code, or load the same code but reinforce different policies, like heap size, security settings, etc. Applications may need multiple zones for work loads of different purposes or different policies.

There are 2 types of zone:

  • Napa zone - zone consists of Napa.js managed JavaScript workers (V8 isolates). Can be multiple, each may contain multiple workers. Workers in Napa zone support partial Node.JS APIs.
  • Node zone - a 'virtual' zone which exposes Node.js event loop, has access to full Node.js capabilities.

This complex enables you to use Napa zone for heavy-lifting work, and Node zone for IO. Node zone also compensates Napa zone on its incomplete support of Node APIs.

Following code creates a Napa zone with 8 workers:

var napa = require('napajs');
var zone = napa.zone.create('sample-zone', { workers: 8 });

Following code accesses the Node zone:

var zone = napa.zone.node;

Two operations can be performed on zones:

  1. Broadcast - run code that changes worker state on all workers, returning a promise for pending operation. Through the promise, we can only know if operation succeed or failed. Usually we use broadcast to bootstrap application, pre-cache objects, or change application settings.
  2. Execute - run code that doesn't change worker state on an arbitrary worker, returning a promise of getting the result. Execute is designed for doing the real work.

Zone operations are on a basis of first-come-first-serve, while broadcast takes higher priority over execute.

Following code demonstrated how broadcast and execute collaborate to complete a simple task:

function foo() {
   console.log('hi');
}

// This setups function definition of foo in all workers in the zone.
zone.broadcast(foo.toString());

// This execute function foo on an arbitrary worker.
zone.execute(() => { global.foo() });

Please refer to zone API for details.

Transporting JavaScript values

V8 is not designed for running JavaScript across multiple isolates, which means every isolate manages their own heap. Passing values from one isolate to another has to be marshalled/unmarshalled. The size of payload and complexity of object will greatly impact communication efficiency. In Napa, we try to work out a design pattern for efficient object sharing, based on the fact that all JavaScript isolates (exposed as workers) reside in the same process, and native objects can be wrapped and exposed as JavaScripts objects.

Following concepts are introduced to implement this pattern:

Transportable types

Transportable types are JavaScript types that can be passed or shared transparently across workers. They are used as value types for passing arguments in broadcast and execute, as well as sharing objects in key/value pairs via set and get.

Transportable types are:

  • JavaScript primitive types: null, boolean, number, string
  • Object (TypeScript class) that implement Transportable interface
  • Array or plain JavaScript object that is composite pattern of above.
  • Single JavaScript value undefined

Please refer to transport API for details.

Cross-worker Storage

Store API is introduced as a necessary complement of sharing transportable types across JavaScript workers, on top of passing objects via arguments. During store.set, values marshalled into JSON and stored in process heap, so all threads can access it, and unmarshalled while users retrieve them via store.get.

Following code demonstrates object sharing using store:

var napa = require('napajs');

var zone = napa.zone.create('zone1');
var store = napa.store.create('store1');

// Set 'key1' in node.
store.set('key1', { 
    a: 1, 
    b: "2", 
    c: napa.memory.crtAllocator      // transportable complex type.
};

// Get 'key1' in another thread.
zone.execute(() => {
    var store = global.napa.store.get('store1');
    console.log(store.get('key1'));
});

Though very convenient, it's not recommended to use store to pass values within a transaction or request, since its overhead is more than passing objects by arguments (there are extra locking, etc.). Besides, developers have the obligation to delete the key after usage, while it's automatically managed by reference counting in passing arguments.

Please refer to store API for details.