amplify performance (v2)

Revision 2 of this benchmark created by Eli Perelman on


Preparation HTML

<script>
  /*!
   * Amplify 1.0beta - Core, Store, Request
   * 
   * Copyright 2011 appendTo LLC. (http://appendto.com/team)
   * Dual licensed under the MIT or GPL licenses.
   * http://appendto.com/open-source-licenses
   * 
   * http://amplifyjs.com
   */
  /*!
   * Amplify Core 1.0beta
   * 
   * Copyright 2011 appendTo LLC. (http://appendto.com/team)
   * Dual licensed under the MIT or GPL licenses.
   * http://appendto.com/open-source-licenses
   * 
   * http://amplifyjs.com
   */
  (function(global, undefined) {
  
   var slice = [].slice,
       subscriptions = {};
  
   var amplify = global.amplify = {
    publish: function(topic) {
     var args = slice.call(arguments, 1),
         subscription, length, i = 0,
         ret;
  
     if (!subscriptions[topic]) {
      return true;
     }
  
     for (length = subscriptions[topic].length; i < length; i++) {
      subscription = subscriptions[topic][i];
      ret = subscription.callback.apply(subscription.context, args);
      if (ret === false) {
       break;
      }
     }
     return ret !== false;
    },
  
    subscribe: function(topic, context, callback, priority) {
     if (arguments.length === 3 && typeof callback === "number") {
      priority = callback;
      callback = context;
      context = null;
     }
     if (arguments.length === 2) {
      callback = context;
      context = null;
     }
     priority = priority || 10;
  
     if (!subscriptions[topic]) {
      subscriptions[topic] = [];
     }
  
     var i = subscriptions[topic].length,
         subscriptionInfo = {
       callback: callback,
       context: context,
       priority: priority
         };
  
     while (i--) {
      if (subscriptions[topic][i].priority <= priority) {
       subscriptions[topic].splice(i + 1, 0, subscriptionInfo);
       return callback;
      }
     }
  
     subscriptions[topic].unshift(subscriptionInfo);
  
     return callback;
    },
  
    unsubscribe: function(topic, callback) {
     if (!subscriptions[topic]) {
      return;
     }
  
     var length = subscriptions[topic].length,
         i = 0;
  
     for (; i < length; i++) {
      if (subscriptions[topic][i].callback === callback) {
       subscriptions[topic].splice(i, 1);
       break;
      }
     }
    }
   };
  
  }(this));
  /*!
   * Amplify Store - Persistent Client-Side Storage 1.0beta
   * 
   * Copyright 2011 appendTo LLC. (http://appendto.com/team)
   * Dual licensed under the MIT or GPL licenses.
   * http://appendto.com/open-source-licenses
   * 
   * http://amplifyjs.com
   */
  (function(amplify, undefined) {
  
   var store = amplify.store = function(key, value, options, type) {
    var type = store.type;
    if (options && options.type && options.type in store.types) {
     type = options.type;
    }
    return store.types[type](key, value, options || {});
   };
  
   store.types = {};
   store.type = null;
   store.addType = function(type, storage) {
    if (!store.type) {
     store.type = type;
    }
  
    store.types[type] = storage;
    store[type] = function(key, value, options) {
     options = options || {};
     options.type = type;
     return store(key, value, options);
    };
   }
   store.error = function() {
    return "amplify.store quota exceeded";
   };
  
   function createSimpleStorage(storageType, storage) {
    var values = storage.__amplify__ ? JSON.parse(storage.__amplify__) : {};
  
    function remove(key) {
     if (storage.removeItem) {
      storage.removeItem(key);
     } else {
      delete storage[key];
     }
     delete values[key];
    }
    store.addType(storageType, function(key, value, options) {
     var ret = value,
         now = (new Date()).getTime(),
         storedValue, parsed;
  
     if (!key) {
      ret = {};
      for (key in values) {
       storedValue = storage[key];
       parsed = storedValue ? JSON.parse(storedValue) : {
        expires: -1
       };
       if (parsed.expires && parsed.expires <= now) {
        remove(key);
       } else {
        ret[key.replace(/^__amplify__/, "")] = parsed.data;
       }
      }
      storage.__amplify__ = JSON.stringify(values);
      return ret;
     }
  
     // protect against overwriting built-in properties
     key = "__amplify__" + key;
  
     if (value === undefined) {
      if (values[key]) {
       storedValue = storage[key];
       parsed = storedValue ? JSON.parse(storedValue) : {
        expires: -1
       };
       if (parsed.expires && parsed.expires <= now) {
        remove(key);
       } else {
        return parsed.data;
       }
      }
     } else {
      if (value === null) {
       remove(key);
      } else {
       parsed = JSON.stringify({
        data: value,
        expires: options.expires ? now + options.expires : null
       });
       try {
        storage[key] = parsed;
        values[key] = true;
        // quota exceeded
       } catch (error) {
        // expire old data and try again
        store[storageType]();
        try {
         storage[key] = parsed;
         values[key] = true;
        } catch (error) {
         throw store.error();
        }
       }
      }
     }
  
     storage.__amplify__ = JSON.stringify(values);
     return ret;
    });
   }
  
   // localStorage + sessionStorage
   // IE 8+, Firefox 3.5+, Safari 4+, Chrome 4+, Opera 10.5+, iPhone 2+, Android 2+
   for (var webStorageType in {
    localStorage: 1,
    sessionStorage: 1
   }) {
    // try/catch for file protocol in Firefox
    try {
     if (window[webStorageType].getItem) {
      createSimpleStorage(webStorageType, window[webStorageType]);
     }
    } catch (e) {}
   }
  
   // globalStorage
   // non-standard: Firefox 2+
   // https://developer.mozilla.org/en/dom/storage#globalStorage
   if (window.globalStorage) {
    // try/catch for file protocol in Firefox
    try {
     createSimpleStorage("globalStorage", window.globalStorage[window.location.hostname]);
     // Firefox 2.0 and 3.0 have sessionStorage and globalStorage
     // make sure we defualt to globalStorage
     // but don't default to globalStorage in 3.5+ which also has localStorage
     if (store.type === "sessionStorage") {
      store.type = "globalStorage";
     }
    } catch (e) {}
   }
  
   // userData
   // non-standard: IE 5+
   // http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx
   (function() {
    // append to html instead of body so we can do this from the head
    var div = document.createElement("div"),
        attrKey = "amplify",
        attrs;
    div.style.display = "none";
    document.getElementsByTagName("head")[0].appendChild(div);
    if (div.addBehavior) {
     div.addBehavior("#default#userdata");
     div.load(attrKey);
     attrs = div.getAttribute(attrKey) ? JSON.parse(div.getAttribute(attrKey)) : {};
  
     store.addType("userData", function(key, value, options) {
      var ret = value,
          now = (new Date()).getTime(),
          attr, parsed, prevValue;
  
      if (!key) {
       ret = {};
       for (key in attrs) {
        attr = div.getAttribute(key);
        parsed = attr ? JSON.parse(attr) : {
         expires: -1
        };
        if (parsed.expires && parsed.expires <= now) {
         div.removeAttribute(key);
         delete attrs[key];
        } else {
         ret[key] = parsed.data;
        }
       }
       div.setAttribute(attrKey, JSON.stringify(attrs));
       div.save(attrKey);
       return ret;
      }
  
      // convert invalid characters to dashes
      // http://www.w3.org/TR/REC-xml/#NT-Name
      // simplified to assume the starting character is valid
      // also removed colon as it is invalid in HTML attribute names
      key = key.replace(/[^-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u37f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/g, "-");
  
      if (value === undefined) {
       if (key in attrs) {
        attr = div.getAttribute(key);
        parsed = attr ? JSON.parse(attr) : {
         expires: -1
        };
        if (parsed.expires && parsed.expires <= now) {
         div.removeAttribute(key);
         delete attrs[key];
        } else {
         return parsed.data;
        }
       }
      } else {
       if (value === null) {
        div.removeAttribute(key);
        delete attrs[key];
       } else {
        // we need to get the previous value in case we need to rollback
        prevValue = div.getAttribute(key);
        parsed = JSON.stringify({
         data: value,
         expires: (options.expires ? (now + options.expires) : null)
        });
        div.setAttribute(key, parsed);
        attrs[key] = true;
       }
      }
  
      div.setAttribute(attrKey, JSON.stringify(attrs));
      try {
       div.save(attrKey);
       // quota exceeded
      } catch (error) {
       // roll the value back to the previous value
       if (prevValue === null) {
        div.removeAttribute(key);
        delete attrs[key];
       } else {
        div.setAttribute(key, prevValue);
       }
  
       // expire old data and try again
       store.userData();
       try {
        div.setAttribute(key, parsed);
        attrs[key] = true;
        div.save(attrKey);
       } catch (error) {
        // roll the value back to the previous value
        if (prevValue === null) {
         div.removeAttribute(key);
         delete attrs[key];
        } else {
         div.setAttribute(key, prevValue);
        }
        throw store.error();
       }
      }
      return ret;
     });
    }
   }());
  
   // in-memory storage
   // fallback for all browsers to enable the API even if we can't persist data
   createSimpleStorage("memory", {});
  
  }(this.amplify = this.amplify || {}));
  /*!
   * Amplify Request 1.0beta
   * 
   * Copyright 2011 appendTo LLC. (http://appendto.com/team)
   * Dual licensed under the MIT or GPL licenses.
   * http://appendto.com/open-source-licenses
   * 
   * http://amplifyjs.com
   */
  (function(amplify, undefined) {
  
   function noop() {}
  
   function isFunction(obj) {
    return ({}).toString.call(obj) === "[object Function]";
   }
  
   amplify.request = function(resourceId, data, callback) {
    // default to an empty hash just so we can handle a missing resourceId
    // in one place
    var settings = resourceId || {};
  
    if (typeof settings === "string") {
     if (isFunction(data)) {
      callback = data;
      data = {};
     }
     settings = {
      resourceId: resourceId,
      data: data || {},
      success: callback
     };
    }
  
    var request = {
     abort: noop
    },
        resource = amplify.request.resources[settings.resourceId],
        success = settings.success || noop,
        error = settings.error || noop;
    settings.success = function(data, status) {
     status = status || "success";
     amplify.publish("request.success", settings, data, status);
     amplify.publish("request.complete", settings, data, status);
     success(data, status);
    };
    settings.error = function(data, status) {
     status = status || "error";
     amplify.publish("request.error", settings, data, status);
     amplify.publish("request.complete", settings, data, status);
     error(data, status);
    };
  
    if (!resource) {
     if (!settings.resourceId) {
      throw "amplify.request: no resourceId provided";
     }
     throw "amplify.request: unknown resourceId: " + settings.resourceId;
    }
  
    if (!amplify.publish("request.before", settings)) {
     settings.error(null, "abort");
     return;
    }
  
    amplify.request.resources[settings.resourceId](settings, request);
    return request;
   };
  
   amplify.request.types = {};
   amplify.request.resources = {};
   amplify.request.define = function(resourceId, type, settings) {
    if (typeof type === "string") {
     if (!(type in amplify.request.types)) {
      throw "amplify.request.define: unknown type: " + type;
     }
  
     settings.resourceId = resourceId;
     amplify.request.resources[resourceId] =
     amplify.request.types[type](settings);
    } else {
     // no pre-processor or settings for one-off types (don't invoke)
     amplify.request.resources[resourceId] = type;
    }
   };
  
  }(amplify));
  
  
  
  
  
  (function(amplify, $, undefined) {
  
   var xhrProps = ["status", "statusText", "responseText", "responseXML", "readyState"];
  
   amplify.request.types.ajax = function(defnSettings) {
    defnSettings = $.extend({
     type: "GET"
    }, defnSettings);
  
    return function(settings, request) {
     var regex, xhr, url = defnSettings.url,
         data = settings.data,
         abort = request.abort,
         ajaxSettings = {},
         aborted = false,
         ampXHR = {
       readyState: 0,
       setRequestHeader: function(name, value) {
        return xhr.setRequestHeader(name, value);
       },
       getAllResponseHeaders: function() {
        return xhr.getAllResponseHeaders();
       },
       getResponseHeader: function(key) {
        return xhr.getResponseHeader(key);
       },
       overrideMimeType: function(type) {
        return xhr.overrideMideType(type);
       },
       abort: function() {
        aborted = true;
        try {
         xhr.abort();
         // IE 7 throws an error when trying to abort
        } catch (e) {}
        handleResponse(null, "abort");
       },
       success: function(data, status) {
        settings.success(data, status);
       },
       error: function(data, status) {
        settings.error(data, status);
       }
         };
  
     if (typeof data !== "string") {
      data = $.extend(true, {}, defnSettings.data, data);
      $.each(data, function(key, value) {
       regex = new RegExp("{" + key + "}", "g");
       if (regex.test(url)) {
        url = url.replace(regex, value);
        delete data[key];
       }
      });
     }
  
     $.extend(ajaxSettings, defnSettings, {
      url: url,
      type: defnSettings.type,
      data: data,
      dataType: defnSettings.dataType,
      success: function(data, status) {
       handleResponse(data, status);
      },
      error: function(_xhr, status) {
       handleResponse(null, status);
      },
      beforeSend: function(_xhr, _ajaxSettings) {
       xhr = _xhr;
       ajaxSettings = _ajaxSettings;
       var ret = defnSettings.beforeSend ? defnSettings.beforeSend.call(this, ampXHR, ajaxSettings) : true;
       return ret && amplify.publish("request.before.ajax", defnSettings, settings, ajaxSettings, ampXHR);
      }
     });
     $.ajax(ajaxSettings);
  
     function handleResponse(data, status) {
      $.each(xhrProps, function(i, key) {
       try {
        ampXHR[key] = xhr[key];
       } catch (e) {}
      });
      if (ampXHR.statusText === "OK") {
       ampXHR.statusText = "success";
      }
      if (data === undefined) {
       // TODO: add support for ajax errors with data
       data = null;
      }
      if (aborted) {
       status = "abort";
      }
      if (/timeout|error|abort/.test(status)) {
       ampXHR.error(data, status);
      } else {
       ampXHR.success(data, status);
      }
      // avoid handling a response multiple times
      // this can happen if a request is aborted
      // TODO: figure out if this breaks polling or multi-part responses
      handleResponse = $.noop;
     }
  
     request.abort = function() {
      ampXHR.abort();
      abort.call(this);
     };
    };
   };
  
  
  
   var cache = amplify.request.cache = {
    _key: function(resourceId, url, data) {
     data = url + data;
     var length = data.length,
         i = 0,
         checksum = chunk();
  
     while (i < length) {
      checksum ^= chunk();
     }
  
     function chunk() {
      return data.charCodeAt(i++) << 24 | data.charCodeAt(i++) << 16 | data.charCodeAt(i++) << 8 | data.charCodeAt(i++) << 0;
     }
  
     return "request-" + resourceId + "-" + checksum;
    },
  
    _default: (function() {
     var memoryStore = {};
     return function(resource, settings, ajaxSettings, ampXHR) {
      // data is already converted to a string by the time we get here
      var cacheKey = cache._key(settings.resourceId, ajaxSettings.url, ajaxSettings.data),
          duration = resource.cache;
  
      if (cacheKey in memoryStore) {
       ampXHR.success(memoryStore[cacheKey]);
       return false;
      }
      var success = ampXHR.success;
      ampXHR.success = function(data) {
       memoryStore[cacheKey] = data;
       if (typeof duration === "number") {
        setTimeout(function() {
         delete memoryStore[cacheKey];
        }, duration);
       }
       success.apply(this, arguments);
      };
     };
    }())
   };
  
   if (amplify.store) {
    $.each(amplify.store.types, function(type) {
     cache[type] = function(resource, settings, ajaxSettings, ampXHR) {
      var cacheKey = cache._key(settings.resourceId, ajaxSettings.url, ajaxSettings.data),
          cached = amplify.store[type](cacheKey);
  
      if (cached) {
       ajaxSettings.success(cached);
       return false;
      }
      var success = ampXHR.success;
      ampXHR.success = function(data) {
       amplify.store[type](cacheKey, data, {
        expires: resource.cache.expires
       });
       success.apply(this, arguments);
      };
     };
    });
    cache.persist = cache[amplify.store.type];
   }
  
   amplify.subscribe("request.before.ajax", function(resource) {
    var cacheType = resource.cache;
    if (cacheType) {
     // normalize between objects and strings/booleans/numbers
     cacheType = cacheType.type || cacheType;
     return cache[cacheType in cache ? cacheType : "_default"].apply(this, arguments);
    }
   });
  
  
  
   amplify.request.decoders = {
    // http://labs.omniti.com/labs/jsend
    jsend: function(data, status, ampXHR, success, error) {
     if (data.status === "success") {
      success(data.data);
     } else if (data.status === "fail") {
      error(data.data, "fail");
     } else if (data.status === "error") {
      delete data.status;
      error(data, "error");
     }
    }
   };
  
   amplify.subscribe("request.before.ajax", function(resource, settings, ajaxSettings, ampXHR) {
    var _success = ampXHR.success,
        _error = ampXHR.error,
        decoder = $.isFunction(resource.decoder) ? resource.decoder : resource.decoder in amplify.request.decoders ? amplify.request.decoders[resource.decoder] : amplify.request.decoders._default;
  
    if (!decoder) {
     return;
    }
  
    function success(data, status) {
     _success(data, status);
    }
  
    function error(data, status) {
     _error(data, status);
    }
    ampXHR.success = function(data, status) {
     decoder(data, status, ampXHR, success, error);
    };
    ampXHR.error = function(data, status) {
     decoder(data, status, ampXHR, success, error);
    };
   });
  
  }(amplify, jQuery));
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
Subscribe
amplify.subscribe('mycustomevent', function() {});
ready
Unsubscribe
amplify.unsubscribe('mycustomevent', function() {});
ready

Revisions

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

  • Revision 1: published by Eli Perelman on
  • Revision 2: published by Eli Perelman on