Merged PR 293569: Implement anonymous function execution.

1. Support function transport.
2. Implement zone.execute on function.
This commit is contained in:
Daiyi Peng 2017-06-15 18:10:11 +00:00 коммит произвёл Daiyi Peng
Родитель f41cb059be
Коммит b8f58f7ca0
15 изменённых файлов: 289 добавлений и 125 удалений

Просмотреть файл

@ -1,9 +1,9 @@
# Benchmark
## Summary:
## Summary
- JavaScript execution in napajs is on par with node, using the same version of V8, which is expected.
- `zone.execute` scales linearly on number of workers, which is expected.
- The overhead of calling `zone.execute` from node is around 0.1ms after warm-up, `zone.executeSync` is around 0.2ms.
- The overhead of calling `zone.execute` from node is around 0.1ms after warm-up, `zone.executeSync` is around 0.2ms. The cost of using anonymous function is neglectable.
- `transport.marshall` cost on small plain JavaScript values is about 3x of JSON.stringify.
- The overhead of `store.set` and `store.get` is around 0.06ms plus transport overhead on the objecs.
@ -41,7 +41,7 @@ Transport overhead (#1, #3, #4, #7) varies by size and complexity of payload, wi
Please refer to [execute-overhead.ts](./execute-overhead.ts) for test details.
### Overhead after warm-up
Average overhead is around 0.06ms to 0.12ms for `zone.execute`, and around 0.16ms for `zone.executeSync`
Average overhead is around 0.06ms to 0.12ms for `zone.execute`, and around 0.16ms for `zone.executeSync`.
| repeat | zone.execute (ms) | zone.executeSync (ms) |
|----------|-------------------|-----------------------|
@ -50,6 +50,8 @@ Average overhead is around 0.06ms to 0.12ms for `zone.execute`, and around 0.16m
| 10000 | 810.687 | 1799.866 |
| 50000 | 3387.361 | 8169.023 |
*10000 times of zone.executeSync on anonymouse function is 1780.241ms. The gap is within range of bench noise.
### Overhead during warm-up:
| Sequence of call | Time (ms) |

Просмотреть файл

@ -31,6 +31,13 @@ export function bench(zone: napa.zone.Zone): Promise<void> {
}
console.log(`Elapse of running empty function for ${REPEAT} times: ${formatTimeDiff(process.hrtime(start), true)}\n`);
console.log("## `zone.executeSync` overhead calling anonymous function\n");
start = process.hrtime();
for (let i = 0; i < REPEAT; ++i) {
zone.executeSync(() => {}, ARGS);
}
console.log(`Elapse of running empty anonymous function for ${REPEAT} times: ${formatTimeDiff(process.hrtime(start), true)}\n`);
// execute after warm-up
return new Promise<void>((resolve, reject) => {
let finished = 0;

Просмотреть файл

@ -1,59 +1,2 @@
/// <summary> Store is a facility to share (built-in JavaScript types or Transportable subclasses) objects across isolates. </summary>
export interface Store {
/// <summary> Id of this store. </summary>
readonly id: string;
/// <summary> Number of keys in this store. </summary>
readonly size: number;
/// <summary> Check if this store has a key. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
/// <returns> True if this store has the key. </returns>
has(key: string): boolean;
/// <summary> Get JavaScript value by key. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
/// <returns> Value for key, undefined if not found. </returns>
get(key: string): any;
/// <summary> Insert or update a JavaScript value by key. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
/// <param name="value"> Value. Any value of built-in JavaScript types or Transportable subclasses can be accepted. </summary>
set(key: string, value: any): void;
/// <summary> Remove a key with its value from this store. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
delete(key: string): void;
}
let binding = require('./binding');
/// <summary> Create a store with an id. </summary>
/// <param name="id"> String identifier which can be used to get the store from all isolates. </summary>
/// <returns> A store object or throws Error if store with this id already exists. </returns>
/// <remarks> Store object will be destroyed when reference from all isolates are unreferenced.
/// It's usually a best practice to keep a long-living reference in user modules or global scope. </remarks>
export function create(id: string): Store {
return binding.createStore(id);
}
/// <summary> Get a store with an id. </summary>
/// <param name="id"> String identifier which is passed to create/getOrCreate earlier. </summary>
/// <returns> A store object if exists, otherwise undefined. </returns>
export function get(id: string): Store {
return binding.getStore(id);
}
/// <summary> Get a store with an id, or create it if not exist. </summary>
/// <param name="id"> String identifier which can be used to get the store from all isolates. </summary>
/// <returns> A store object associated with the id. </returns>
/// <remarks> Store object will be destroyed when reference from all isolates are unreferenced.
/// It's usually a best practice to keep a long-living reference in user modules or global scope. </remarks>
export function getOrCreate(id: string): Store {
return binding.getOrCreateStore(id);
}
/// <summary> Returns number of stores that is alive. </summary>
export function count(): number {
return binding.getStoreCount();
}
export * from './store/store';
export * from './store/store-api';

33
lib/store/store-api.ts Normal file
Просмотреть файл

@ -0,0 +1,33 @@
import { Store } from './store';
let binding = require('../binding');
/// <summary> Create a store with an id. </summary>
/// <param name="id"> String identifier which can be used to get the store from all isolates. </summary>
/// <returns> A store object or throws Error if store with this id already exists. </returns>
/// <remarks> Store object will be destroyed when reference from all isolates are unreferenced.
/// It's usually a best practice to keep a long-living reference in user modules or global scope. </remarks>
export function create(id: string): Store {
return binding.createStore(id);
}
/// <summary> Get a store with an id. </summary>
/// <param name="id"> String identifier which is passed to create/getOrCreate earlier. </summary>
/// <returns> A store object if exists, otherwise undefined. </returns>
export function get(id: string): Store {
return binding.getStore(id);
}
/// <summary> Get a store with an id, or create it if not exist. </summary>
/// <param name="id"> String identifier which can be used to get the store from all isolates. </summary>
/// <returns> A store object associated with the id. </returns>
/// <remarks> Store object will be destroyed when reference from all isolates are unreferenced.
/// It's usually a best practice to keep a long-living reference in user modules or global scope. </remarks>
export function getOrCreate(id: string): Store {
return binding.getOrCreateStore(id);
}
/// <summary> Returns number of stores that is alive. </summary>
export function count(): number {
return binding.getStoreCount();
}

27
lib/store/store.ts Normal file
Просмотреть файл

@ -0,0 +1,27 @@
/// <summary> Store is a facility to share (built-in JavaScript types or Transportable subclasses) objects across isolates. </summary>
export interface Store {
/// <summary> Id of this store. </summary>
readonly id: string;
/// <summary> Number of keys in this store. </summary>
readonly size: number;
/// <summary> Check if this store has a key. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
/// <returns> True if this store has the key. </returns>
has(key: string): boolean;
/// <summary> Get JavaScript value by key. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
/// <returns> Value for key, undefined if not found. </returns>
get(key: string): any;
/// <summary> Insert or update a JavaScript value by key. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
/// <param name="value"> Value. Any value of built-in JavaScript types or Transportable subclasses can be accepted. </summary>
set(key: string, value: any): void;
/// <summary> Remove a key with its value from this store. </summary>
/// <param name="key"> Case-sensitive string key. </summary>
delete(key: string): void;
}

Просмотреть файл

@ -10,10 +10,14 @@ export * from './transport/transport';
import { Handle } from './memory/handle';
import { TransportContext } from './transport/transportable';
import * as functionTransporter from './transport/function-transporter';
let binding = require('./binding');
/// <summary> Create a transport context. </summary>
export function createTransportContext(handle? : Handle): TransportContext {
return new binding.TransportContextWrap(handle);
}
}
export let saveFunction = functionTransporter.save;
export let loadFunction = functionTransporter.load;

Просмотреть файл

@ -0,0 +1,76 @@
////////////////////////////////////////////////////////////////////////
// Module to support function transport.
import { Store } from '../store/store';
import * as assert from 'assert';
/// <summary> Function hash to function cache. </summary>
let _hashToFunctionCache: {[hash: string]: (...args: any[]) => any} = {};
/// <summary> Function to hash cache. </summary>
/// <remarks> Function cannot be used as a key in TypeScript. </remarks>
let _functionToHashCache: any = {};
/// <summary> Marshalled function body cache. </summary>
let _store: Store;
/// <summary> Get underlying store to save marshall function body across isolates. </summary>
function getStore(): Store {
if (_store == null) {
// Lazy creation function store
// 1) avoid circular runtime dependency between store and transport.
// 2) avoid unnecessary cost when function transport is not used.
_store = require('../store/store-api')
.getOrCreate('__napajs_marshalled_functions');
}
return _store;
}
/// <summary> Save function and get a hash string to use it later. </summary>
export function save(func: (...args: any[]) => any): string {
let hash = _functionToHashCache[(<any>(func))];
if (hash == null) {
// Should happen only on first marshall of input function in current isolate.
let body = func.toString();
hash = getFunctionHash(body);
getStore().set(hash, body);
cacheFunction(hash, func);
}
return hash;
}
/// <summary> Load a function with a hash retrieved from `save`. </summary>
export function load(hash: string): (...args: any[]) => any {
let func = _hashToFunctionCache[hash];
if (func == null) {
// Should happen only on first unmarshall of given hash in current isolate..
let body = getStore().get(hash);
if (body == null) {
throw new Error(`Function hash cannot be found: ${hash}`);
}
func = eval(`(${body})`);
cacheFunction(hash, func);
}
return func;
}
/// <summary> Cache function with its hash in current isolate. </summary>
function cacheFunction(hash: string, func: (...args: any[]) => any) {
_functionToHashCache[<any>(func)] = hash;
_hashToFunctionCache[hash] = func;
}
/// <summary> Generate hash for function definition using DJB2 algorithm.
/// See: https://en.wikipedia.org/wiki/DJB2
/// </summary>
function getFunctionHash(functionDef: string): string {
let hash = 5381;
for (let i = 0; i < functionDef.length; ++i) {
hash = (hash * 33) ^ functionDef.charCodeAt(i);
}
/* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
* integers. Since we want the results to be always positive, convert the
* signed int to an unsigned by doing an unsigned bitshift. */
return (hash >>> 0).toString(16);
}

Просмотреть файл

@ -1,5 +1,5 @@
import * as transportable from './transportable';
import * as functionTransporter from './function-transporter';
import * as path from 'path';
/// <summary> Per-isolate cid => constructor registry. </summary>
@ -25,7 +25,7 @@ export function register(subClass: new(...args: any[]) => any) {
/// <summary> Marshall transform a JS value to a plain JS value that will be stringified. </summary>
export function marshallTransform(jsValue: any, context: transportable.TransportContext): any {
if (jsValue != null && typeof jsValue === 'object' && !Array.isArray(jsValue)) {
if (jsValue != null && typeof jsValue === 'object' && !Array.isArray(jsValue)) {
let constructorName = Object.getPrototypeOf(jsValue).constructor.name;
if (constructorName !== 'Object') {
if (typeof jsValue['cid'] === 'function') {
@ -45,6 +45,9 @@ export function marshallTransform(jsValue: any, context: transportable.Transport
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.`);
@ -58,41 +61,33 @@ function unmarshallTransform(payload: any, context: transportable.TransportConte
/// <summary> Unmarshall from JSON string to a JavaScript value, which could contain complex/native objects. </summary>
/// <param name="json"> JSON string. </summary>
/// <param name="reviver"> Reviver that transform parsed values into new values. </param>
/// <param name="context"> Transport context to save shared pointers. </param>
/// <returns> Parsed JavaScript value, which could be built-in JavaScript types or deserialized Transportable objects. </returns>
export function unmarshall(
json: string,
context: transportable.TransportContext,
reviver?: (key: any, value: any) => any): any {
context: transportable.TransportContext): any {
return JSON.parse(json,
(key: any, value: any): any => {
value = unmarshallTransform(value, context);
if (reviver != null) {
value = reviver(key, value);
}
return value;
return unmarshallTransform(value, context);
});
}
/// <summary> Marshall a JavaScript value to JSON. </summary>
/// <param name="jsValue"> JavaScript value to stringify, which maybe built-in JavaScript types or transportable objects. </param>
/// <param name="replacer"> Replace JS value with transformed value before writing to string. </param>
/// <param name="space"> Space used to format JSON. </param>
/// <param name="context"> Transport context to save shared pointers. </param>
/// <returns> JSON string. </returns>
export function marshall(
jsValue: any,
context: transportable.TransportContext,
replacer?: (key: string, value: any) => any,
space?: string | number): string {
context: transportable.TransportContext): string {
// Function is transportable only as root object.
// This is to avoid unexpected marshalling on member functions.
if (typeof jsValue === 'function') {
return `{"_cid": "function", "hash": "${functionTransporter.save(jsValue)}"}`;
}
return JSON.stringify(jsValue,
(key: string, value: any) => {
value = marshallTransform(value, context);
if (replacer) {
value = replacer(key, value);
}
return value;
},
space);
}
return marshallTransform(value, context);
});
}

Просмотреть файл

@ -1,5 +1,5 @@
import * as zone from "./zone";
import * as transport from "../transport";
import * as zone from './zone';
import * as transport from '../transport';
declare var __in_napa: boolean;
@ -136,20 +136,32 @@ export class NapaZone implements zone.Zone {
}
private createExecuteRequest(arg1: any, arg2: any, arg3?: any, arg4?: any) : ExecuteRequest {
if (arg1 instanceof Function) {
throw new Error("Execute with function is not implemented");
let moduleName: string = null;
let functionName: string = null;
let args: any[] = null;
let timeout: number = 0;
if (typeof arg1 === 'function') {
moduleName = "__function";
functionName = transport.saveFunction(arg1);
args = arg2;
timeout = arg3;
}
else {
moduleName = arg1;
functionName = arg2;
args = arg3;
timeout = arg4;
}
let transportContext: transport.TransportContext = transport.createTransportContext();
let request : ExecuteRequest = {
module: arg1,
function: arg2,
arguments: (<Array<any>>arg3).map(arg => { return transport.marshall(arg, transportContext); }),
timeout: arg4,
return {
module: moduleName,
function: functionName,
arguments: (<Array<any>>args).map(arg => { return transport.marshall(arg, transportContext); }),
timeout: timeout,
transportContext: transportContext
};
return request;
}
}
}

Просмотреть файл

@ -1,19 +1,31 @@
var transport = require('napajs/lib/transport');
function __zone_execute__(moduleName, functionName, args, contextHandle) {
var module = (moduleName == null || moduleName.length === 0)? this : require(moduleName);
var func = module;
if (functionName != null && functionName.length != 0) {
var path = functionName.split('.');
for (item of path) {
func = func[item];
if (func === undefined) {
throw new Error("Cannot find function '" + functionName + "' in module '" + moduleName + "'");
var module = null;
if (moduleName == null || moduleName.length === 0) {
module = this;
} else if (moduleName !== '__function') {
module = require(moduleName);
}
var func = null;
if (module != null) {
func = module;
if (functionName != null && functionName.length != 0) {
var path = functionName.split('.');
for (item of path) {
func = func[item];
if (func === undefined) {
throw new Error("Cannot find function '" + functionName + "' in module '" + moduleName + "'");
}
}
}
}
if (typeof func !== 'function') {
throw new Error("'" + functionName + "' in module '" + moduleName + "' is not a function");
if (typeof func !== 'function') {
throw new Error("'" + functionName + "' in module '" + moduleName + "' is not a function");
}
} else {
// Anonymous function.
func = transport.loadFunction(functionName);
}
var transportContext = transport.createTransportContext(contextHandle);

Просмотреть файл

@ -15,6 +15,7 @@
<QCustomInput Include="$(NapaVanillaRoot)\lib\core\core-modules.json" />
<QCustomInput Include="$(NapaVanillaRoot)\package.json" />
<QCustomInput Include="$(NapaVanillaRoot)\README.md" />
<QCustomInput Include="$(NapaVanillaRoot)\docs\**\*" />
</ItemGroup>
<PropertyGroup>
@ -49,7 +50,13 @@
<DeclarationFiles Include="$(NapaVanillaRoot)\lib\$(IntermediateOutputPath)\**\*.d.ts"/>
</ItemGroup>
<Copy SourceFiles="@(DeclarationFiles)" DestinationFiles="@(DeclarationFiles->'$(PackageOutPath)\types\%(RecursiveDir)%(Filename)%(Extension)')" ContinueOnError="false" SkipUnchangedFiles="true" />
<!-- ./docs/*.* -->
<ItemGroup>
<DocFiles Include="$(NapaVanillaRoot)\docs\**\*.*"/>
</ItemGroup>
<Copy SourceFiles="@(DocFiles)" DestinationFiles="@(DocFiles->'$(PackageOutPath)\doc\%(RecursiveDir)%(Filename)%(Extension)')" ContinueOnError="false" SkipUnchangedFiles="true" />
<!-- Setup ./inc directory -->
<ItemGroup>
<NapaIncFiles Include="$(NapaVanillaRoot)\inc\**\*"/>

Просмотреть файл

@ -56,7 +56,13 @@ export function executeTestFunction(id: string): any {
// TODO: replace with execute when TODO:#3 is done.
return zone.executeSync((input: string) => {
return input;
}, ['hello world']);
}, ['hello world']).value;
}
export function executeTestFunctionWithClosure(id: string): any {
let zone = napa.zone.get(id);
// TODO: replace with execute when TODO:#3 is done.
return zone.executeSync(() => { return zone; }, []).value;
}
/// <summary> Memory test helpers. </summary>
@ -169,10 +175,14 @@ export function simpleTypeTransportTest() {
});
}
export function jsTransportableTest() {
export function jsTransportTest() {
testMarshallUnmarshall(new CanPass(napa.memory.crtAllocator));
}
export function functionTransportTest() {
testMarshallUnmarshall(() => { return 0; });
}
export function addonTransportTest() {
testMarshallUnmarshall(napa.memory.debugAllocator(napa.memory.crtAllocator));
}

Просмотреть файл

@ -86,6 +86,26 @@ describe('napajs/store', () => {
assert.deepEqual(store1.get('f'), debugAllocator);
});
it('function type: set in node, get in node', () => {
store1.set('g', () => { return 0; });
assert.equal(store1.get('g').toString(), (() => { return 0; }).toString());
});
it('function type: set in node, get in napa', () => {
store1.set('h', () => { return 0; });
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "storeVerifyGet", ['store1', 'h', () => { return 0; }]);
});
it('function type: set in napa, get in napa', () => {
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "storeSet", ['store1', 'i', () => { return 0; }]);
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "storeVerifyGet", ['store1', 'i', () => { return 0; }]);
});
it('function type: set in napa, get in node', () => {
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "storeSet", ['store1', 'j', () => { return 0; }]);
assert.deepEqual(store1.get('j').toString(), (() => { return 0; }).toString());
});
it('delete in node, check in node', () => {
assert(store1.has('a'));
store1.delete('a');
@ -112,9 +132,9 @@ describe('napajs/store', () => {
});
it('size', () => {
// set 'a', 'b', 'c', 'd', 'a', 'b', 'e', 'f'.
// set 'a', 'b', 'c', 'd', 'a', 'b', 'e', 'f', 'g', 'h', 'i', 'j'.
// delete 'a', 'b', 'c', 'd'
assert.equal(store1.size, 2);
assert.equal(store1.size, 6);
});
});

Просмотреть файл

@ -34,6 +34,7 @@ describe('napajs/transport', () => {
assert(napa.transport.isTransportable([1, 2, new t.CanPass(napa.memory.crtAllocator)]));
assert(napa.transport.isTransportable({ a: 1}));
assert(napa.transport.isTransportable({ a: 1, b: new t.CanPass(napa.memory.crtAllocator)}));
assert(napa.transport.isTransportable(() => { return 0; }));
assert(!napa.transport.isTransportable(new t.CannotPass()));
assert(!napa.transport.isTransportable([1, new t.CannotPass()]));
assert(!napa.transport.isTransportable({ a: 1, b: new t.CannotPass()}));
@ -50,11 +51,11 @@ describe('napajs/transport', () => {
}).timeout(3000);
it('@node: JS transportable', () => {
t.jsTransportableTest();
t.jsTransportTest();
});
it('@napa: JS transportable', () => {
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "jsTransportableTest", []);
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "jsTransportTest", []);
});
it('@node: addon transportable', () => {
@ -65,6 +66,14 @@ describe('napajs/transport', () => {
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "addonTransportTest", []);
});
it('@node: function transportable', () => {
t.functionTransportTest();
});
it('@napa: function transportable', () => {
napaZone.executeSync(NAPA_ZONE_TEST_MODULE, "functionTransportTest", []);
});
it('@node: composite transportable', () => {
t.compositeTransportTest();
});

Просмотреть файл

@ -484,7 +484,7 @@ describe('napajs/zone', function () {
});
});
// Blocked by TODO #1 and TODO #5.
// Blocked by TODO #1.
it.skip('@node: -> node zone with anonymous function', () => {
return napa.zone.current.execute((input: string) => {
return input;
@ -494,8 +494,7 @@ describe('napajs/zone', function () {
});
});
// TODO #5: implment anonymous function in execute/executeSync.
it.skip('@node: -> napa zone with anonymous function', () => {
it('@node: -> napa zone with anonymous function', () => {
return napaZone1.execute((input: string) => {
return input;
}, ['hello world'])
@ -504,8 +503,7 @@ describe('napajs/zone', function () {
});
});
// Blocked by TODO #5.
it.skip('@napa: -> napa zone with anonymous function', () => {
it('@napa: -> napa zone with anonymous function', () => {
return napaZone1.execute(napaZoneTestModule, 'executeTestFunction', ["napa-zone2"])
.then((result: napa.zone.ExecuteResult) => {
assert.equal(result.value, 'hello world');
@ -524,14 +522,23 @@ describe('napajs/zone', function () {
it.skip('@node: -> node zone with anonymous function having closure (should fail)', () => {
});
it.skip('@node: -> napa zone with anonymous function having closure (should fail)', () => {
it('@node: -> napa zone with anonymous function having closure (should fail)', () => {
return shouldFail(() => {
return napaZone1.execute(() => { return napaZone1; }, ['hello world'])
});
});
it.skip('@napa: -> napa zone with anonymous function having closure (should fail)', () => {
it('@napa: -> napa zone with anonymous function having closure (should fail)', () => {
return shouldFail(() => {
return napaZone1.execute(napaZoneTestModule, 'executeTestFunctionWithClosure', ["napa-zone2"]);
});
});
// Blocked by TODO #1.
it.skip('@napa: -> node zone with anonymous function having closure (should fail)', () => {
return shouldFail(() => {
return napaZone1.execute(napaZoneTestModule, 'executeTestFunctionWithClosure', ["node"]);
});
});
// Blocked by TODO #1.