﻿/**
\file boomerang.js
boomerang measures various performance characteristics of your user's browsing
experience and beacons it back to your server.

\details
To use this you'll need a web site, lots of users and the ability to do
something with the data you collect.  How you collect the data is up to
you, but we have a few ideas.

Copyright (c) 2010 Yahoo! Inc. All rights reserved.
Code licensed under the BSD License.  See the file LICENSE.txt
for the full license text.
*/

// beaconing section
// the parameter is the window
(function (w) {

    var impl, boomr, k, d = w.document;

    // Short namespace because I don't want to keep typing BOOMERANG
    if (typeof BOOMR === "undefined") {
        BOOMR = {};
    }
    // don't allow this code to be included twice
    if (BOOMR.version) {
        return;
    }

    BOOMR.version = "0.9";


    // impl is a private object not reachable from outside the BOOMR object
    // users can set properties by passing in to the init() method
    impl = {
        // properties
        beacon_url: "",
        // strip out everything except last two parts of hostname.
        // This doesn't work well for domains that end with a country tld,
        // but we allow the developer to override site_domain for that.
        site_domain: w.location.hostname.
				replace(/.*?([^.]+\.[^.]+)\.?$/, '$1').
				toLowerCase(),
        //! User's ip address determined on the server.  Used for the BA cookie
        user_ip: '',

        events: {
            "page_ready": [],
            "page_unload": [],
            "visibility_changed": [],
            "before_beacon": []
        },

        vars: {},

        disabled_plugins: {},

        fireEvent: function (e_name, data) {
            var i, h, e;
            if (!this.events.hasOwnProperty(e_name)) {
                return false;
            }

            e = this.events[e_name];

            for (i = 0; i < e.length; i++) {
                h = e[i];
                h[0].call(h[2], data, h[1]);
            }

            return true;
        },

        addListener: function (el, sType, fn, capture) {
            if (el.addEventListener) {
                el.addEventListener(sType, fn, (capture));
            }
            else if (el.attachEvent) {
                el.attachEvent("on" + sType, fn);
            }
        }
    };


    // We create a boomr object and then copy all its properties to BOOMR so that
    // we don't overwrite anything additional that was added to BOOMR before this
    // was called... for example, a plugin.
    boomr = {
        // Utility functions
        utils: {
            getCookie: function (name) {
                if (!name) {
                    return null;
                }

                name = ' ' + name + '=';

                var i, cookies;
                cookies = ' ' + d.cookie + ';';
                if ((i = cookies.indexOf(name)) >= 0) {
                    i += name.length;
                    cookies = cookies.substring(i, cookies.indexOf(';', i));
                    return cookies;
                }

                return null;
            },

            setCookie: function (name, subcookies, max_age, path, domain, sec) {
                var value = "",
			    k, nameval, c,
			    exp = "";

                if (!name) {
                    return false;
                }

                for (k in subcookies) {
                    if (subcookies.hasOwnProperty(k)) {
                        value += '&' + encodeURIComponent(k)
							+ '=' + encodeURIComponent(subcookies[k]);
                    }
                }
                value = value.replace(/^&/, '');

                if (max_age) {
                    exp = new Date();
                    exp.setTime(exp.getTime() + max_age * 1000);
                    exp = exp.toGMTString();
                }

                nameval = name + '=' + value;
                c = nameval +
				((max_age) ? "; expires=" + exp : "") +
				((path) ? "; path=" + path : "") +
				((typeof domain !== "undefined") ? "; domain="
						+ (domain !== null ? domain : impl.site_domain) : "") +
				((sec) ? "; secure" : "");

                if (nameval.length < 4000) {
                    d.cookie = c;
                    // confirm cookie was set (could be blocked by user's settings, etc.)
                    return (value === this.getCookie(name));
                }

                return false;
            },

            getSubCookies: function (cookie) {
                var cookies_a,
			    i, l, kv,
			    cookies = {};

                if (!cookie) {
                    return null;
                }

                cookies_a = cookie.split('&');

                if (cookies_a.length === 0) {
                    return null;
                }

                for (i = 0, l = cookies_a.length; i < l; i++) {
                    kv = cookies_a[i].split('=');
                    kv.push(""); // just in case there's no value
                    cookies[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
                }

                return cookies;
            },

            removeCookie: function (name) {
                return this.setCookie(name, {}, 0, "/", null);
            },

            pluginConfig: function (o, config, plugin_name, properties) {
                var i, props = 0;

                if (!config || !config[plugin_name]) {
                    return false;
                }

                for (i = 0; i < properties.length; i++) {
                    if (typeof config[plugin_name][properties[i]] !== "undefined") {
                        o[properties[i]] = config[plugin_name][properties[i]];
                        props++;
                    }
                }

                return (props > 0);
            }
        },

        init: function (config) {
            var i, k,
		    properties = ["beacon_url", "site_domain", "user_ip"];

            if (!config) {
                config = {};
            }

            for (i = 0; i < properties.length; i++) {
                if (typeof config[properties[i]] !== "undefined") {
                    impl[properties[i]] = config[properties[i]];
                }
            }

            if (typeof config.log !== "undefined") {
                this.log = config.log;
            }
            if (!this.log) {
                this.log = function (m, l, s) { };
            }

            for (k in this.plugins) {
                // config[pugin].enabled has been set to false
                if (config[k]
				&& typeof config[k].enabled !== "undefined"
				&& config[k].enabled === false
			) {
                    impl.disabled_plugins[k] = 1;
                    continue;
                }
                else if (impl.disabled_plugins[k]) {
                    delete impl.disabled_plugins[k];
                }

                // plugin exists and has an init method
                if (this.plugins.hasOwnProperty(k)
				&& typeof this.plugins[k].init === "function"
			) {
                    this.plugins[k].init(config);
                }
            }

            // The developer can override onload by setting autorun to false
            if (typeof config.autorun === "undefined" || config.autorun !== false) {
                impl.addListener(w, "load",
						function () {
						    impl.fireEvent("page_ready");
						}
					);
            }

            // webkitvisibilitychange is useful to detect if the page loaded through prerender
            impl.addListener(d, "webkitvisibilitychange",
						function () {
						    impl.fireEvent("visibility_changed");
						}
				);
            // This must be the last one to fire
            impl.addListener(w, "unload", function () { w = null; });

            return this;
        },

        // The page dev calls this method when they determine the page is usable.
        // Only call this if autorun is explicitly set to false
        page_ready: function () {
            impl.fireEvent("page_ready");
            return this;
        },

        subscribe: function (e_name, fn, cb_data, cb_scope) {
            var i, h, e;

            if (!impl.events.hasOwnProperty(e_name)) {
                return this;
            }

            e = impl.events[e_name];

            // don't allow a handler to be attached more than once to the same event
            for (i = 0; i < e.length; i++) {
                h = e[i];
                if (h[0] === fn && h[1] === cb_data && h[2] === cb_scope) {
                    return this;
                }
            }
            e.push([fn, cb_data || {}, cb_scope || null]);

            // Attach unload handlers directly to the window.onunload and
            // window.onbeforeunload events. The first of the two to fire will clear
            // fn so that the second doesn't fire. We do this because technically
            // onbeforeunload is the right event to fire, but all browsers don't
            // support it.  This allows us to fall back to onunload when onbeforeunload
            // isn't implemented
            if (e_name === 'page_unload') {
                impl.addListener(w, "unload",
						function () {
						    if (fn) {
						        fn.call(cb_scope, null, cb_data);
						    }
						    fn = cb_scope = cb_data = null;
						}
					);
                impl.addListener(w, "beforeunload",
						function () {
						    if (fn) {
						        fn.call(cb_scope, null, cb_data);
						    }
						    fn = cb_scope = cb_data = null;
						}
					);
            }

            return this;
        },

        addVar: function (name, value) {
            if (typeof name === "string") {
                impl.vars[name] = value;
            }
            else if (typeof name === "object") {
                var o = name, k;
                for (k in o) {
                    if (o.hasOwnProperty(k)) {
                        impl.vars[k] = o[k];
                    }
                }
            }
            return this;
        },

        removeVar: function () {
            var i, params;
            if (!arguments.length) {
                return this;
            }

            if (arguments.length === 1
				&& Object.prototype.toString.apply(arguments[0]) === "[object Array]") {
                params = arguments[0];
            }
            else {
                params = arguments;
            }

            for (i = 0; i < params.length; i++) {
                if (impl.vars.hasOwnProperty(params[i])) {
                    delete impl.vars[params[i]];
                }
            }

            return this;
        },

        sendBeacon: function () {
            var k, url, img, nparams = 0;

            // At this point someone is ready to send the beacon.  We send
            // the beacon only if all plugins have finished doing what they
            // wanted to do
            for (k in this.plugins) {
                if (this.plugins.hasOwnProperty(k)) {
                    if (impl.disabled_plugins[k]) {
                        continue;
                    }
                    if (!this.plugins[k].is_complete()) {
                        return this;
                    }
                }
            }

            // If we reach here, all plugins have completed
            impl.fireEvent("before_beacon", impl.vars);

            // Don't send a beacon if no beacon_url has been set
            if (!impl.beacon_url) {
                return this;
            }

            // use document.URL instead of location.href because of a safari bug
            url = impl.beacon_url + '?v=' + encodeURIComponent(BOOMR.version) +
			'&u=' + encodeURIComponent(d.URL.replace(/#.*/, ''));

            for (k in impl.vars) {
                if (impl.vars.hasOwnProperty(k)) {
                    nparams++;
                    url += "&" + encodeURIComponent(k)
					+ "=" + encodeURIComponent(impl.vars[k]);
                }
            }

            // only send beacon if we actually have something to beacon back
            if (nparams) {
                img = new Image();
                img.src = url;
            }

            return this;
        }

    };

    var make_logger = function (l) {
        return function (m, s) {
            this.log(m, l, "boomerang" + (s ? "." + s : "")); return this;
        };
    };

    boomr.debug = make_logger("debug");
    boomr.info = make_logger("info");
    boomr.warn = make_logger("warn");
    boomr.error = make_logger("error");

    if (w.YAHOO && w.YAHOO.widget && w.YAHOO.widget.Logger) {
        boomr.log = w.YAHOO.log;
    }
    else if (typeof w.Y !== "undefined" && typeof w.Y.log !== "undefined") {
        boomr.log = w.Y.log;
    }
    else if (typeof console !== "undefined" && typeof console.log !== "undefined") {
        boomr.log = function (m, l, s) { console.log(s + ": [" + l + "] ", m); };
    }


    for (k in boomr) {
        if (boomr.hasOwnProperty(k)) {
            BOOMR[k] = boomr[k];
        }
    }

    BOOMR.plugins = BOOMR.plugins || {};

} (window));

// end of boomerang beaconing section
// Now we start built in plugins.


// This is the Round Trip Time plugin.  Abbreviated to RT
// the parameter is the window
(function (w) {

    var d = w.document;

    BOOMR = BOOMR || {};
    BOOMR.plugins = BOOMR.plugins || {};

    // private object
    var impl = {
        complete: false, //! Set when this plugin has completed

        timers: {}, 	//! Custom timers that the developer can use
        // Format for each timer is { start: XXX, end: YYY, delta: YYY-XXX }
        cookie: 'RT', 	//! Name of the cookie that stores the start time and referrer
        cookie_exp: 600, 	//! Cookie expiry in seconds
        strict_referrer: true, //! By default, don't beacon if referrers don't match.
        // If set to false, beacon both referrer values and let
        // the back end decide

        navigationStart: undefined,
        responseStart: undefined,

        // The start method is fired on page unload.  It is called with the scope
        // of the BOOMR.plugins.RT object
        start: function () {
            var t_end, t_start = new Date().getTime();

            // Disable use of RT cookie by setting its name to a falsy value
            if (!this.cookie) {
                return this;
            }

            // We use document.URL instead of location.href because of a bug in safari 4
            // where location.href is URL decoded
            if (!BOOMR.utils.setCookie(this.cookie,
						{ s: t_start, r: d.URL.replace(/#.*/, '') },
						this.cookie_exp,
						"/", null)
		) {
                BOOMR.error("cannot set start cookie", "rt");
                return this;
            }

            t_end = new Date().getTime();
            if (t_end - t_start > 50) {
                // It took > 50ms to set the cookie
                // The user Most likely has cookie prompting turned on so
                // t_start won't be the actual unload time
                // We bail at this point since we can't reliably tell t_done
                BOOMR.utils.removeCookie(this.cookie);

                // at some point we may want to log this info on the server side
                BOOMR.error("took more than 50ms to set cookie... aborting: "
					+ t_start + " -> " + t_end, "rt");
            }

            return this;
        },

        initNavTiming: function () {
            var ti, p;

            if (this.navigationStart) {
                return;
            }

            // Get start time from WebTiming API see:
            // https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/NavigationTiming/Overview.html
            // http://blogs.msdn.com/b/ie/archive/2010/06/28/measuring-web-page-performance.aspx
            // http://blog.chromium.org/2010/07/do-you-know-how-slow-your-web-page-is.html
            p = w.performance || w.msPerformance || w.webkitPerformance || w.mozPerformance;

            if (p && p.timing) {
                ti = p.timing;
            }
            else if (w.chrome && w.chrome.csi) {
                // Older versions of chrome also have a timing API that's sort of documented here:
                // http://ecmanaut.blogspot.com/2010/06/google-bom-feature-ms-since-pageload.html
                // source here:
                // http://src.chromium.org/viewvc/chrome/trunk/src/chrome/renderer/loadtimes_extension_bindings.cc?view=markup
                ti = {
                    navigationStart: w.chrome.csi().startE,
                    responseStart: undefined
                };
                BOOMR.addVar("rt.start", "csi");
            }
            else if (w.gtbExternal) {
                // The Google Toolbar exposes navigation start time similar to old versions of chrome
                // This would work for any browser that has the google toolbar installed
                ti = {
                    navigationStart: w.gtbExternal.startE(),
                    responseStart: undefined
                };
                BOOMR.addVar("rt.start", "gtb");
            }

            if (ti) {
                // Always use navigationStart since it falls back to fetchStart
                // If not set, we leave t_start alone so that timers that depend
                // on it don't get sent back.  Never use requestStart since if
                // the first request fails and the browser retries, it will contain
                // the value for the new request.
                BOOMR.addVar("rt.start", "navigation");
                this.navigationStart = ti.navigationStart || undefined;
                this.responseStart = ti.responseStart || undefined;
            }
            else {
                BOOMR.warn("This browser doesn't support the WebTiming API", "rt");
            }

            return;
        }
    };

    BOOMR.plugins.RT = {
        // Methods

        init: function (config) {
            impl.complete = false;
            impl.timers = {};

            BOOMR.utils.pluginConfig(impl, config, "RT",
					["cookie", "cookie_exp", "strict_referrer"]);

            BOOMR.subscribe("page_ready", this.done, null, this);
            BOOMR.subscribe("page_unload", impl.start, null, impl);

            return this;
        },

        startTimer: function (timer_name, time_value) {
            if (timer_name) {
                if (timer_name === 't_page') {
                    this.endTimer('t_resp', time_value);
                }
                impl.timers[timer_name] = { start: (typeof time_value === "number" ? time_value : new Date().getTime()) };
                impl.complete = false;
            }

            return this;
        },

        endTimer: function (timer_name, time_value) {
            if (timer_name) {
                impl.timers[timer_name] = impl.timers[timer_name] || {};
                if (typeof impl.timers[timer_name].end === "undefined") {
                    impl.timers[timer_name].end =
						(typeof time_value === "number" ? time_value : new Date().getTime());
                }
            }

            return this;
        },

        setTimer: function (timer_name, time_delta) {
            if (timer_name) {
                impl.timers[timer_name] = { delta: time_delta };
            }

            return this;
        },

        // Called when the page has reached a "usable" state.  This may be when the
        // onload event fires, or it could be at some other moment during/after page
        // load when the page is usable by the user
        done: function () {
            var t_start, r, r2,
		    subcookies, basic_timers = { t_done: 1, t_resp: 1, t_page: 1 },
		    ntimers = 0, t_name, timer, t_other = [];

            if (impl.complete) {
                return this;
            }

            impl.initNavTiming();

            if (document.webkitVisibilityState && document.webkitVisibilityState === "prerender") {
                // This means that onload fired through a pre-render.  We'll capture this
                // time, but wait for t_done until after the page has become either visible
                // or hidden (ie, it moved out of the pre-render state)
                // http://code.google.com/chrome/whitepapers/pagevisibility.html
                // http://www.w3.org/TR/2011/WD-page-visibility-20110602/
                // http://code.google.com/chrome/whitepapers/prerender.html

                this.startTimer("t_load", impl.navigationStart);
                this.endTimer("t_load"); 	// this will measure actual onload time for a prerendered page
                this.startTimer("t_prerender", impl.navigationStart);
                this.startTimer("t_postrender"); // time from prerender to visible or hidden

                BOOMR.subscribe("visibility_changed", this.done, null, this);

                return this;
            }

            // If the dev has already called endTimer, then this call will do nothing
            // else, it will stop the page load timer
            this.endTimer("t_done");

            if (impl.responseStart) {
                // Use NavTiming API to figure out resp latency and page time
                this.setTimer("t_resp", impl.responseStart - impl.navigationStart);
                if (impl.timers.t_load) {
                    this.setTimer("t_page", impl.timers.t_load.end - impl.responseStart);
                }
                else {
                    this.setTimer("t_page", new Date().getTime() - impl.responseStart);
                }
            }
            else if (impl.timers.hasOwnProperty('t_page')) {
                // If the dev has already started t_page timer, we can end it now as well
                this.endTimer("t_page");
            }

            // If a prerender timer was started, we can end it now as well
            if (impl.timers.hasOwnProperty('t_postrender')) {
                this.endTimer("t_postrender");
                this.endTimer("t_prerender");
            }

            // A beacon may be fired automatically on page load or if the page dev fires
            // it manually with their own timers.  It may not always contain a referrer
            // (eg: XHR calls).  We set default values for these cases

            r = r2 = d.referrer.replace(/#.*/, '');

            // If impl.cookie is not set, the dev does not want to use cookie time
            if (impl.cookie) {
                subcookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(impl.cookie));
                BOOMR.utils.removeCookie(impl.cookie);

                if (subcookies !== null
				&& typeof subcookies.s !== "undefined"
				&& typeof subcookies.r !== "undefined"
			) {
                    r = subcookies.r;
                    if (!impl.strict_referrer || r === r2) {
                        t_start = parseInt(subcookies.s, 10);
                    }
                }
            }

            if (t_start) {
                BOOMR.addVar("rt.start", "cookie");
            }
            else {
                t_start = impl.navigationStart;
            }

            // make sure old variables don't stick around
            BOOMR.removeVar('t_done', 't_page', 't_resp', 'r', 'r2');

            for (t_name in impl.timers) {
                if (!impl.timers.hasOwnProperty(t_name)) {
                    continue;
                }

                timer = impl.timers[t_name];

                // if delta is a number, then it was set using setTimer
                // if not, then we have to calculate it using start & end
                if (typeof timer.delta !== "number") {
                    if (typeof timer.start !== "number") {
                        timer.start = t_start;
                    }
                    timer.delta = timer.end - timer.start;
                }

                // If the caller did not set a start time, and if there was no start cookie
                // then timer.delta will be NaN, in which case we discard it.
                if (isNaN(timer.delta)) {
                    continue;
                }

                if (basic_timers.hasOwnProperty(t_name)) {
                    BOOMR.addVar(t_name, timer.delta);
                }
                else {
                    t_other.push(t_name + '|' + timer.delta);
                }
                ntimers++;
            }

            // At this point we decide whether the beacon should be sent or not
            if (ntimers) {
                BOOMR.addVar("r", r);

                if (r2 !== r) {
                    BOOMR.addVar("r2", r2);
                }

                if (t_other.length) {
                    BOOMR.addVar("t_other", t_other.join(','));
                }
            }

            impl.timers = {};
            impl.complete = true;

            BOOMR.sendBeacon(); // we call sendBeacon() anyway because some other plugin
            // may have blocked waiting for RT to complete
            return this;
        },

        is_complete: function () { return impl.complete; }

    };

} (window));
// End of RT plugin


/**
\file navtiming.js
Plugin to collect metrics from the W3C Navigation Timing API. For more information about Navigation Timing,
see: http://www.w3.org/TR/navigation-timing/
*/

// w is the window object
(function(w) {

// First make sure BOOMR is actually defined.  It's possible that your plugin is loaded before boomerang, in which case
// you'll need this.
BOOMR = BOOMR || {};
BOOMR.plugins = BOOMR.plugins || {};

// A private object to encapsulate all your implementation details
var impl = {
	complete: false,
	done: function() {
		var p, pn, pt, data;
		p = w.performance || w.msPerformance || w.webkitPerformance || w.mozPerformance;
		if(p && p.timing && p.navigation) {
			BOOMR.info("This user agent supports NavigationTiming.", "nt");
			pn = w.performance.navigation;
			pt = w.performance.timing;
			data = {
				nt_red_cnt: pn.redirectCount,
				nt_nav_type: pn.type,
				nt_nav_st: pt.navigationStart,
				nt_red_st: pt.redirectStart,
				nt_red_end: pt.redirectEnd,
				nt_fet_st: pt.fetchStart,
				nt_dns_st: pt.domainLookupStart,
				nt_dns_end: pt.domainLookupEnd,
				nt_con_st: pt.connectStart,
				nt_con_end: pt.connectEnd,
				nt_req_st: pt.requestStart,
				nt_res_st: pt.responseStart,
				nt_res_end: pt.responseEnd,
				nt_domloading: pt.domLoading,
				nt_domint: pt.domInteractive,
				nt_domcontloaded: pt.domContentLoaded,
				nt_domcomp: pt.domComplete,
				nt_load_st: pt.loadEventStart,
				nt_load_end: pt.loadEventEnd,
				nt_unload_st: pt.unloadEventStart,
				nt_unload_end: pt.unloadEventEnd
			};
			if (pt.secureConnectionStart) {
				// secureConnectionStart is OPTIONAL in the spec
				data.nt_ssl_st = pt.secureConnectionStart;
			}
			BOOMR.addVar(data);
		}
		this.complete = true;
		BOOMR.sendBeacon();
	}
};
	
BOOMR.plugins.NavigationTiming = {
	init: function() {
		BOOMR.subscribe("page_ready", impl.done, null, impl);
		return this;
	},

	// Any other public methods would be defined here

	is_complete: function() {
		return impl.complete;
	}
};

}(window));




// This is the Bandwidth & Latency plugin abbreviated to BW
// the parameter is the window
(function (w) {

    var d = w.document;

    BOOMR = BOOMR || {};
    BOOMR.plugins = BOOMR.plugins || {};

    // We choose image sizes so that we can narrow down on a bandwidth range as
    // soon as possible the sizes chosen correspond to bandwidth values of
    // 14-64kbps, 64-256kbps, 256-1024kbps, 1-2Mbps, 2-8Mbps, 8-30Mbps & 30Mbps+
    // Anything below 14kbps will probably timeout before the test completes
    // Anything over 60Mbps will probably be unreliable since latency will make up
    // the largest part of download time. If you want to extend this further to
    // cover 100Mbps & 1Gbps networks, use image sizes of 19,200,000 & 153,600,000
    // bytes respectively
    // See https://spreadsheets.google.com/ccc?key=0AplxPyCzmQi6dDRBN2JEd190N1hhV1N5cHQtUVdBMUE&hl=en_GB
    // for a spreadsheet with the details
    var images = [
	{ name: "image-0.png", size: 11483, timeout: 1400 },
	{ name: "image-1.png", size: 40658, timeout: 1200 },
	{ name: "image-2.png", size: 164897, timeout: 1300 },
	{ name: "image-3.png", size: 381756, timeout: 1500 },
	{ name: "image-4.png", size: 1234664, timeout: 1200 },
	{ name: "image-5.png", size: 4509613, timeout: 1200 },
	{ name: "image-6.png", size: 9084559, timeout: 1200 }
];

    images.end = images.length;
    images.start = 0;

    // abuse arrays to do the latency test simply because it avoids a bunch of
    // branches in the rest of the code.
    // I'm sorry Douglas
    images.l = { name: "image-l.gif", size: 35, timeout: 1000 };

    // private object
    var impl = {
        // properties
        base_url: 'images/',
        timeout: 15000,
        nruns: 5,
        latency_runs: 10,
        user_ip: '',
        cookie_exp: 7 * 86400,
        cookie: 'BA',

        // state
        results: [],
        latencies: [],
        latency: null,
        runs_left: 0,
        aborted: false,
        complete: false,
        running: false,

        // methods

        // numeric comparator.  Returns negative number if a < b, positive if a > b and 0 if they're equal
        // used to sort an array numerically
        ncmp: function (a, b) { return (a - b); },

        // Calculate the interquartile range of an array of data points
        iqr: function (a) {
            var l = a.length - 1, q1, q3, fw, b = [], i;

            q1 = (a[Math.floor(l * 0.25)] + a[Math.ceil(l * 0.25)]) / 2;
            q3 = (a[Math.floor(l * 0.75)] + a[Math.ceil(l * 0.75)]) / 2;

            fw = (q3 - q1) * 1.5;

            l++;

            for (i = 0; i < l && a[i] < q3 + fw; i++) {
                if (a[i] > q1 - fw) {
                    b.push(a[i]);
                }
            }

            return b;
        },

        calc_latency: function () {
            var i, n,
			sum = 0, sumsq = 0,
			amean, median,
			std_dev, std_err,
			lat_filtered;

            // We first do IQR filtering and use the resulting data set
            // for all calculations
            lat_filtered = this.iqr(this.latencies.sort(this.ncmp));
            n = lat_filtered.length;

            BOOMR.debug(lat_filtered, "bw");

            // First we get the arithmetic mean, standard deviation and standard error
            // We ignore the first since it paid the price of DNS lookup, TCP connect
            // and slow start
            for (i = 1; i < n; i++) {
                sum += lat_filtered[i];
                sumsq += lat_filtered[i] * lat_filtered[i];
            }

            n--; // Since we started the loop with 1 and not 0

            amean = Math.round(sum / n);

            std_dev = Math.sqrt(sumsq / n - sum * sum / (n * n));

            // See http://en.wikipedia.org/wiki/1.96 and http://en.wikipedia.org/wiki/Standard_error_%28statistics%29
            std_err = (1.96 * std_dev / Math.sqrt(n)).toFixed(2);

            std_dev = std_dev.toFixed(2);


            n = lat_filtered.length - 1;

            median = Math.round(
				(lat_filtered[Math.floor(n / 2)] + lat_filtered[Math.ceil(n / 2)]) / 2
			);

            return { mean: amean, median: median, stddev: std_dev, stderr: std_err };
        },

        calc_bw: function () {
            var i, j, n = 0,
			r, bandwidths = [], bandwidths_corrected = [],
			sum = 0, sumsq = 0, sum_corrected = 0, sumsq_corrected = 0,
			amean, std_dev, std_err, median,
			amean_corrected, std_dev_corrected, std_err_corrected, median_corrected,
			nimgs, bw, bw_c;

            for (i = 0; i < this.nruns; i++) {
                if (!this.results[i] || !this.results[i].r) {
                    continue;
                }

                r = this.results[i].r;

                // the next loop we iterate through backwards and only consider the largest
                // 3 images that succeeded that way we don't consider small images that
                // downloaded fast without really saturating the network
                nimgs = 0;
                for (j = r.length - 1; j >= 0 && nimgs < 3; j--) {
                    // if we hit an undefined image time, we skipped everything before this
                    if (typeof r[j] === 'undefined') {
                        break;
                    }
                    if (r[j].t === null) {
                        continue;
                    }

                    n++;
                    nimgs++;

                    // multiply by 1000 since t is in milliseconds and not seconds
                    bw = images[j].size * 1000 / r[j].t;
                    bandwidths.push(bw);

                    bw_c = images[j].size * 1000 / (r[j].t - this.latency.mean);
                    bandwidths_corrected.push(bw_c);
                }
            }

            BOOMR.debug('got ' + n + ' readings', "bw");

            BOOMR.debug('bandwidths: ' + bandwidths, "bw");
            BOOMR.debug('corrected: ' + bandwidths_corrected, "bw");

            // First do IQR filtering since we use the median here
            // and should use the stddev after filtering.
            if (bandwidths.length > 3) {
                bandwidths = this.iqr(bandwidths.sort(this.ncmp));
                bandwidths_corrected = this.iqr(bandwidths_corrected.sort(this.ncmp));
            } else {
                bandwidths = bandwidths.sort(this.ncmp);
                bandwidths_corrected = bandwidths_corrected.sort(this.ncmp);
            }

            BOOMR.debug('after iqr: ' + bandwidths, "bw");
            BOOMR.debug('corrected: ' + bandwidths_corrected, "bw");

            // Now get the mean & median.
            // Also get corrected values that eliminate latency
            n = Math.max(bandwidths.length, bandwidths_corrected.length);
            for (i = 0; i < n; i++) {
                if (i < bandwidths.length) {
                    sum += bandwidths[i];
                    sumsq += Math.pow(bandwidths[i], 2);
                }
                if (i < bandwidths_corrected.length) {
                    sum_corrected += bandwidths_corrected[i];
                    sumsq_corrected += Math.pow(bandwidths_corrected[i], 2);
                }
            }

            n = bandwidths.length;
            amean = Math.round(sum / n);
            std_dev = Math.sqrt(sumsq / n - Math.pow(sum / n, 2));
            std_err = Math.round(1.96 * std_dev / Math.sqrt(n));
            std_dev = Math.round(std_dev);

            n = bandwidths.length - 1;
            median = Math.round(
				(bandwidths[Math.floor(n / 2)] + bandwidths[Math.ceil(n / 2)]) / 2
			);

            n = bandwidths_corrected.length;
            amean_corrected = Math.round(sum_corrected / n);
            std_dev_corrected = Math.sqrt(sumsq_corrected / n - Math.pow(sum_corrected / n, 2));
            std_err_corrected = (1.96 * std_dev_corrected / Math.sqrt(n)).toFixed(2);
            std_dev_corrected = std_dev_corrected.toFixed(2);

            n = bandwidths_corrected.length - 1;
            median_corrected = Math.round(
					(
						bandwidths_corrected[Math.floor(n / 2)]
						+ bandwidths_corrected[Math.ceil(n / 2)]
					) / 2
				);

            BOOMR.debug('amean: ' + amean + ', median: ' + median, "bw");
            BOOMR.debug('corrected amean: ' + amean_corrected + ', '
				+ 'median: ' + median_corrected, "bw");

            return {
                mean: amean,
                stddev: std_dev,
                stderr: std_err,
                median: median,
                mean_corrected: amean_corrected,
                stddev_corrected: std_dev_corrected,
                stderr_corrected: std_err_corrected,
                median_corrected: median_corrected
            };
        },

        defer: function (method) {
            var that = this;
            return setTimeout(function () { method.call(that); that = null; }, 10);
        },

        load_img: function (i, run, callback) {
            var url = this.base_url + images[i].name
			+ '?t=' + (new Date().getTime()) + Math.random(),
		    timer = 0, tstart = 0,
		    img = new Image(),
		    that = this;

            img.onload = function () {
                img.onload = img.onerror = null;
                img = null;
                clearTimeout(timer);
                if (callback) {
                    callback.call(that, i, tstart, run, true);
                }
                that = callback = null;
            };
            img.onerror = function () {
                img.onload = img.onerror = null;
                img = null;
                clearTimeout(timer);
                if (callback) {
                    callback.call(that, i, tstart, run, false);
                }
                that = callback = null;
            };

            // the timeout does not abort download of the current image, it just sets an
            // end of loop flag so we don't attempt download of the next image we still
            // need to wait until onload or onerror fire to be sure that the image
            // download isn't using up bandwidth.  This also saves us if the timeout
            // happens on the first image.  If it didn't, we'd have nothing to measure.
            timer = setTimeout(function () {
                if (callback) {
                    callback.call(that, i, tstart, run, null);
                }
            },
				images[i].timeout
					+ Math.min(400, this.latency ? this.latency.mean : 400)
			);

            tstart = new Date().getTime();
            img.src = url;
        },

        lat_loaded: function (i, tstart, run, success) {
            if (run !== this.latency_runs + 1) {
                return;
            }

            if (success !== null) {
                var lat = new Date().getTime() - tstart;
                this.latencies.push(lat);
            }
            // if we've got all the latency images at this point,
            // so we can calculate latency
            if (this.latency_runs === 0) {
                this.latency = this.calc_latency();
            }

            this.defer(this.iterate);
        },

        img_loaded: function (i, tstart, run, success) {
            if (run !== this.runs_left + 1) {
                return;
            }

            if (this.results[this.nruns - run].r[i]) {	// already called on this image
                return;
            }

            // if timeout, then we set the next image to the end of loop marker
            if (success === null) {
                this.results[this.nruns - run].r[i + 1] = { t: null, state: null, run: run };
                return;
            }

            var result = {
                start: tstart,
                end: new Date().getTime(),
                t: null,
                state: success,
                run: run
            };
            if (success) {
                result.t = result.end - result.start;
            }
            this.results[this.nruns - run].r[i] = result;

            // we terminate if an image timed out because that means the connection is
            // too slow to go to the next image
            if (i >= images.end - 1
			|| typeof this.results[this.nruns - run].r[i + 1] !== 'undefined'
		) {
                BOOMR.debug(this.results[this.nruns - run], "bw");
                // First run is a pilot test to decide what the largest image
                // that we can download is. All following runs only try to
                // download this image
                if (run === this.nruns) {
                    images.start = i;
                }
                this.defer(this.iterate);
            } else {
                this.load_img(i + 1, run, this.img_loaded);
            }
        },

        finish: function () {
            if (!this.latency) {
                this.latency = this.calc_latency();
            }
            var bw = this.calc_bw(),
			o = {
			    bw: bw.median_corrected,
			    bw_err: parseFloat(bw.stderr_corrected, 10),
			    lat: this.latency.mean,
			    lat_err: parseFloat(this.latency.stderr, 10),
			    bw_time: Math.round(new Date().getTime() / 1000)
			};

            BOOMR.addVar(o);

            // If we have an IP address we can make the BA cookie persistent for a while
            // because we'll recalculate it if necessary (when the user's IP changes).
            if (!isNaN(o.bw)) {
                BOOMR.utils.setCookie(this.cookie,
						{
						    ba: Math.round(o.bw),
						    be: o.bw_err,
						    l: o.lat,
						    le: o.lat_err,
						    ip: this.user_ip,
						    t: o.bw_time
						},
						(this.user_ip ? this.cookie_exp : 0),
						"/",
						null
				);
            }

            this.complete = true;
            BOOMR.sendBeacon();
            this.running = false;
        },

        iterate: function () {
            if (this.aborted) {
                return false;
            }

            if (!this.runs_left) {
                this.finish();
            }
            else if (this.latency_runs) {
                this.load_img('l', this.latency_runs--, this.lat_loaded);
            }
            else {
                this.results.push({ r: [] });
                this.load_img(images.start, this.runs_left--, this.img_loaded);
            }
        },

        setVarsFromCookie: function (cookies) {
            var ba = parseInt(cookies.ba, 10),
		    bw_e = parseFloat(cookies.be, 10),
		    lat = parseInt(cookies.l, 10) || 0,
		    lat_e = parseFloat(cookies.le, 10) || 0,
		    c_sn = cookies.ip.replace(/\.\d+$/, '0'), // Note this is IPv4 only
		    t = parseInt(cookies.t, 10),
		    p_sn = this.user_ip.replace(/\.\d+$/, '0'),

            // We use the subnet instead of the IP address because some people
            // on DHCP with the same ISP may get different IPs on the same subnet
            // every time they log in

		    t_now = Math.round((new Date().getTime()) / 1000); // seconds

            // If the subnet changes or the cookie is more than 7 days old,
            // then we recheck the bandwidth, else we just use what's in the cookie
            if (c_sn === p_sn && t >= t_now - this.cookie_exp) {
                this.complete = true;
                BOOMR.addVar({
                    'bw': ba,
                    'lat': lat,
                    'bw_err': bw_e,
                    'lat_err': lat_e
                });

                return true;
            }

            return false;
        }

    };

    BOOMR.plugins.BW = {
        init: function (config) {
            var cookies;

            BOOMR.utils.pluginConfig(impl, config, "BW",
						["base_url", "timeout", "nruns", "cookie", "cookie_exp"]);

            if (config && config.user_ip) {
                impl.user_ip = config.user_ip;
            }

            images.start = 0;
            impl.runs_left = impl.nruns;
            impl.latency_runs = 10;
            impl.results = [];
            impl.latencies = [];
            impl.latency = null;
            impl.complete = false;
            impl.aborted = false;

            BOOMR.removeVar('ba', 'ba_err', 'lat', 'lat_err');

            cookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(impl.cookie));

            if (!cookies || !cookies.ba || !impl.setVarsFromCookie(cookies)) {
                BOOMR.subscribe("page_ready", this.run, null, this);
            }

            return this;
        },

        run: function () {
            if (impl.running || impl.complete) {
                return this;
            }

            if (w.location.protocol === 'https:') {
                // we don't run the test for https because SSL stuff will mess up b/w
                // calculations we could run the test itself over HTTP, but then IE
                // will complain about insecure resources, so the best is to just bail
                // and hope that the user gets the cookie from some other page

                BOOMR.info("HTTPS detected, skipping bandwidth test", "bw");
                impl.complete = true;
                return this;
            }

            impl.running = true;

            setTimeout(this.abort, impl.timeout);

            impl.defer(impl.iterate);

            return this;
        },

        abort: function () {
            impl.aborted = true;
            if (impl.running) {
                impl.finish(); // we don't defer this call because it might be called from
                // onunload and we want the entire chain to complete
                // before we return
            }
            return this;
        },

        is_complete: function () { return impl.complete; }
    };

} (window));
// End of BW plugin



/*jslint white: false, devel: true, onevar: true, browser: true, undef: true, nomen: true, regexp: false, continue: true, plusplus: false, bitwise: false, newcap: true, maxerr: 50, indent: 4 */

