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 src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js">
</script>
<script>
// Lawnchair
/**
* Lawnchair
* =========
* A lightweight JSON document store.
*
*/
var Lawnchair = function(opts) {
this.init(opts);
}
Lawnchair.prototype = {
init: function(opts) {
var adaptors = {
'webkit': window.WebkitSQLiteAdaptor,
'gears': window.GearsSQLiteAdaptor,
'dom': window.DOMStorageAdaptor,
'cookie': window.CookieAdaptor,
'air': window.AIRSQLiteAdaptor,
'userdata': window.UserDataAdaptor,
'air-async': window.AIRSQLiteAsyncAdaptor,
'blackberry': window.BlackBerryPersistentStorageAdaptor
};
this.adaptor = opts.adaptor ? new adaptors[opts.adaptor](opts) : new WebkitSQLiteAdaptor(opts);
// Check for native JSON functions.
if (!JSON || !JSON.stringify) throw "Native JSON functions unavailable - please include http://www.json.org/json2.js or run on a decent browser :P";
},
// Save an object to the store. If a key is present then update. Otherwise create a new record.
save: function(obj, callback) {
this.adaptor.save(obj, callback)
},
// Invokes a callback on an object with the matching key.
get: function(key, callback) {
this.adaptor.get(key, callback)
},
// Returns whether a key exists to a callback.
exists: function(callback) {
this.adaptor.exists(callback)
},
// Returns all rows to a callback.
all: function(callback) {
this.adaptor.all(callback)
},
// Removes a json object from the store.
remove: function(keyOrObj, callback) {
this.adaptor.remove(keyOrObj, callback)
},
// Removes all documents from a store and returns self.
nuke: function(callback) {
this.adaptor.nuke(callback);
return this
},
// Returns a page of results based on offset provided by user and perPage option
paged: function(page, callback) {
this.adaptor.paged(page, callback)
},
/**
* Iterator that accepts two paramters (methods or eval strings):
*
* - conditional test for a record
* - callback to invoke on matches
*
*/
find: function(condition, callback) {
var is = (typeof condition == 'string') ?
function(r) {
return eval(condition)
} : condition;
var cb = this.adaptor.terseToVerboseCallback(callback);
this.each(function(record, index) {
if (is(record)) cb(record, index); // thats hot
});
},
/**
* Classic iterator.
* - Passes the record and the index as the second parameter to the callback.
* - Accepts a string for eval or a method to be invoked for each document in the collection.
*/
each: function(callback) {
var cb = this.adaptor.terseToVerboseCallback(callback);
this.all(function(results) {
var l = results.length;
for (var i = 0; i < l; i++) {
cb(results[i], i);
}
});
}
// --
};
/**
* LawnchairAdaptorHelpers
* =======================
* Useful helpers for creating Lawnchair stores. Used as a mixin.
*
*/
var LawnchairAdaptorHelpers = {
// merging default properties with user defined args
merge: function(defaultOption, userOption) {
return (userOption == undefined || userOption == null) ? defaultOption : userOption;
},
// awesome shorthand callbacks as strings. this is shameless theft from dojo.
terseToVerboseCallback: function(callback) {
return (typeof arguments[0] == 'string') ?
function(r, i) {
eval(callback);
} : callback;
},
// Returns current datetime for timestamps.
now: function() {
return new Date().getTime();
},
// Returns a unique identifier
uuid: function(len, radix) {
// based on Robert Kieffer's randomUUID.js at http://www.broofa.com
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [];
radix = radix || chars.length;
if (len) {
for (var i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (var i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
},
// Serialize a JSON object as a string.
serialize: function(obj) {
var r = '';
r = JSON.stringify(obj);
return r;
},
// Deserialize JSON.
deserialize: function(json) {
return eval('(' + json + ')');
}
};
/**
* DOMStorageAdaptor
* ===================
* DOM Storage implementation for Lawnchair.
*
* - originally authored by Joseph Pecoraro
* - window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser
*
*/
var DOMStorageAdaptor = function(options) {
for (var i in LawnchairAdaptorHelpers) {
this[i] = LawnchairAdaptorHelpers[i];
}
this.init(options);
};
DOMStorageAdaptor.prototype = {
init: function(options) {
var self = this;
this.storage = this.merge(window.localStorage, options.storage);
this.table = this.merge('field', options.table);
if (!window.Storage) {
this.storage = (function() {
// window.top.name ensures top level, and supports around 2Mb
var data = window.top.name ? self.deserialize(window.top.name) : {};
return {
setItem: function(key, value) {
data[key] = value + ""; // force to string
window.top.name = self.serialize(data);
},
removeItem: function(key) {
delete data[key];
window.top.name = self.serialize(data);
},
getItem: function(key) {
return data[key] || null;
},
clear: function() {
data = {};
window.top.name = '';
}
};
})();
};
},
save: function(obj, callback) {
var id = this.table + '::' + (obj.key || this.uuid());
delete obj.key;
this.storage.setItem(id, this.serialize(obj));
if (callback) {
obj.key = id.split('::')[1];
callback(obj);
}
},
get: function(key, callback) {
var obj = this.deserialize(this.storage.getItem(this.table + '::' + key));
var cb = this.terseToVerboseCallback(callback);
if (obj) {
obj.key = key;
if (callback) cb(obj);
} else {
if (callback) cb(null);
}
},
all: function(callback) {
var cb = this.terseToVerboseCallback(callback);
var results = [];
for (var i = 0, l = this.storage.length; i < l; ++i) {
var id = this.storage.key(i);
var tbl = id.split('::')[0]
var key = id.split('::').slice(1).join("::");
if (tbl == this.table) {
var obj = this.deserialize(this.storage.getItem(id));
obj.key = key;
results.push(obj);
}
}
if (cb) cb(results);
},
remove: function(keyOrObj, callback) {
var key = this.table + '::' + (typeof keyOrObj === 'string' ? keyOrObj : keyOrObj.key);
this.storage.removeItem(key);
if (callback) callback();
},
nuke: function(callback) {
var self = this;
this.all(function(r) {
for (var i = 0, l = r.length; i < l; i++) {
self.remove(r[i]);
}
if (callback) callback();
});
}
};
/**
* WebkitSQLiteAdaptor
* ===================
* Sqlite implementation for Lawnchair.
*
*/
var WebkitSQLiteAdaptor = function(options) {
for (var i in LawnchairAdaptorHelpers) {
this[i] = LawnchairAdaptorHelpers[i];
}
this.init(options);
};
WebkitSQLiteAdaptor.prototype = {
init: function(options) {
var that = this;
var merge = that.merge;
var opts = (typeof arguments[0] == 'string') ? {
table: options
} : options;
// default properties
this.name = merge('Lawnchair', opts.name);
this.version = merge('1.0', opts.version);
this.table = merge('field', opts.table);
this.display = merge('shed', opts.display);
this.max = merge(65536, opts.max);
this.db = merge(null, opts.db);
this.perPage = merge(10, opts.perPage);
// default sqlite callbacks
this.onError = function() {};
this.onData = function() {};
if ("onError" in opts) {
this.onError = opts.onError;
}
// error out on shit browsers
if (!window.openDatabase) throw ('Lawnchair, "This browser does not support sqlite storage."');
// instantiate the store
this.db = openDatabase(this.name, this.version, this.display, this.max);
// create a default database and table if one does not exist
this.db.transaction(function(tx) {
tx.executeSql("SELECT COUNT(*) FROM " + that.table, [], function() {}, function(tx, error) {
that.db.transaction(function(tx) {
tx.executeSql("CREATE TABLE " + that.table + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)", [], function() {}, that.onError);
});
});
});
},
save: function(obj, callback) {
var that = this;
var update = function(id, obj, callback) {
that.db.transaction(function(t) {
t.executeSql("UPDATE " + that.table + " SET value=?, timestamp=? WHERE id=?", [that.serialize(obj), that.now(), id], function() {
if (callback != undefined) {
obj.key = id;
that.terseToVerboseCallback(callback)(obj);
}
}, that.onError);
});
};
var insert = function(obj, callback) {
that.db.transaction(function(t) {
var id = (obj.key == undefined) ? that.uuid() : obj.key;
delete(obj.key);
t.executeSql("INSERT INTO " + that.table + " (id, value,timestamp) VALUES (?,?,?)", [id, that.serialize(obj), that.now()], function() {
if (callback != undefined) {
obj.key = id;
that.terseToVerboseCallback(callback)(obj);
}
}, that.onError);
});
};
if (obj.key == undefined) {
insert(obj, callback);
} else {
this.get(obj.key, function(r) {
var isUpdate = (r != null);
if (isUpdate) {
var id = obj.key;
delete(obj.key);
update(id, obj, callback);
} else {
insert(obj, callback);
}
});
}
},
get: function(key, callback) {
var that = this;
this.db.transaction(function(t) {
t.executeSql("SELECT value FROM " + that.table + " WHERE id = ?", [key], function(tx, results) {
if (results.rows.length == 0) {
that.terseToVerboseCallback(callback)(null);
} else {
var o = that.deserialize(results.rows.item(0).value);
o.key = key;
that.terseToVerboseCallback(callback)(o);
}
}, this.onError);
});
},
all: function(callback) {
var cb = this.terseToVerboseCallback(callback);
var that = this;
this.db.transaction(function(t) {
t.executeSql("SELECT * FROM " + that.table, [], function(tx, results) {
if (results.rows.length == 0) {
cb([]);
} else {
var r = [];
for (var i = 0, l = results.rows.length; i < l; i++) {
var raw = results.rows.item(i).value;
var obj = that.deserialize(raw);
obj.key = results.rows.item(i).id;
r.push(obj);
}
cb(r);
}
}, that.onError);
});
},
paged: function(page, callback) {
var cb = this.terseToVerboseCallback(callback);
var that = this;
this.db.transaction(function(t) {
var offset = that.perPage * (page - 1); // a little offset math magic so users don't have to be 0-based
var sql = "SELECT * FROM " + that.table + " ORDER BY timestamp ASC LIMIT ? OFFSET ?";
t.executeSql(sql, [that.perPage, offset], function(tx, results) {
if (results.rows.length == 0) {
cb([]);
} else {
var r = [];
for (var i = 0, l = results.rows.length; i < l; i++) {
var raw = results.rows.item(i).value;
var obj = that.deserialize(raw);
obj.key = results.rows.item(i).id;
r.push(obj);
}
cb(r);
}
}, that.onError);
});
},
remove: function(keyOrObj, callback) {
var that = this;
if (callback) callback = that.terseToVerboseCallback(callback);
this.db.transaction(function(t) {
t.executeSql("DELETE FROM " + that.table + " WHERE id = ?", [(typeof keyOrObj == 'string') ? keyOrObj : keyOrObj.key], callback || that.onData, that.onError);
});
},
nuke: function(callback) {
var that = this;
if (callback) callback = that.terseToVerboseCallback(callback);
this.db.transaction(function(tx) {
tx.executeSql("DELETE FROM " + that.table, [], callback || that.onData, that.onError);
});
}
};
// 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'
});
</script>
Ready to run.
Test | Ops/sec | |
---|---|---|
DOM |
| ready |
SQL |
| ready |
You can edit these tests or add more tests to this page by appending /edit to the URL.