jsPerf.app is an online JavaScript performance benchmark test runner & jsperf.com mirror. It is a complete rewrite in homage to the once excellent jsperf.com now with hopefully a more modern & maintainable codebase.
jsperf.com URLs are mirrored at the same path, e.g:
https://jsperf.com/negative-modulo/2
Can be accessed at:
https://jsperf.app/negative-modulo/2
Comparing the speed of using a local SQLite database versus window.localStorage to store key/value pairs using Lawnchair.
<script>
/**
* Lawnchair!
* ---
* clientside json store
*
*/
var Lawnchair = function (options, callback) {
// ensure Lawnchair was called as a constructor
if (!(this instanceof Lawnchair)) return new Lawnchair(options, callback);
// lawnchair requires json
if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.'
// options are optional; callback is not
if (arguments.length <= 2 && arguments.length > 0) {
callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1];
options = (typeof arguments[0] === 'function') ? {} : arguments[0];
} else {
throw 'Incorrect # of ctor args!'
}
// TODO perhaps allow for pub/sub instead?
if (typeof callback !== 'function') throw 'No callback was provided';
// default configuration
this.record = options.record || 'record' // default for records
this.name = options.name || 'records' // default name for underlying store
// mixin first valid adapter
var adapter
// if the adapter is passed in we try to load that only
if (options.adapter) {
for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) {
if (Lawnchair.adapters[i].adapter === options.adapter) {
adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined;
break;
}
}
// otherwise find the first valid adapter for this env
}
else {
for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) {
adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined
if (adapter) break
}
}
// we have failed
if (!adapter) throw 'No valid adapter.'
// yay! mixin the adapter
for (var j in adapter)
this[j] = adapter[j]
// call init for each mixed in plugin
for (var i = 0, l = Lawnchair.plugins.length; i < l; i++)
Lawnchair.plugins[i].call(this)
// init the adapter
this.init(options, callback)
}
Lawnchair.adapters = []
/**
* queues an adapter for mixin
* ===
* - ensures an adapter conforms to a specific interface
*
*/
Lawnchair.adapter = function (id, obj) {
// add the adapter id to the adapter obj
// ugly here for a cleaner dsl for implementing adapters
obj['adapter'] = id
// methods required to implement a lawnchair adapter
var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ')
, indexOf = this.prototype.indexOf
// mix in the adapter
for (var i in obj) {
if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i
}
// if we made it this far the adapter interface is valid
// insert the new adapter as the preferred adapter
Lawnchair.adapters.splice(0,0,obj)
}
Lawnchair.plugins = []
/**
* generic shallow extension for plugins
* ===
* - if an init method is found it registers it to be called when the lawnchair is inited
* - yes we could use hasOwnProp but nobody here is an asshole
*/
Lawnchair.plugin = function (obj) {
for (var i in obj)
i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i]
}
/**
* helpers
*
*/
Lawnchair.prototype = {
isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' },
/**
* this code exists for ie8... for more background see:
* http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream
*/
indexOf: function(ary, item, i, l) {
if (ary.indexOf) return ary.indexOf(item)
for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i
return -1
},
// awesome shorthand callbacks as strings. this is shameless theft from dojo.
lambda: function (callback) {
return this.fn(this.record, callback)
},
// first stab at named parameters for terse callbacks; dojo: first != best // ;D
fn: function (name, callback) {
return typeof callback == 'string' ? new Function(name, callback) : callback
},
// returns a unique identifier (by way of Backbone.localStorage.js)
// TODO investigate smaller UUIDs to cut on storage cost
uuid: function () {
var S4 = function () {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
},
// a classic iterator
each: function (callback) {
var cb = this.lambda(callback)
// iterate from chain
if (this.__results) {
for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i)
}
// otherwise iterate the entire collection
else {
this.all(function(r) {
for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i)
})
}
return this
}
// --
};
Lawnchair.adapter('webkit-sqlite', (function () {
// private methods
var fail = function (e, i) { console.log('error in sqlite adaptor!', e, i) }
, now = function () { return new Date() } // FIXME need to use better date fn
// not entirely sure if this is needed...
if (!Function.prototype.bind) {
Function.prototype.bind = function( obj ) {
var slice = [].slice
, args = slice.call(arguments, 1)
, self = this
, nop = function () {}
, bound = function () {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)))
}
nop.prototype = self.prototype
bound.prototype = new nop()
return bound
}
}
// public methods
return {
valid: function() { return !!(window.openDatabase) },
init: function (options, callback) {
var that = this
, cb = that.fn(that.name, callback)
, create = "CREATE TABLE IF NOT EXISTS " + this.record + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)"
, win = function(){ return cb.call(that, that); }
// open a connection and create the db if it doesn't exist
this.db = openDatabase(this.name, '1.0.0', this.name, 65536)
this.db.transaction(function (t) {
t.executeSql(create, [], win, fail)
})
},
keys: function (callback) {
var cb = this.lambda(callback)
, that = this
, keys = "SELECT id FROM " + this.record + " ORDER BY timestamp DESC"
this.db.transaction(function(t) {
var win = function (xxx, results) {
if (results.rows.length == 0 ) {
cb.call(that, [])
} else {
var r = [];
for (var i = 0, l = results.rows.length; i < l; i++) {
r.push(results.rows.item(i).id);
}
cb.call(that, r)
}
}
t.executeSql(keys, [], win, fail)
})
return this
},
// you think thats air you're breathing now?
save: function (obj, callback) {
var that = this
, id = obj.key || that.uuid()
, ins = "INSERT INTO " + this.record + " (value, timestamp, id) VALUES (?,?,?)"
, up = "UPDATE " + this.record + " SET value=?, timestamp=? WHERE id=?"
, win = function () { if (callback) { obj.key = id; that.lambda(callback).call(that, obj) }}
, val = [now(), id]
// existential
that.exists(obj.key, function(exists) {
// transactions are like condoms
that.db.transaction(function(t) {
// TODO move timestamp to a plugin
var insert = function (obj) {
val.unshift(JSON.stringify(obj))
t.executeSql(ins, val, win, fail)
}
// TODO move timestamp to a plugin
var update = function (obj) {
delete(obj.key)
val.unshift(JSON.stringify(obj))
t.executeSql(up, val, win, fail)
}
// pretty
exists ? update(obj) : insert(obj)
})
});
return this
},
// FIXME this should be a batch insert / just getting the test to pass...
batch: function (objs, cb) {
var results = []
, done = false
, that = this
var updateProgress = function(obj) {
results.push(obj)
done = results.length === objs.length
}
var checkProgress = setInterval(function() {
if (done) {
if (cb) that.lambda(cb).call(that, results)
clearInterval(checkProgress)
}
}, 200)
for (var i = 0, l = objs.length; i < l; i++)
this.save(objs[i], updateProgress)
return this
},
get: function (keyOrArray, cb) {
var that = this
, sql = ''
// batch selects support
if (this.isArray(keyOrArray)) {
sql = 'SELECT id, value FROM ' + this.record + " WHERE id IN ('" + keyOrArray.join("','") + "')"
} else {
sql = 'SELECT id, value FROM ' + this.record + " WHERE id = '" + keyOrArray + "'"
}
// FIXME
// will always loop the results but cleans it up if not a batch return at the end..
// in other words, this could be faster
var win = function (xxx, results) {
var o = null
, r = []
if (results.rows.length) {
for (var i = 0, l = results.rows.length; i < l; i++) {
o = JSON.parse(results.rows.item(i).value)
o.key = results.rows.item(i).id
r.push(o)
}
}
if (!that.isArray(keyOrArray)) r = r.length ? r[0] : null
if (cb) that.lambda(cb).call(that, r)
}
this.db.transaction(function(t){ t.executeSql(sql, [], win, fail) })
return this
},
exists: function (key, cb) {
var is = "SELECT * FROM " + this.record + " WHERE id = ?"
, that = this
, win = function(xxx, results) { if (cb) that.fn('exists', cb).call(that, (results.rows.length > 0)) }
this.db.transaction(function(t){ t.executeSql(is, [key], win, fail) })
return this
},
all: function (callback) {
var that = this
, all = "SELECT * FROM " + this.record
, r = []
, cb = this.fn(this.name, callback) || undefined
, win = function (xxx, results) {
if (results.rows.length != 0) {
for (var i = 0, l = results.rows.length; i < l; i++) {
var obj = JSON.parse(results.rows.item(i).value)
obj.key = results.rows.item(i).id
r.push(obj)
}
}
if (cb) cb.call(that, r)
}
this.db.transaction(function (t) {
t.executeSql(all, [], win, fail)
})
return this
},
remove: function (keyOrObj, cb) {
var that = this
, key = typeof keyOrObj === 'string' ? keyOrObj : keyOrObj.key
, del = "DELETE FROM " + this.record + " WHERE id = ?"
, win = function () { if (cb) that.lambda(cb).call(that) }
this.db.transaction( function (t) {
t.executeSql(del, [key], win, fail);
});
return this;
},
nuke: function (cb) {
var nuke = "DELETE FROM " + this.record
, that = this
, win = cb ? function() { that.lambda(cb).call(that) } : function(){}
this.db.transaction(function (t) {
t.executeSql(nuke, [], win, fail)
})
return this
}
//////
}})());
Lawnchair.adapter('memory', (function(){
var storage = {}, index = []
return {
valid: function() { return true },
init: function(opts, cb) {
this.fn(this.name, cb).call(this, this)
return this
},
keys: function (callback) {
this.fn('keys', callback).call(this, index)
return this
},
save: function(obj, cb) {
var key = obj.key || this.uuid()
this.exists(key, function(exists) {
if (!exists) {
if (obj.key) delete obj.key
index.push(key)
}
storage[key] = obj
if (cb) {
obj.key = key
this.lambda(cb).call(this, obj)
}
})
return this
},
batch: function (objs, cb) {
var r = []
for (var i = 0, l = objs.length; i < l; i++) {
this.save(objs[i], function(record) {
r.push(record)
})
}
if (cb) this.lambda(cb).call(this, r)
return this
},
get: function (keyOrArray, cb) {
var r;
if (this.isArray(keyOrArray)) {
r = []
for (var i = 0, l = keyOrArray.length; i < l; i++) {
r.push(storage[keyOrArray[i]])
}
} else {
r = storage[keyOrArray]
if (r) r.key = keyOrArray
}
if (cb) this.lambda(cb).call(this, r)
return this
},
exists: function (key, cb) {
this.lambda(cb).call(this, !!(storage[key]))
return this
},
all: function (cb) {
var r = []
for (var i = 0, l = index.length; i < l; i++) {
var obj = storage[index[i]]
obj.key = index[i]
r.push(obj)
}
this.fn(this.name, cb).call(this, r)
return this
},
remove: function (keyOrArray, cb) {
var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray]
for (var i = 0, l = del.length; i < l; i++) {
delete storage[del[i]]
index.splice(this.indexOf(index, del[i]), 1)
}
if (cb) this.lambda(cb).call(this)
return this
},
nuke: function (cb) {
storage = {}
index = []
if (cb) this.lambda(cb).call(this)
return this
}
}
/////
})());
/**
* dom storage adapter
* ===
* - originally authored by Joseph Pecoraro
*
*/
//
// TODO does it make sense to be chainable all over the place?
// chainable: nuke, remove, all, get, save, all
// not chainable: valid, keys
//
Lawnchair.adapter('dom', (function() {
var storage = window.localStorage
// the indexer is an encapsulation of the helpers needed to keep an ordered index of the keys
var indexer = function(name) {
return {
// the key
key: name + '._index_',
// returns the index
all: function() {
var a = storage.getItem(this.key)
if (a) {
a = JSON.parse(a)
}
if (a === null) storage.setItem(this.key, JSON.stringify([])) // lazy init
return JSON.parse(storage.getItem(this.key))
},
// adds a key to the index
add: function (key) {
var a = this.all()
a.push(key)
storage.setItem(this.key, JSON.stringify(a))
},
// deletes a key from the index
del: function (key) {
var a = this.all(), r = []
// FIXME this is crazy inefficient but I'm in a strata meeting and half concentrating
for (var i = 0, l = a.length; i < l; i++) {
if (a[i] != key) r.push(a[i])
}
storage.setItem(this.key, JSON.stringify(r))
},
// returns index for a key
find: function (key) {
var a = this.all()
for (var i = 0, l = a.length; i < l; i++) {
if (key === a[i]) return i
}
return false
}
}
}
// adapter api
return {
// ensure we are in an env with localStorage
valid: function () {
return !!storage && function() {
// in mobile safari if safe browsing is enabled, window.storage
// is defined but setItem calls throw exceptions.
var success = true
var value = Math.random()
try {
storage.setItem(value, value)
} catch (e) {
success = false
}
storage.removeItem(value)
return success
}()
},
init: function (options, callback) {
this.indexer = indexer(this.name)
if (callback) this.fn(this.name, callback).call(this, this)
},
save: function (obj, callback) {
var key = obj.key ? this.name + '.' + obj.key : this.name + '.' + this.uuid()
// if the key is not in the index push it on
if (this.indexer.find(key) === false) this.indexer.add(key)
// now we kil the key and use it in the store colleciton
delete obj.key;
storage.setItem(key, JSON.stringify(obj))
obj.key = key.slice(this.name.length + 1)
if (callback) {
this.lambda(callback).call(this, obj)
}
return this
},
batch: function (ary, callback) {
var saved = []
// not particularily efficient but this is more for sqlite situations
for (var i = 0, l = ary.length; i < l; i++) {
this.save(ary[i], function(r){
saved.push(r)
})
}
if (callback) this.lambda(callback).call(this, saved)
return this
},
// accepts [options], callback
keys: function(callback) {
if (callback) {
var name = this.name
, keys = this.indexer.all().map(function(r){ return r.replace(name + '.', '') })
this.fn('keys', callback).call(this, keys)
}
return this // TODO options for limit/offset, return promise
},
get: function (key, callback) {
if (this.isArray(key)) {
var r = []
for (var i = 0, l = key.length; i < l; i++) {
var k = this.name + '.' + key[i]
var obj = storage.getItem(k)
if (obj) {
obj = JSON.parse(obj)
obj.key = key[i]
r.push(obj)
}
}
if (callback) this.lambda(callback).call(this, r)
} else {
var k = this.name + '.' + key
var obj = storage.getItem(k)
if (obj) {
obj = JSON.parse(obj)
obj.key = key
}
if (callback) this.lambda(callback).call(this, obj)
}
return this
},
exists: function (key, cb) {
var exists = this.indexer.find(this.name+'.'+key) === false ? false : true ;
this.lambda(cb).call(this, exists);
return this;
},
// NOTE adapters cannot set this.__results but plugins do
// this probably should be reviewed
all: function (callback) {
var idx = this.indexer.all()
, r = []
, o
, k
for (var i = 0, l = idx.length; i < l; i++) {
k = idx[i] //v
o = JSON.parse(storage.getItem(k))
o.key = k.replace(this.name + '.', '')
r.push(o)
}
if (callback) this.fn(this.name, callback).call(this, r)
return this
},
remove: function (keyOrObj, callback) {
var key = this.name + '.' + ((keyOrObj.key) ? keyOrObj.key : keyOrObj)
this.indexer.del(key)
storage.removeItem(key)
if (callback) this.lambda(callback).call(this)
return this
},
nuke: function (callback) {
this.all(function(r) {
for (var i = 0, l = r.length; i < l; i++) {
this.remove(r[i]);
}
if (callback) this.lambda(callback).call(this)
})
return this
}
}})());
// data
var someData = {
'key': 'my-uuid',
'name': 'Fugly McFunston',
'rank': 'Junior Leftenant',
'serial-number': 'ABC123'
};
var lawnchair_dom = new Lawnchair({
adaptor: 'dom'
});
var lawnchair_sql = new Lawnchair({
table: 'segdeha',
adaptor: 'webkit-sqlite'
});
var lawnchair_memory = new Lawnchair({
adaptor: 'memory'
});
</script>
Ready to run.
Test | Ops/sec | |
---|---|---|
DOM |
| ready |
SQL |
| ready |
Memory |
| ready |
You can edit these tests or add more tests to this page by appending /edit to the URL.