From dc0d62cf7e43ee618964c686d81d257dbcc9a2c2 Mon Sep 17 00:00:00 2001 From: Mariusz Nowak Date: Mon, 6 Aug 2012 12:40:20 +0200 Subject: [PATCH] Initial (derived from es5-ext package) --- .gitignore | 3 + .travis.yml | 12 ++ CHANGES | 2 + LICENCE | 19 +++ Makefile | 9 ++ README.md | 143 +++++++++++++++++++++ lib/index.js | 292 ++++++++++++++++++++++++++++++++++++++++++ lib/profile.js | 57 +++++++++ package.json | 43 +++++++ test/index.js | 333 ++++++++++++++++++++++++++++++++++++++++++++++++ test/profile.js | 9 ++ 11 files changed, 922 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGES create mode 100644 LICENCE create mode 100644 Makefile create mode 100644 README.md create mode 100644 lib/index.js create mode 100644 lib/profile.js create mode 100644 package.json create mode 100644 test/index.js create mode 100644 test/profile.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90f2817 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +/node_modules +/npm-debug.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..530a8af --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: + - 0.6 + - 0.8 + - 0.9 + +before_script: + - ln -s ../ node_modules/memoize + +notifications: + email: + - medikoo+memoize@medikoo.com diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..8f71b61 --- /dev/null +++ b/CHANGES @@ -0,0 +1,2 @@ +v0.1.0 -- ? +* Initial (taken out from es5-ext package) diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..4d75961 --- /dev/null +++ b/LICENCE @@ -0,0 +1,19 @@ +Copyright (C) 2012 Mariusz Nowak (www.medikoo.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..065e848 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +SHELL = bash + +install: + npm install + +test: + npm test + +.PHONY: install test diff --git a/README.md b/README.md new file mode 100644 index 0000000..811f2e4 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# memoize - Memoization for any type and length of function arguments + +Complete memoize solution, originally derived from [es5-ext](https://github.com/medikoo/es5-ext) package. Works with any type and length of function arguments. It's one of the fastest solutions available. + +```javascript + memoized = memoize(fn); + + memoized('foo', 3); + memoized('foo', 3); // Cache hit +``` + +## Installation +### Node.js + +In your project path: + + $ npm install memoizee + + +### Browser + +You can easily create browser bundle with help of [modules-webmake](https://github.com/medikoo/modules-webmake). Mind that it relies on some EcmaScript5 features, so for older browsers you need as well [es5-shim](https://github.com/kriskowal/es5-shim) + + +## Options +### Arguments length + +By default fixed number of arguments that function takes is assumed (it's +assumed on function's `length` property) this behaviour can be overriden: + +```javascript + memoized = memoize(fn, { length: 2 }); + + memoized('foo'); // Assumed ('foo', undefined) + memoized('foo', undefined); // Cache hit + + memoized('foo', 3, {}); // Third argument is ignored (but passed to underlying function) + memoized('foo', 3, 13); // Cache hit +``` + +Dynamic _length_ behavior can be forced by setting `length` to `false`, that means memoize will work with any number of arguments. + +```javascript + memoized = memoize(fn, { length: false }); + + memoized('foo'); + memoized('foo'); // Cache hit + memoized('foo', undefined); + memoized('foo', undefined); // Cache hit + + memoized('foo', 3, {}); + memoized('foo', 3, 13); + memoized('foo', 3, 13); // Cache hit +``` + +### Resolvers + +When expecting arguments of certain types it's good to coerce them before doing memoization. We can do that by passing additional resolving functions array: + +```javascript + memoized = memoize(fn, { length: 2, resolvers: [String, Boolean] }); + + memoized(12, [1,2,3].length); + memoized("12", true); // Cache hit + memoized({ toString: function () { return "12"; } }, {}); // Cache hit +``` + +### Primitive mode + +Dealing with input arguments as they are, may not be performant on large result sets. Optionally memoization can be run in _primitive_ mode, internally then obtained results are saved on hash (not array) and arguments are coerced to strings to generate unique hash id. +This mode will work properly only if your arguments can be coerced to unique strings. Mind also that perfmance gain when using this mode is only observed on large result sets (thousands of results) otherwise is not worth a hassle. + +```javascript + memoized = memoize(fn, { primitive: true }); + + memoized('/path/one'); + memoized('/path/one'); // Cache hit +``` + +### Memoizing a method + +When we're defining a prototype, we may want to define method that will memoize it's results in relation to each instance. Normal way to obtain that would be: + +```javascript + var Foo = function () { + memoize(this.bar.bind(this)); + // ... constructor logic + }; + Foo.prototype.bar = function () { + // ... method logic + }; +``` + +With `method` option we can configure memoization directly on prototype and not in constructor. Following will have same effect: + + var Foo = function () { + // ... constructor logic + }; + Foo.prototype.bar = memoize(function () { + // ... method logic + }, { method: 'bar' }); + +Additionally we may provide descriptor which would be used for defining method on instance object: + + var Foo = function () { + // ... constructor logic + }; + Foo.prototype.bar = memoize(function () { + // ... method logic + }, { method: { name: 'bar', descriptor: { configurable: true } } }); + +#### Cache handling + +Collected cache can be cleared. To clear all collected data: + + memoizedFn.clearAll(); + +or to clear data for particall call. + + memoizedFn.clear('foo', true); + +Arguments passed to `clear` are treated with same rules as input arguments passed to function + +## Profiling & Statistics + +`lib/profile` module provides statistical data about memoized function calls. How many calls where initial and how many were result of cache hit. To collect data it needs to be imported before memoize initialization of functions that we want to track. + +```javascript + +var memProfile = require('memoizee/lib/profile') + , memoize = require('memoizee'); + +var memoized = memoize(fn); +memoized(1, 2); +memoized(1, 2); + +memProfile.statistics // Holds statistics data in convenient hash form +memProfile.log(); // Outputs statictis data in readable form (using console.log) +``` + +## Tests [![Build Status](https://secure.travis-ci.org/medikoo/memoize.png?branch=master)](https://secure.travis-ci.org/medikoo/memoize) + + $ npm test diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..36b07fb --- /dev/null +++ b/lib/index.js @@ -0,0 +1,292 @@ +'use strict'; + +var isArray = Array.isArray + , join = Array.prototype.join + , map = Array.prototype.map + , push = Array.prototype.push + , slice = Array.prototype.slice + , apply = Function.prototype.apply + , create = Object.create + , defineProperty = Object.defineProperty + , global = require('es5-ext/lib/global') + , toArray = require('es5-ext/lib/Array/from') + , indexOf = require('es5-ext/lib/Array/prototype/e-index-of') + , callable = require('es5-ext/lib/Object/valid-callable') + , isString = require('es5-ext/lib/String/is-string') + + , resolve, memoize, id = 0; + +resolve = function (args) { + return this.map(function (r, i) { + return r ? r(args[i]) : args[i]; + }).concat(slice.call(args, this.length)); +}; + +// Implementation details: +// +// Results are saved internally within array matrix: +// cache[0] -> Result of calling function with no arguments +// cache[1] -> Matrix that keeps results for one argument function calls +// cache[1][0] -> Array of different arguments with which +// function have been called +// cache[1][1] -> Array of results that matches cache[1][0] arguments array +// cache[2] -> Matrix that keeps results for two argument function calls +// cache[2][0] -> Array of different first (of two) arguments with which +// function have been called +// cache[2][1] -> Matrixes that keeps results for two arguments function calls +// Each matrix matches first argument found in cache[2][0] +// cache[2][1][x][0] -> Array of different second arguments with which +// function have been called. +// cache[2][1][x][1] -> Array of results that matches cache[2][1][x][0] +// arguments array +// ...and so on + +memoize = module.exports = function () { + var fn, mfn, options, length, resolver, method, cache, find, save, clear + , value, primitive, getPrimitiveId, profile, gcMode, onclear; + + fn = callable(this); + if (fn.memoized) { + // Prevent memoization of already memoized function + return fn; + } + + options = Object(arguments[0]); + + if (isNaN(options.length)) { + length = fn.length; + } else if (options.length === false) { + length = options.length; + } else { + length = Number(options.length); + } + if (options.resolvers) { + resolver = toArray(options.resolvers); + resolver.forEach(function (r) { + (r == null) || callable(r); + }); + resolver = resolve.bind(resolver); + } + if (options.method) { + if (isString(options.method)) { + method = { name: String(options.method), + descriptor: { configurable: true, writable: true } } + } else { + method = options.method; + method.name = String(method.name); + method.descriptor = (method.descriptor == null) ? + { configurable: true, writable: true } : Object(method.descriptor); + } + options = create(options); + options.method = undefined; + } + primitive = Boolean(options.primitive); + gcMode = Boolean(options.gcMode); + if (options.onclear != null) { + onclear = callable(options.onclear); + } + + cache = primitive ? {} : []; + + if (memoize._profile) { + profile = memoize._profile(); + } + + find = function (length, args) { + var index = 0, rset = cache, i; + + if (length === 0) { + value = rset[length]; + return rset.hasOwnProperty(length); + } else if ((rset = rset[length])) { + while (index < (length - 1)) { + i = indexOf.call(rset[0], args[index]); + if (i === -1) { + return false; + } + rset = rset[1][i]; + ++index; + } + i = indexOf.call(rset[0], args[index]); + if (i === -1) { + return false; + } + value = rset[1][i]; + if (gcMode) { + ++value.count; + value = value.value; + } + return true; + } + return false; + }; + + save = function (length, args, value) { + var index = 0, rset = cache, i; + + if (gcMode) { + value = { value: value, count: 1 }; + } + if (length === 0) { + rset[length] = value; + } else { + if (!rset[length]) { + rset[length] = [[], []]; + } + rset = rset[length]; + while (index < (length - 1)) { + i = indexOf.call(rset[0], args[index]); + if (i === -1) { + i = rset[0].push(args[index]) - 1; + rset[1].push([[], []]); + } + rset = rset[1][i]; + ++index; + } + i = indexOf.call(rset[0], args[index]); + if (i === -1) { + i = rset[0].push(args[index]) - 1; + } + rset[1][i] = value; + } + }; + + clear = function (length, args) { + var index = 0, rset = cache, i, path = [], value; + + if (length === 0) { + if (gcMode) { + value = rset[length]; + if (value) { + if (--value.count) { + return; + } + delete rset[length]; + } + } else { + delete rset[length]; + } + } else if ((rset = rset[length])) { + while (index < (length - 1)) { + i = indexOf.call(rset[0], args[index]); + if (i === -1) { + return; + } + path.push(rset, i); + rset = rset[1][i]; + ++index; + } + i = indexOf.call(rset[0], args[index]); + if (i === -1) { + return; + } + if (gcMode) { + value = rset[1][i]; + if (--value.count) { + return; + } + } + rset[0].splice(i, 1); + rset[1].splice(i, 1); + while (!rset[0].length && path.length) { + i = path.pop(); + rset = path.pop(); + rset[0].splice(i, 1); + rset[1].splice(i, 1); + } + } + if (value && onclear) { + onclear(value.value, args); + } + }; + + getPrimitiveId = function (length, args) { + var argsLength; + if (length) { + argsLength = args.length; + if (length < argsLength) { + args = slice.call(args, 0, length); + } else if (length > argsLength) { + args = toArray(args); + push.apply(args, Array(length - argsLength)); + } + return (length > 1) ? join.call(args, '\u0001') : + (args[0] + '\u0002'); + } else { + return ''; + } + }; + + mfn = function () { + var args, alength, id; + if (method && this && (this !== global)) { + method.descriptor.value = memoize.call(fn.bind(this), options); + defineProperty(this, method.name, method.descriptor); + return method.descriptor.value.apply(this, arguments); + } + args = resolver ? resolver(arguments) : arguments; + alength = (length === false) ? args.length : length; + + if (primitive) { + id = getPrimitiveId(alength, args); + if (cache.hasOwnProperty(id)) { + profile && ++profile.cached; + if (gcMode) { + ++cache[id].count; + return cache[id].value; + } + return cache[id]; + } else { + profile && ++profile.initial; + mfn.args = arguments; + mfn.preventCache = false; + value = apply.call(fn, this, args); + if (!mfn.preventCache) { + cache[id] = gcMode ? { value: value, count: 1 } : value; + } + delete mfn.args; + return value; + } + } + if (find(alength, args)) { + profile && ++profile.cached; + return value; + } else { + profile && ++profile.initial; + mfn.args = arguments; + mfn.preventCache = false; + value = apply.call(fn, this, args); + if (!mfn.preventCache) { + save(alength, args, value); + } + delete mfn.args; + return value; + } + }; + mfn.memoized = true; + + mfn.clear = function () { + var args, alength, id, value; + args = resolver ? resolver(arguments) : arguments; + alength = (length === false) ? args.length : length; + + if (primitive) { + id = getPrimitiveId(alength, args); + if (gcMode && ((value = cache[id])) && (--value.count)) { + return; + } + delete cache[id]; + if (value && onclear) { + onclear(value.value, args); + } + } else { + clear(alength, args); + } + }; + + mfn.clearAll = function () { + cache = primitive ? {} : []; + }; + + return mfn; +}; diff --git a/lib/profile.js b/lib/profile.js new file mode 100644 index 0000000..e13647f --- /dev/null +++ b/lib/profile.js @@ -0,0 +1,57 @@ +'use strict'; + +var max = Math.max + , partial = require('es5-ext/lib/Function/prototype/partial') + , forEach = require('es5-ext/lib/Object/for-each') + , pad = require('es5-ext/lib/String/prototype/pad') + , memoize = require('./') + + , stats = exports.statistics = {}; + +memoize._profile = function () { + var id, stack = (new Error()).stack; + id = stack.split('\n')[3].replace(/\n/g, "\\n").trim(); + return (stats[id] = { initial: 0, cached: 0 }); +}; + +exports.log = function () { + var initial, cached, ordered, ipad, cpad, ppad, toPrc; + + initial = cached = 0; + ordered = []; + toPrc = function (initial, cached) { + if (!initial && !cached) { + return '0.00%'; + } + return ((cached / (initial + cached)) * 100).toFixed(2) + '%'; + }; + + console.log("------------------------------------------------------------"); + console.log("Memoize statistics:"); + + forEach(stats, function (data, name) { + initial += data.initial; + cached += data.cached; + ordered.push([name, data]); + }, null, function (a, b) { + return (this[b].initial + this[b].cached) - + (this[a].initial + this[a].cached); + }); + + console.log(""); + ipad = partial.call(pad, " ", + max(String(initial).length, "Initial".length)); + cpad = partial.call(pad, " ", max(String(cached).length, "From cache".length)); + ppad = partial.call(pad, " ", 6); + console.log(ipad.call("Initial"), " ", cpad.call("From cache"), + " ", ppad.call("Gain")); + console.log(ipad.call(initial), " ", cpad.call(cached), + " ", ppad.call(toPrc(initial, cached)), " Total"); + ordered.forEach(function (data) { + var name = data[0]; + data = data[1]; + console.log(ipad.call(data.initial), " ", cpad.call(data.cached), + " ", ppad.call(toPrc(data.initial, data.cached)), " " + name); + }); + console.log("------------------------------------------------------------"); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..84a4977 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "memoizee", + "version": "0.1.0", + "description": "Memoization for any type and length of function arguments", + "main": "lib", + "scripts": { + "test": "node node_modules/tad/bin/tad lib" + }, + "repository": { + "type": "git", + "url": "git://github.com/medikoo/memoize.git" + }, + "keywords": [ + "memoize", + "cache", + "memoization", + "memo", + "memcached", + "hashing.", + "storage", + "caching", + "memory", + "gc", + "weak", + "garbage", + "collector" + ], + "bugs": { + "email": "medikoo+memoize@medikoo.com", + "url": "https://github.com/medikoo/memoize/issues" + }, + "engines": { + "node": ">=0.4" + }, + "dependencies": { + "es5-ext": "git://github.com/medikoo/es5-ext.git" + }, + "devDependencies": { + "tad": "0.1.x" + }, + "author": "Mariusz Nowak (http://www.medikoo.com/)", + "license": "MIT" +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a1cfca1 --- /dev/null +++ b/test/index.js @@ -0,0 +1,333 @@ +'use strict'; + +var toArray = require('es5-ext/lib/Array/from'); + +module.exports = function (t, a) { + return { + "0": function () { + var i = 0, fn = function () { ++i; return 3; }; + + fn = t.call(fn); + a(fn(), 3, "First"); + a(fn(1), 3, "Second"); + a(fn(5), 3, "Third"); + a(i, 1, "Called once"); + }, + "1": function () { + var i = 0, fn = function (x) { ++i; return x; }; + + fn = t.call(fn); + return { + "No arg": function () { + i = 0; + a(fn(), undefined, "First"); + a(fn(), undefined, "Second"); + a(fn(), undefined, "Third"); + a(i, 1, "Called once"); + }, + "Arg": function () { + var x = {}; + i = 0; + a(fn(x, 8), x, "First"); + a(fn(x, 4), x, "Second"); + a(fn(x, 2), x, "Third"); + a(i, 1, "Called once"); + }, + "Other Arg": function () { + var x = {}; + i = 0; + a(fn(x, 2), x, "First"); + a(fn(x, 9), x, "Second"); + a(fn(x, 3), x, "Third"); + a(i, 1, "Called once"); + } + }; + }, + "3": function () { + var i = 0, fn = function (x, y, z) { ++i; return [x, y, z]; }, r; + + fn = t.call(fn); + return { + "No args": function () { + i = 0; + a.deep(r = fn(), [undefined, undefined, undefined], "First"); + a(fn(), r, "Second"); + a(fn(), r, "Third"); + a(i, 1, "Called once"); + }, + "Some Args": function () { + var x = {}; + i = 0; + a.deep(r = fn(x, 8), [x, 8, undefined], "First"); + a(fn(x, 8), r, "Second"); + a(fn(x, 8), r, "Third"); + a(i, 1, "Called once"); + return { + "Other": function () { + a.deep(r = fn(x, 5), [x, 5, undefined], "Second"); + a(fn(x, 5), r, "Third"); + a(i, 2, "Called once"); + } + }; + }, + "Full stuff": function () { + var x = {}; + i = 0; + a.deep(r = fn(x, 8, 23, 98), [x, 8, 23], "First"); + a(fn(x, 8, 23, 43), r, "Second"); + a(fn(x, 8, 23, 9), r, "Third"); + a(i, 1, "Called once"); + return { + "Other": function () { + a.deep(r = fn(x, 23, 8, 13), [x, 23, 8], "Second"); + a(fn(x, 23, 8, 22), r, "Third"); + a(i, 2, "Called once"); + } + }; + } + }; + }, + "Dynamic": function () { + var i = 0, fn = function () { ++i; return arguments; }, r; + + fn = t.call(fn, { length: false }); + return { + "No args": function () { + i = 0; + a.deep(toArray(r = fn()), [], "First"); + a(fn(), r, "Second"); + a(fn(), r, "Third"); + a(i, 1, "Called once"); + }, + "Some Args": function () { + var x = {}; + i = 0; + a.deep(toArray(r = fn(x, 8)), [x, 8], "First"); + a(fn(x, 8), r, "Second"); + a(fn(x, 8), r, "Third"); + a(i, 1, "Called once"); + }, + "Many args": function () { + var x = {}; + i = 0; + a.deep(toArray(r = fn(x, 8, 23, 98)), [x, 8, 23, 98], "First"); + a(fn(x, 8, 23, 98), r, "Second"); + a(fn(x, 8, 23, 98), r, "Third"); + a(i, 1, "Called once"); + } + }; + }, + "Original arguments": function (a) { + var fn, mfn, x = {}; + fn = function (x, y) { return toArray(mfn.args); }; + mfn = t.call(fn); + + a.deep(mfn(23, 'raz', x), [23, 'raz', x]); + }, + "Resolvers": function () { + var i = 0, fn, fn2, r, j = 0, z; + fn = t.call(function () { ++i; return arguments; }, + { length: 3, resolvers: [Boolean, String] }); + return { + "No args": function () { + i = 0; + a.deep(toArray(r = fn()), [false, 'undefined'], "First"); + a(fn(), r, "Second"); + a(fn(), r, "Third"); + a(i, 1, "Called once"); + }, + "Some Args": function () { + var x = {}; + i = 0; + a.deep(toArray(r = fn(0, 34, x, 45)), [false, '34', x, 45], + "First"); + a(fn(0, 34, x, 22), r, "Second"); + a(fn(0, 34, x, false), r, "Third"); + a(i, 1, "Called once"); + return { + "Other": function () { + a.deep(toArray(r = fn(1, 34, x, 34)), + [true, '34', x, 34], "Second"); + a(fn(1, 34, x, 89), r, "Third"); + a(i, 2, "Called once"); + } + }; + } + }; + }, + "Clear Cache": { + "Specific": function () { + var i = 0, fn, mfn, r, x = {}; + + fn = function (a, b, c) { + if (c === 3) { + ++i; + } + return arguments; + } + + mfn = t.call(fn); + mfn(1, x, 3); + mfn(1, x, 4); + mfn.clear(1, x, 4); + mfn(1, x, 3); + mfn(1, x, 3); + a(i, 1, "Pre clear"); + mfn.clear(1, x, 3); + mfn(1, x, 3); + a(i, 2, "After clear"); + + i = 0; + mfn = t.call(fn, { length: false }); + mfn(1, x, 3); + mfn(1, x, 3); + mfn(); + mfn(); + mfn.clear(); + mfn(1, x, 3); + a(i, 1, "Proper no arguments clear"); + }, + "All": function () { + var i = 0, fn, r, x = {}; + + fn = function (a, b, c) { + ++i; + return arguments; + } + + fn = t.call(fn); + fn(1, x, 3); + fn(1, x, 4); + fn(1, x, 3); + fn(1, x, 4); + a(i, 2, "Pre clear"); + fn.clearAll(); + fn(1, x, 3); + fn(1, x, 4); + fn(1, x, 3); + fn(1, x, 4); + a(i, 4, "After clear"); + } + }, + "Method": { + "No descriptor": function (a) { + var mfn, x = {}, i = 0, fn = function () { + ++i; + return this; + }; + + mfn = t.call(fn, { method: 'foo' }); + a(mfn.call(x), x, "Context"); + a(x.foo(), x, "Method"); + a(i, 1, "Cached"); + }, + "Descriptor": function (a) { + var mfn, x = {}, i = 0, fn = function () { + ++i; + return this; + }; + + mfn = t.call(fn, + { method: { name: 'foo', descriptor: { configurable: true } } }); + a(mfn.call(x), x, "Context"); + a.deep(Object.getOwnPropertyDescriptor(x, 'foo'), + { enumerable: false, configurable: true, writable: false, + value: x.foo }); + a(x.foo(), x, "Method"); + a(i, 1, "Cached"); + } + }, + "Primitive": { + "No args": function (a) { + var i = 0, fn = function () { ++i; return arguments[0]; }, mfn; + mfn = t.call(fn, { primitive: true }); + a(mfn('ble'), 'ble', "#1"); + a(mfn({}), 'ble', "#2"); + a(i, 1, "Called once"); + }, + "One arg": function (a) { + var i = 0, fn = function (x) { ++i; return x; }, mfn + , y = { toString: function () { return 'foo' } }; + mfn = t.call(fn, { primitive: true }); + a(mfn(y), y, "#1"); + a(mfn('foo'), y, "#2"); + a(i, 1, "Called once"); + }, + "Many args": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn + , y = { toString: function () { return 'foo' } }; + mfn = t.call(fn, { primitive: true }); + a(mfn(y, 'bar', 'zeta'), 'foobarzeta', "#1"); + a(mfn('foo', 'bar', 'zeta'), 'foobarzeta', "#2"); + a(i, 1, "Called once"); + }, + "Clear cache": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn + , y = { toString: function () { return 'foo' } }; + mfn = t.call(fn, { primitive: true }); + a(mfn(y, 'bar', 'zeta'), 'foobarzeta', "#1"); + a(mfn('foo', 'bar', 'zeta'), 'foobarzeta', "#2"); + a(i, 1, "Called once"); + mfn.clear('foo', { toString: function () { return 'bar'; } }, + 'zeta'); + a(mfn(y, 'bar', 'zeta'), 'foobarzeta', "#3"); + a(i, 2, "Called twice"); + } + }, + "GC Mode": { + "Regular": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn + , invoked = false; + mfn = t.call(fn, { gcMode: true, onclear: function (val, args) { + a(val, 15, "onclear: Value"); + a.deep(toArray(args), [3, 5, 7], "onclear: Arguments"); + invoked = true; + } }); + mfn.clear(3, 5, 7); + a(mfn(3, 5, 7), 15, "Initial"); + a(mfn(3, 5, 7), 15, "Cache"); + mfn.clear(3, 5, 7); + mfn(3, 5, 7); + mfn.clear(3, 5, 7); + mfn(3, 5, 7); + mfn.clear(3, 5, 7); + mfn(3, 5, 7); + a(i, 1, "Not cleared"); + mfn.clear(3, 5, 7); + mfn.clear(3, 5, 7); + a(invoked, true, "Cleared"); + mfn(3, 5, 7); + a(i, 2, "Restarted"); + mfn(3, 5, 7); + a(i, 2, "Cached again"); + }, + "Primitive": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn + , invoked = false; + mfn = t.call(fn, { primitive: true, gcMode: true, + onclear: function (val, args) { + a(val, 15, "onclear: Value"); + a.deep(toArray(args), [3, 5, 7], "onclear: Arguments"); + invoked = true; + } }); + mfn.clear(3, 5, 7); + a(mfn(3, 5, 7), 15, "Initial"); + a(mfn(3, 5, 7), 15, "Cache"); + mfn.clear(3, 5, 7); + mfn(3, 5, 7); + mfn.clear(3, 5, 7); + mfn(3, 5, 7); + mfn.clear(3, 5, 7); + mfn(3, 5, 7); + a(i, 1, "Not cleared"); + mfn.clear(3, 5, 7); + mfn.clear(3, 5, 7); + a(invoked, true, "Cleared"); + mfn(3, 5, 7); + a(i, 2, "Restarted"); + mfn(3, 5, 7); + a(i, 2, "Cached again"); + } + } + }; +}; diff --git a/test/profile.js b/test/profile.js new file mode 100644 index 0000000..829d027 --- /dev/null +++ b/test/profile.js @@ -0,0 +1,9 @@ +'use strict'; + +var memoize = require('../lib') + +module.exports = function (t, a) { + a(typeof memoize._profile, 'function', "Set on memoize"); + a(typeof t.statistics, 'object', "Access to statistics"); + a(typeof t.log, 'function', "Access to log function"); +};