« get me outta code hell

contract: BlackBox stub & NormalizedArrayMap - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-05-07 19:21:41 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-05-07 19:21:41 -0300
commit86e8b47b5aeeae5f2fc3b87bb5930fb4c25f88ab (patch)
tree558ad9c3a647a139b0ca7d0f936c7861fa6162d3
parent63b1b5b6fd14d3bacdcb979298b4fa669de4f20b (diff)
contract: BlackBox stub & NormalizedArrayMap
Spooky scary skeletons

Also includes a bonus very dumb performance test!
-rw-r--r--src/contract.js218
-rw-r--r--test/unit/contract/black-box.js81
2 files changed, 259 insertions, 40 deletions
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;
+  }
+});