Lawnchair: webkitsqlite vs domStorage (v10)

Revision 10 of this benchmark created by zhao on


Description

Comparing the speed of using a local SQLite database versus window.localStorage to store key/value pairs using Lawnchair.

Preparation HTML

<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>

Test runner

Ready to run.

Testing in
TestOps/sec
DOM
var myData;
lawnchair_dom.nuke();
lawnchair_dom.save(someData);
lawnchair_dom.get('my-uuid', function(r) {
  myData = r;
})
lawnchair_dom.remove('my-uuid');
ready
SQL
var myData;
lawnchair_sql.nuke();
lawnchair_sql.save(someData, function() {
  lawnchair_sql.get('my-uuid', function(r) {
    myData = r;
    lawnchair_sql.remove('my-uuid');
  })
});
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.