UNPKG

webkitgtk

Version:

webkitgtk addon with powerful Node.js API

587 lines (553 loc) 14.5 kB
module.exports = function tracker(preload, cstamp, stallXhr, stallTimeout, stallInterval, stallFrame, emit) { const EV = { init: 0, ready: 1, load: 2, idle: 3, busy: 4, unload: 5 }; let lastEvent = EV.init; let lastRunEvent = EV.init; let hasLoaded = false; let hasReady = false; let missedEvent; let preloadList = []; let observer; const intervals = {len: 0, stall: 0, inc: 1}; const timeouts = {len: 0, stall: 0, inc: 1}; const immediates = {len: 0, inc: 1}; const tasks = {len: 0, inc: 1}; const frames = {len: 0, stall: 0, ignore: !stallFrame}; const requests = {len: 0, stall: 0}; const tracks = {len: 0, stall: 0}; const fetchs = {len: 0}; if (preload) disableExternalResources(); else trackExternalResources(); if (!window.setImmediate) window.setImmediate = window.setTimeout; if (!window.clearImmediate) window.clearImmediate = window.clearTimeout; const w = {}; ['setImmediate', 'clearImmediate', 'queueMicrotask', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'XMLHttpRequest', 'WebSocket', 'fetch', 'requestAnimationFrame', 'cancelAnimationFrame'].forEach((meth) => { if (window[meth]) w[meth] = window[meth].bind(window); }); window['hasRunEvent_' + cstamp] = function(event) { if (EV[event] > lastRunEvent) { lastRunEvent = EV[event]; check('lastrun' + event); } }; window['ignore_' + cstamp] = ignoreListener; window['cancel_' + cstamp] = cancelListener; if (document.readyState != 'loading') readyListener(); else document.addEventListener('DOMContentLoaded', readyListener, false); if (document.readyState == 'complete') loadListener(); else window.addEventListener('load', loadListener, false); function disableExternalResources() { function jumpAuto(node) { const tag = node.nodeName.toLowerCase(); const params = { body: ["onload", null], script: ["type", "text/plain"] }[tag]; if (!params) return; const att = params[0]; const val = node.hasAttribute(att) ? node[att] : undefined; if (lastEvent == EV.init) { node[att] = params[1]; preloadList.push({node: node, val: val, att: att}); } } observer = new MutationObserver((mutations) => { let node, list; for (let m = 0; m < mutations.length; m++) { list = mutations[m].addedNodes; if (!list) continue; for (let i = 0; i < list.length; i++) { node = list[i]; if (node.nodeType != 1) continue; jumpAuto(node); } } }); observer.observe(document.documentElement || document, { childList: true, subtree: true }); } function trackExternalResources() { observer = new MutationObserver((mutations) => { let node, list; for (let m = 0; m < mutations.length; m++) { list = mutations[m].addedNodes; if (!list) continue; for (let i = 0; i < list.length; i++) { node = list[i]; if (node.nodeType != 1) continue; trackNode(node); } } }); observer.observe(document.documentElement || document, { childList: true, subtree: true }); } function closeObserver() { if (!observer) return; observer.disconnect(); observer = null; } function ignoreListener(uri) { if (!uri || uri.slice(0, 5) == "data:") return; let req = requests[uri]; if (!req) req = requests[uri] = {count: 0}; req.stall = true; let tra = tracks[uri]; if (!tra) tra = tracks[uri] = {count: 0}; tra.ignore = true; } function cancelListener(uri) { if (!uri || uri.slice(0, 5) == "data:") return; let obj = tracks[uri]; if (!obj) obj = tracks[uri] = {count:0}; if (obj.cancel) return; obj.cancel = true; const count = obj.count; tracks.stall += count; obj.count = 0; if (tracks.len <= tracks.stall) check('tracks'); } function trackNodeDone() { const uri = this.src || this.href; if (!uri) { console.error("trackNodeDone called on a node without uri"); return; } const obj = tracks[uri]; if (!obj) { console.error("trackNodeDone called on untracked uri", uri); return; } if (obj.ignore) return; if (!obj.cancel) { tracks.len--; obj.count--; } if (tracks.len <= tracks.stall) check('tracks'); this.removeEventListener('load', trackNodeDone); this.removeEventListener('error', trackNodeDone); } function trackNode(node) { if (node.nodeName == "LINK") { if (!node.href || node.rel != "import" && node.rel != "stylesheet") return; // do not track when not supported if (node.rel == "import" && !node.import && !window.HTMLImports) return; } else if (node.nodeName == "SCRIPT") { if (!node.src || node.type && node.type != "text/javascript") return; } else { return; } const uri = node.src || node.href; if (!uri || uri.slice(0, 5) == "data:") return; let obj = tracks[uri]; if (!obj) obj = tracks[uri] = {count: 0}; if (obj.cancel) { node.dispatchEvent(new CustomEvent('error', {bubbles: false})); return; } if (obj.ignore) return; tracks.len++; obj.count++; node.addEventListener('load', trackNodeDone); node.addEventListener('error', trackNodeDone); } function loadListener() { if (hasLoaded) return; window.removeEventListener('load', loadListener, false); hasLoaded = true; if (lastEvent == EV.ready) { check('load'); } else if (lastEvent < EV.ready) { missedEvent = EV.load; } } function readyListener() { if (hasReady) return; document.removeEventListener('DOMContentLoaded', readyListener, false); hasReady = true; if (lastEvent != EV.init) return; if (preloadList.length) { closeObserver(); w.setTimeout(function() { preloadList.forEach((obj) => { if (obj.val === undefined) obj.node.removeAttribute(obj.att); else obj.node[obj.att] = obj.val; }); preloadList = []; check("ready"); if (missedEvent == EV.load) { w.setTimeout(check.bind(this, 'load')); } }); } else { check("ready"); if (missedEvent == EV.load) { w.setTimeout(check.bind(this, 'load')); } } } function absolute(url) { return (new URL(url, document.location)).href; } function doneImmediate(id) { let t = id !== null && immediates[id]; if (t) { delete immediates[id]; immediates.len--; if (immediates.len == 0) { check('immediate'); } } else { t = id; } return t; } window.setImmediate = function setImmediate(fn) { immediates.len++; const obj = { fn: fn }; const fnobj = function(obj) { doneImmediate(obj.id); let err; try { obj.fn.apply(null, Array.from(arguments).slice(1)); } catch (e) { err = e; } if (err) throw err; // rethrow }.bind(null, obj); const t = w.setImmediate(fnobj); const id = ++immediates.inc; immediates[id] = t; obj.id = id; return id; }; window.clearImmediate = function(id) { const t = doneImmediate(id); return w.clearImmediate(t); }; window.queueMicrotask = function(fn) { tasks.len++; return w.queueMicrotask(() => { let err; try { fn(); } catch(ex) { err = ex; } tasks.len--; if (tasks.len == 0) { check('task'); } if (err) throw err; }); }; function checkTimeouts() { delete timeouts.to; timeouts.ignore = true; if (lastEvent == EV.load) check('timeout'); } function doneTimeout(id) { let t; const obj = id != null && timeouts[id]; if (obj) { if (obj.stall) timeouts.stall--; delete timeouts[id]; timeouts.len--; if (timeouts.len <= timeouts.stall) { check('timeout'); } t = obj.t; } else { t = id; } return t; } window.setTimeout = function setTimeout(fn, timeout) { let stall = false; timeout = timeout || 0; if (timeout >= stallTimeout || timeouts.ignore && timeout > 0) { stall = true; timeouts.stall++; } timeouts.len++; const obj = { fn: fn }; const fnobj = function(obj) { let err; try { obj.fn.apply(null, Array.from(arguments).slice(1)); } catch (e) { err = e; } doneTimeout(obj.id); if (err) throw err; // rethrow }.bind(null, obj); const t = w.setTimeout(fnobj, timeout); const id = ++timeouts.inc; timeouts[id] = {stall: stall, t: t}; obj.id = id; return id; }; window.clearTimeout = function(id) { const t = doneTimeout(id); return w.clearTimeout(t); }; function checkIntervals() { delete intervals.to; intervals.ignore = true; if (lastEvent == EV.load) check('interval'); } window.setInterval = function(fn, interval) { interval = interval || 0; let stall = false; if (interval >= stallInterval) { stall = true; intervals.stall++; } intervals.len++; const t = w.setInterval(fn, interval); const id = ++intervals.inc; intervals[id] = {stall: stall, t: t}; return id; }; window.clearInterval = function(id) { let t; const obj = id != null && intervals[id]; if (obj) { if (obj.stall) intervals.stall--; delete intervals[id]; intervals.len--; if (intervals.len <= intervals.stall && !intervals.ignore) { check('interval'); } t = obj.t; } else { t = id; } return w.clearInterval(t); }; function doneFrame(id) { if (id && frames[id]) { delete frames[id]; frames.len--; if (frames.len <= frames.stall && !frames.ignore) { check('frame'); } } } if (w.requestAnimationFrame) window.requestAnimationFrame = function(fn) { const id = w.requestAnimationFrame((ts) => { let err; doneFrame(id); try { fn(ts); } catch (e) { err = e; } if (err) throw err; // rethrow }); if (!frames.ignore) { frames.len++; frames[id] = true; } if (!frames.timeout && !frames.ignore) { frames.timeout = w.setTimeout(() => { frames.ignore = true; check('frame'); }, stallFrame); } return id; }; if (w.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { doneFrame(id); return w.cancelAnimationFrame(id); }; if (w.WebSocket) window.WebSocket = function() { const ws = new w.WebSocket(Array.from(arguments)); function checkws() { check('websocket'); } function uncheckws() { this.removeEventListener('message', checkws); this.removeEventListener('close', uncheckws); } ws.addEventListener('message', checkws); ws.addEventListener('close', uncheckws); return ws; }; if (w.fetch) window.fetch = function(url, obj) { requests.len++; const req = { done: false, url: url }; req.timeout = w.setTimeout(() => { req.stalled = true; cleanFetch(req); }, stallXhr); return new Promise((resolve, reject) => { fetchs.len++; w.fetch(url, obj).catch((ex) => { reject(ex); cleanFetch(req); }).then((res) => { resolve(res); cleanFetch(req); }); }); }; function cleanFetch(req) { if (req.timeout) { clearTimeout(req.timeout); delete req.timeout; } if (req.stalled) { requests.stall++; req.done = true; } if (!req.done) { req.done = true; requests.len--; } check('fetch'); w.setTimeout(() => { fetchs.len--; }); } const wopen = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function(method, url) { if (this._private) xhrClean.call(this); this.addEventListener("progress", xhrProgress); this.addEventListener("load", xhrChange); this.addEventListener("error", xhrClean); this.addEventListener("abort", xhrClean); this.addEventListener("timeout", xhrClean); this._private = {url: absolute(url)}; const ret = wopen.apply(this, Array.from(arguments)); return ret; }; const wsend = window.XMLHttpRequest.prototype.send; window.XMLHttpRequest.prototype.send = function() { const priv = this._private; if (!priv) return; requests.len++; try { wsend.apply(this, Array.from(arguments)); } catch (e) { xhrClean.call(this); return; } let req = requests[priv.url]; if (req) { if (req.stall) requests.stall++; } else { req = requests[priv.url] = {}; } req.count = (req.count || 0) + 1; priv.timeout = xhrTimeout(priv.url); }; function xhrTimeout(url) { return w.setTimeout(() => { const req = requests[url]; if (req) { if (!req.stall) requests.stall++; req.count--; check('xhr timeout', url); } }, stallXhr); } function xhrProgress(e) { const priv = this._private; if (!priv) return; if (e.totalSize > 0 && priv.timeout) { // set a new timeout w.clearTimeout(priv.timeout); priv.timeout = xhrTimeout(priv.url); } } function xhrChange() { if (this.readyState != this.DONE) return; xhrClean.call(this); } function xhrClean() { const priv = this._private; if (!priv) return; delete this._private; this.removeEventListener("progress", xhrProgress); this.removeEventListener("load", xhrChange); this.removeEventListener("abort", xhrClean); this.removeEventListener("error", xhrClean); this.removeEventListener("timeout", xhrClean); if (priv.timeout) w.clearTimeout(priv.timeout); const req = requests[priv.url]; if (req) { req.count--; if (req.stall) requests.stall--; } requests.len--; check('xhr clean'); } function check(from, url) { w.queueMicrotask(() => { checkNow(from, url); }); } function checkNow(from, url) { const info = { immediates: immediates.len == 0, tasks: tasks.len == 0, fetchs: fetchs.len == 0, timeouts: timeouts.len <= timeouts.stall, intervals: intervals.len <= intervals.stall || intervals.ignore, frames: frames.len <= frames.stall || frames.ignore, requests: requests.len <= requests.stall, tracks: tracks.len <= tracks.stall, lastEvent: lastEvent, lastRunEvent: lastRunEvent }; if (document.readyState == "complete") { // if loading was stopped (location change or else) the load event // is not emitted but readyState is complete hasLoaded = true; } if (lastEvent <= lastRunEvent) { if (lastEvent == EV.load) { if (info.tracks && info.immediates && info.tasks && info.fetchs && info.timeouts && info.intervals && info.frames && info.requests) { lastEvent += 1; closeObserver(); emit("idle", from, url, info); } } else if (lastEvent == EV.idle) { emit("busy", from, url); } else if (lastEvent == EV.init && hasReady) { lastEvent += 1; emit("ready", from, url, info); } else if (lastEvent == EV.ready && hasLoaded) { lastEvent += 1; emit("load", from, url, info); intervals.to = w.setTimeout(checkIntervals, stallInterval); timeouts.to = w.setTimeout(checkTimeouts, stallTimeout); } else { return; } } } };