From 86e8b47b5aeeae5f2fc3b87bb5930fb4c25f88ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 7 May 2023 19:21:41 -0300 Subject: contract: BlackBox stub & NormalizedArrayMap Spooky scary skeletons Also includes a bonus very dumb performance test! --- src/contract.js | 218 ++++++++++++++++++++++++++++++++-------- test/unit/contract/black-box.js | 81 +++++++++++++++ 2 files changed, 259 insertions(+), 40 deletions(-) create mode 100644 test/unit/contract/black-box.js diff --git a/src/contract.js b/src/contract.js index 737f1bbd..75b2bce3 100644 --- a/src/contract.js +++ b/src/contract.js @@ -1,3 +1,111 @@ +export class NormalizedWeakMap extends WeakMap { + #cache = new WeakMap(); + + normalize(key) { + throw new Error(`normalize not implemented`); + } + + #cachedNormalize(key) { + if (typeof key !== 'object') { + throw new TypeError(`Expected key to be an object`); + } + + if (this.#cache.has(key)) { + return this.#cache.get(key); + } else { + const normalized = this.normalize(key); + this.#cache.set(key, normalized); + return normalized; + } + } + + get(key) { + return super.get(this.#cachedNormalize(key)); + } + + set(key, value) { + return super.set(this.#cachedNormalize(key), value); + } + + has(key) { + return super.has(this.#cachedNormalize(key)); + } + + delete(key) { + return super.delete(this.#cachedNormalize(key)); + } +} + +export class NormalizedArrayMap extends NormalizedWeakMap { + #topCache = this.#createCache([]); + + normalize(array) { + let index = 0; + let cache = this.#topCache; + let infantCache = false; + + while (index < array.length) { + const item = array[index]; + const whichCache = (typeof item === 'object' ? 1 : 2); + + let nextCache = undefined; + + if (!infantCache) { + // Note: This could still be undefined - infantCache just skips the get + // op here because it would *definitely* be undefined. + nextCache = cache[whichCache].get(item); + } + + if (nextCache === undefined) { + nextCache = this.#createCache(); + cache[whichCache].set(item, nextCache); + infantCache = true; + } + + cache = nextCache; + index++; + } + + return cache[0]; + } + + #createCache() { + return [{}, new WeakMap(), new Map()]; + } +} + +export class BlackBox { + profiling = true; /* Unused for now */ + caching = true; + + #cache = new NormalizedArrayMap(); + #computeFunction = null; + + constructor(computeFunction) { + this.#computeFunction = computeFunction; + } + + getEvaluator() { + return (...args) => { + if (this.caching) { + return this.evaluateCached(...args); + } else { + return this.#computeFunction(...args); + } + }; + } + + evaluateCached(...args) { + if (this.#cache.has(args)) { + return this.#cache.get(args); + } else { + const result = this.#computeFunction(...args); + this.#cache.set(args, result); + return result; + } + } +} + export class ContractManager { #registeredContracts = Object.create(null); @@ -35,55 +143,27 @@ export class ContractManager { const subcontracts = {}; const structure = {}; - const contextualizeHook = (args, {type, ...hook}) => { - switch (type) { - case 'argument': - return {type: 'argument', index: hook.index}; - case 'selectPropertyPath': { - /* - switch (hook.object.type) { - case 'argument': - console.log('select argument', hook.object.index, '=', args[hook.object.index]); - return {type: 'selectPropertyPath', object: args[hook.object.index], path: hook.path}; - case 'selectPropertyPath': - console.log('merge', hook.object.path, 'with', hook.path); - return {type: 'selectPropertyPath', object: args[hook.object.object.index], path: [...hook.object.path, ...hook.path]}; - default: - throw new Error(`Can't contextualize unknown hook type OF OBJECT ${hook.object.type}`); - } - */ - const contextualizedObject = contextualizeHook(args, hook.object); - console.log(`contextualized property object:`, contextualizedObject); - switch (contextualizedObject.type) { - case 'argument': - return {type: 'selectPropertyPath', object: args[contextualizedObject.index], path: hook.path}; - case 'selectPropertyPath': - return {type: 'selectPropertyPath', object: contextualizedObject.object, path: [...contextualizedObject.path, ...hook.path]}; - } - } - default: - throw new Error(`Can't contextualize unknown hook type ${type}`); - } - }; - const contractUtility = { subcontract: (name, ...args) => { - const info = this.getContractInfo(name.startsWith('#') ? name.slice(1) : name); + const hook = {type: 'subcontract', name, args}; + const shape = {type: 'subcontract', name, args}; - for (const hook of info.hooks) { - hooks.push(contextualizeHook(args, hook)); - } - - return {type: 'subcontract', name, args}; + hooks.push(hook); + return shape; }, provide: (properties) => { Object.assign(structure, properties); }, - selectProperty: (object, property) => { - hooks.push(contextualizeHook(args, {type: 'selectPropertyPath', object, path: [property]})); - return {type: 'selectPropertyPath', object, path: [property]}; + selectProperty: (object, propertyString) => { + const propertyPath = propertyString.split('.'); + + const hook = {type: 'selectPropertyPath', object, path: propertyPath}; + const shape = {type: 'selectPropertyPath', object, path: propertyPath}; + + hooks.push(hook); + return shape; }, }; @@ -97,6 +177,7 @@ export class ContractManager { } } +/* const {default: {contracts}} = await import('./content/dependencies/generateAlbumTrackList.js'); const util = await import('util'); @@ -116,3 +197,60 @@ for (const hook of manager.getContractHooks(testContract)) { } // console.log(util.inspect(manager.getContractInfo(testContract).structure, {colors: true, depth: Infinity})); +*/ + +// lousy perf test +if ((await import('./util/node-utils.js')).isMain(import.meta.url)) { + const obj1 = {foo: 3, bar: 4}; + const obj2 = {baz: 5, qux: 6}; + + let fn = (object, key) => object[key] ** 2; + let bb = new BlackBox(fn); + let evaluate = bb.getEvaluator(); + + const gogogo = (once) => { + let iters = 0; + for (let end = Date.now() + 1000; Date.now() < end;) { + once(obj1, 'foo'); + once(obj1, 'foo'); + once(obj2, 'qux'); + once(obj2, 'qux'); + once(obj1, 'foo'); + once(obj1, 'bar'); + once(obj2, 'baz'); + once(obj1, 'foo'); + iters += 8; + } + return iters; + }; + + console.log(`Iterations - black box w/ cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/ cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/ cache: ${gogogo(evaluate)}`); + + bb.caching = false; + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + + bb.caching = false; + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + console.log(`Iterations - black box w/o cache: ${gogogo(evaluate)}`); + + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); + console.log(`Iterations - direct pass to fn: ${gogogo(fn)}`); +} diff --git a/test/unit/contract/black-box.js b/test/unit/contract/black-box.js new file mode 100644 index 00000000..21c05b52 --- /dev/null +++ b/test/unit/contract/black-box.js @@ -0,0 +1,81 @@ +import {BlackBox} from '../../../src/contract.js'; +import {mockFunction} from '../../lib/generic-mock.js'; +import {showAggregate} from '../../../src/util/sugar.js'; + +import t from 'tap'; + +t.test(`BlackBox - caching`, t => { + t.plan(8); + + const obj1 = {foo: 3, bar: 4}; + const obj2 = {baz: 5, qux: 6}; + + let {value: fn, close: closeMock} = + mockFunction((object, key) => object[key] ** 2) + + fn = fn + .args([obj1, 'foo']).next() + .args([obj2, 'qux']).next() + .args([obj1, 'bar']).next() + .args([obj2, 'baz']); + + const bb = new BlackBox(fn); + const evaluate = bb.getEvaluator(); + + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + t.equal(evaluate(obj2, 'qux'), 6 ** 2); + t.equal(evaluate(obj2, 'qux'), 6 ** 2); + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + + t.equal(evaluate(obj1, 'bar'), 4 ** 2); + t.equal(evaluate(obj2, 'baz'), 5 ** 2); + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + + try { + closeMock(); + } catch (error) { + showAggregate(error); + throw error; + } +}); + +t.test(`BlackBox - no caching`, t => { + t.plan(8); + + const obj1 = {foo: 3, bar: 4}; + const obj2 = {baz: 5, qux: 6}; + + let {value: fn, close: closeMock} = + mockFunction((object, key) => object[key] ** 2) + + fn = fn + .args([obj1, 'foo']).repeat(2) + .args([obj2, 'qux']).repeat(2) + .args([obj1, 'foo']).next() + .args([obj1, 'bar']).next() + .args([obj2, 'baz']).next() + .args([obj1, 'foo']); + + const bb = new BlackBox(fn); + const evaluate = bb.getEvaluator(); + + bb.caching = false; + + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + t.equal(evaluate(obj2, 'qux'), 6 ** 2); + t.equal(evaluate(obj2, 'qux'), 6 ** 2); + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + + t.equal(evaluate(obj1, 'bar'), 4 ** 2); + t.equal(evaluate(obj2, 'baz'), 5 ** 2); + t.equal(evaluate(obj1, 'foo'), 3 ** 2); + + try { + closeMock(); + } catch (error) { + showAggregate(error); + throw error; + } +}); -- cgit 1.3.0-6-gf8a5