resourcetiming-compression
Advanced tools
Comparing version 0.2.2 to 0.3.0
@@ -6,3 +6,3 @@ { | ||
"author": "Nic Jansma", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"repository": { | ||
@@ -25,19 +25,20 @@ "type": "git", | ||
"gulp-eslint": "^2.0.0", | ||
"gulp-mocha": "^1.1.1", | ||
"gulp-mocha": "^2.2.0", | ||
"gulp-rename": "^1.2.0", | ||
"gulp-spawn-mocha": "^2.2.2", | ||
"gulp-uglify": "^1.0.1", | ||
"karma": "^0.12.31", | ||
"karma-chrome-launcher": "^0.1.5", | ||
"karma-coverage": "^0.2.6", | ||
"karma": "^0.13.22", | ||
"karma-chrome-launcher": "^1.0.1", | ||
"karma-coverage": "^1.0.0", | ||
"karma-expect": "^1.1.0", | ||
"karma-firefox-launcher": "^0.1.4", | ||
"karma-ie-launcher": "^0.1.5", | ||
"karma-mocha": "^0.1.9", | ||
"karma-opera-launcher": "^0.1.0", | ||
"karma-phantomjs-launcher": "^0.1.4", | ||
"karma-safari-launcher": "^0.1.1", | ||
"karma-tap-reporter": "0.0.3", | ||
"mocha": "^2.0.1" | ||
"karma-firefox-launcher": "^1.0.0", | ||
"karma-ie-launcher": "^1.0.0", | ||
"karma-mocha": "^1.0.1", | ||
"karma-opera-launcher": "^1.0.0", | ||
"karma-phantomjs-launcher": "^1.0.0", | ||
"karma-safari-launcher": "^1.0.0", | ||
"karma-tap-reporter": "0.0.6", | ||
"mocha": "^2.0.1", | ||
"phantomjs-prebuilt": "^2.1.7" | ||
} | ||
} |
# resourcetiming-compression.js | ||
v0.2.2 | ||
v0.3.0 | ||
@@ -35,9 +35,9 @@ [http://nicj.net](http://nicj.net) | ||
__Development:__ [resourcetiming-compression.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/src/resourcetiming-compression.js) - 15kb | ||
__Development:__ [resourcetiming-compression.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/src/resourcetiming-compression.js) - 30kb | ||
__Production:__ [resourcetiming-compression.min.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/dist/resourcetiming-compression.min.js) - 4.5kb (minified / gzipped) | ||
__Production:__ [resourcetiming-compression.min.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/dist/resourcetiming-compression.min.js) - 2.4kb (minified / gzipped) | ||
__Development:__ [resourcetiming-decompression.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/src/resourcetiming-decompression.js) - 6.5kb | ||
__Development:__ [resourcetiming-decompression.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/src/resourcetiming-decompression.js) - 8.8kb | ||
__Production:__ [resourcetiming-decompression.min.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/dist/resourcetiming-decompression.min.js) - 2kb (minified / gzipped) | ||
__Production:__ [resourcetiming-decompression.min.js](https://github.com/nicjansma/resourcetiming-compression.js/raw/master/dist/resourcetiming-decompression.min.js) - 1kb (minified / gzipped) | ||
@@ -99,2 +99,63 @@ resourcetiming-compression.js is also available as the [npm resourcetiming-compression module](https://npmjs.org/package/resourcetiming-compression). You can install | ||
## Resource Dimensions | ||
If available, when [compressing the resources](http://nicj.net/compressing-resourcetiming/) via `compressResourceTiming()`, any resource that has a visible component on the page (such as an `IMG` element) will have its `height` `width` `top` and `left` values captured and included in the compressed data as well. | ||
This information is encoded as a "special value" in the resource's timings array. | ||
For each resource, multiple hits to the same URL are separated by a pipe (`|`) character: | ||
``` | ||
{ | ||
// this resource was loaded twice with timings 70,1z,1c and 90,1,2 | ||
"http://blah.com/js/foo.js": "370,1z,1c|390,1,2" | ||
} | ||
``` | ||
(See the [blog post](http://nicj.net/compressing-resourcetiming/) for a description of how to interpret the values) | ||
If the resource has visible elements on the page, they will be appended to this list of timings with a special prefix of `*0` ([Base36](https://en.wikipedia.org/wiki/Base36) encoded): | ||
``` | ||
{ | ||
// this resource was loaded twice with timings 70,1z,1c and 90,1,2 and had | ||
// dimensions of height = 1, width = 5, top = 10 and left = 11 | ||
"http://blah.com/js/foo.js": "370,1z,1c|390,1,2|*01,5,a,b" | ||
} | ||
``` | ||
## Resource Sizes | ||
If available via [ResourceTiming2](https://www.w3.org/TR/resource-timing/), when [compressing the resources](http://nicj.net/compressing-resourcetiming/) via `compressResourceTiming()`, the resource's `transferSize`, `encodedBodySize` and `decodedBodySize` will be captured and included in the compressed data as well. | ||
This information is encoded as a "special value" in the resource's timings array. They will be appended to the list of timings with a special prefix of `*1`: | ||
The data will be stored in the order of: `[e, t, d]`. | ||
* `e`: `encodedBodySize` is the [Base36](https://en.wikipedia.org/wiki/Base36) decoded value (`encodedBodySize = parseInt(e, 36)`) | ||
* `t`: | ||
* If a Base36 encoded number, `t` is the `transferSize` increase in size over `encodedBodySize` (`transferSize = parseInt(e, 36) + parseInt(t, 36)`) | ||
* If the value of `"_"`, `transferSize` is `0` | ||
* If missing, `transferSize` was either `0` or `undefined` | ||
* `d`: | ||
* If a Base36 encoded number, `d` is the `decodedBodySize` increase in size over `encodedBodySize` (`decodedBodySize = parseInt(e, 36) + parseInt(d, 36)`) | ||
* If `0`, `encodedBodySize` is `0` | ||
* If missing, `encodedBodySize` was either `0` or `undefined` | ||
Taking the following example: | ||
``` | ||
{ | ||
// this resource was loaded twice with timings 70,1z,1c and 90,1,2 and had | ||
// transferSize | ||
"http://blah.com/js/foo.js": "370,1z,1c|390,1,2|*1a,b,c" | ||
} | ||
``` | ||
Results in: | ||
* `encodedBodySize` = `parseInt("a", 36)` = `10` | ||
* `transferSize` = `parseInt("a", 36) + parseInt("b", 36)` = `21` | ||
* `encodedBodySize` = `parseInt("a", 36) + parseInt("c", 36)` = `22` | ||
## Tests | ||
@@ -114,2 +175,9 @@ | ||
* v0.3.0 - 2016-07-11: | ||
* Captures dimensions (px) of resources | ||
* Captures resource sizes (bytes) from ResourceTiming2 | ||
* Breaks certain URLs up slightly so they don't trigger XSS filters | ||
* Limits URLs to 500 characters, and adds the ability to trim other URLs | ||
* Don't go more than 10 IFRAMEs deep (to avoid recursion bugs) | ||
* Fixes browser bugs with incorrect timings | ||
* v0.2.2 - 2016-06-01: Add 'html' initiatorType for root page | ||
@@ -116,0 +184,0 @@ * v0.2.1 - 2016-04-04: Protect against X-O frame access that crashes some browsers |
@@ -51,3 +51,37 @@ // | ||
// Words that will be broken (by ensuring the optimized trie doesn't contain | ||
// the whole string) in URLs, to ensure NoScript doesn't think this is an XSS attack | ||
var DEFAULT_XSS_BREAK_WORDS = [ | ||
/(h)(ref)/gi, | ||
/(s)(rc)/gi, | ||
/(a)(ction)/gi | ||
]; | ||
// Delimiter to use to break a XSS word | ||
var XSS_BREAK_DELIM = "\n"; | ||
// Maximum number of characters in a URL | ||
var DEFAULT_URL_LIMIT = 500; | ||
// Any ResourceTiming data time that starts with this character is not a time, | ||
// but something else (like dimension data) | ||
var SPECIAL_DATA_PREFIX = "*"; | ||
// Dimension data special type | ||
var SPECIAL_DATA_DIMENSION_TYPE = "0"; | ||
// Dimension data special type | ||
var SPECIAL_DATA_SIZE_TYPE = "1"; | ||
/** | ||
* List of URLs (strings or regexs) to trim | ||
*/ | ||
ResourceTimingCompression.trimUrls = []; | ||
/** | ||
* Words to break to avoid XSS filters | ||
*/ | ||
ResourceTimingCompression.xssBreakWords = DEFAULT_XSS_BREAK_WORDS; | ||
/** | ||
* Converts entries to a Trie: | ||
@@ -69,3 +103,3 @@ * http://en.wikipedia.org/wiki/Trie | ||
ResourceTimingCompression.convertToTrie = function(entries) { | ||
var trie = {}, url, i, value, letters, letter, cur, node; | ||
var trie = {}, url, urlFixed, i, value, letters, letter, cur, node; | ||
@@ -77,4 +111,17 @@ for (url in entries) { | ||
urlFixed = url; | ||
// find any strings to break | ||
for (i = 0; i < this.xssBreakWords.length; i++) { | ||
// Add a XSS_BREAK_DELIM character after the first letter. optimizeTrie will | ||
// ensure this sequence doesn't get combined. | ||
urlFixed = urlFixed.replace(this.xssBreakWords[i], "$1" + XSS_BREAK_DELIM + "$2"); | ||
} | ||
if (!entries.hasOwnProperty(url)) { | ||
continue; | ||
} | ||
value = entries[url]; | ||
letters = url.split(""); | ||
letters = urlFixed.split(""); | ||
cur = trie; | ||
@@ -119,3 +166,13 @@ | ||
// capture trie keys first as we'll be modifying it | ||
var keys = []; | ||
for (node in cur) { | ||
if (cur.hasOwnProperty(node)) { | ||
keys.push(node); | ||
} | ||
} | ||
for (var i = 0; i < keys.length; i++) { | ||
node = keys[i]; | ||
if (typeof cur[node] === "object") { | ||
@@ -127,3 +184,13 @@ // optimize children | ||
delete cur[node]; | ||
node = node + ret.name; | ||
if (node === XSS_BREAK_DELIM) { | ||
// If this node is a newline, which can't be in a regular URL, | ||
// it's due to the XSS patch. Remove the placeholder character, | ||
// and make sure this node isn't compressed by incrementing | ||
// num to be greater than one. | ||
node = ret.name; | ||
num++; | ||
} else { | ||
node = node + ret.name; | ||
} | ||
cur[node] = ret.value; | ||
@@ -173,4 +240,4 @@ } | ||
// strip from microseconds to milliseconds only | ||
var timeMs = Math.round(time), | ||
startTimeMs = Math.round(startTime); | ||
var timeMs = Math.round(time ? time : 0), | ||
startTimeMs = Math.round(startTime ? startTime : 0); | ||
@@ -221,34 +288,55 @@ return timeMs === 0 ? 0 : (timeMs - startTimeMs); | ||
* @param {string} offset Offset in timing from root IFRA | ||
* | ||
* @param {number} depth Recursion depth | ||
* @returns {PerformanceEntry[]} Performance entries | ||
*/ | ||
ResourceTimingCompression.findPerformanceEntriesForFrame = function(frame, isTopWindow, offset) { | ||
ResourceTimingCompression.findPerformanceEntriesForFrame = function(frame, isTopWindow, offset, depth) { | ||
var entries = [], i, navEntries, navStart, frameNavStart, frameOffset, navEntry, t, frameLoc; | ||
navStart = this.getNavStartTime(frame); | ||
if (typeof isTopWindow === "undefined") { | ||
isTopWindow = true; | ||
} | ||
// get sub-frames' entries first | ||
if (frame.frames) { | ||
for (i = 0; i < frame.frames.length; i++) { | ||
frameNavStart = this.getNavStartTime(frame.frames[i]); | ||
frameOffset = 0; | ||
if (frameNavStart > navStart) { | ||
frameOffset = offset + (frameNavStart - navStart); | ||
} | ||
if (typeof offset === "undefined") { | ||
offset = 0; | ||
} | ||
entries = entries.concat(this.findPerformanceEntriesForFrame(frame.frames[i], false, frameOffset)); | ||
} | ||
if (typeof depth === "undefined") { | ||
depth = 0; | ||
} | ||
if (depth > 10) { | ||
return entries; | ||
} | ||
try { | ||
// Try to access location.href first to trigger any Cross-Origin | ||
// warnings. There's also a bug in Chrome ~48 that might cause | ||
// the browser to crash if accessing X-O frame.performance. | ||
// https://code.google.com/p/chromium/issues/detail?id=585871 | ||
// This variable is not otherwise used. | ||
frameLoc = frame.location && frame.location.href; | ||
navStart = this.getNavStartTime(frame); | ||
if (!("performance" in frame) || | ||
!frame.performance || | ||
!frame.performance.getEntriesByType) { | ||
// get sub-frames' entries first | ||
if (frame.frames) { | ||
for (i = 0; i < frame.frames.length; i++) { | ||
frameNavStart = this.getNavStartTime(frame.frames[i]); | ||
frameOffset = 0; | ||
if (frameNavStart > navStart) { | ||
frameOffset = offset + (frameNavStart - navStart); | ||
} | ||
entries = entries.concat(this.findPerformanceEntriesForFrame(frame.frames[i], false, frameOffset)); | ||
} | ||
} | ||
try { | ||
// Try to access location.href first to trigger any Cross-Origin | ||
// warnings. There's also a bug in Chrome ~48 that might cause | ||
// the browser to crash if accessing X-O frame.performance. | ||
// https://code.google.com/p/chromium/issues/detail?id=585871 | ||
// This variable is not otherwise used. | ||
frameLoc = frame.location && frame.location.href; | ||
if (!("performance" in frame) || | ||
!frame.performance || | ||
!frame.performance.getEntriesByType) { | ||
return entries; | ||
} | ||
} catch (e) { | ||
// NOP | ||
return entries; | ||
@@ -266,4 +354,4 @@ } | ||
name: frame.location.href, | ||
startTime: 0, | ||
initiatorType: "html", | ||
startTime: 0, | ||
redirectStart: navEntry.redirectStart, | ||
@@ -284,20 +372,30 @@ redirectEnd: navEntry.redirectEnd, | ||
t = frame.performance.timing; | ||
entries.push({ | ||
name: frame.location.href, | ||
initiatorType: "html", | ||
startTime: 0, | ||
redirectStart: t.redirectStart ? (t.redirectStart - t.navigationStart) : 0, | ||
redirectEnd: t.redirectEnd ? (t.redirectEnd - t.navigationStart) : 0, | ||
fetchStart: t.fetchStart ? (t.fetchStart - t.navigationStart) : 0, | ||
domainLookupStart: t.domainLookupStart ? (t.domainLookupStart - t.navigationStart) : 0, | ||
domainLookupEnd: t.domainLookupEnd ? (t.domainLookupEnd - t.navigationStart) : 0, | ||
connectStart: t.connectStart ? (t.connectStart - t.navigationStart) : 0, | ||
secureConnectionStart: t.secureConnectionStart ? | ||
(t.secureConnectionStart - t.navigationStart) : | ||
0, | ||
connectEnd: t.connectEnd ? (t.connectEnd - t.navigationStart) : 0, | ||
requestStart: t.requestStart ? (t.requestStart - t.navigationStart) : 0, | ||
responseStart: t.responseStart ? (t.responseStart - t.navigationStart) : 0, | ||
responseEnd: t.responseEnd ? (t.responseEnd - t.navigationStart) : 0 | ||
}); | ||
// | ||
// Avoid browser bugs: | ||
// 1. navigationStart being 0 in some cases | ||
// 2. responseEnd being ~2x what navigationStart is | ||
// (ensure the end is within 60 minutes of start) | ||
// | ||
if (t.navigationStart !== 0 && | ||
t.responseEnd <= (t.navigationStart + (60 * 60 * 1000))) { | ||
entries.push({ | ||
name: frame.location.href, | ||
startTime: 0, | ||
initiatorType: "html", | ||
redirectStart: t.redirectStart ? (t.redirectStart - t.navigationStart) : 0, | ||
redirectEnd: t.redirectEnd ? (t.redirectEnd - t.navigationStart) : 0, | ||
fetchStart: t.fetchStart ? (t.fetchStart - t.navigationStart) : 0, | ||
domainLookupStart: t.domainLookupStart ? (t.domainLookupStart - t.navigationStart) : 0, | ||
domainLookupEnd: t.domainLookupEnd ? (t.domainLookupEnd - t.navigationStart) : 0, | ||
connectStart: t.connectStart ? (t.connectStart - t.navigationStart) : 0, | ||
secureConnectionStart: t.secureConnectionStart ? | ||
(t.secureConnectionStart - t.navigationStart) : | ||
0, | ||
connectEnd: t.connectEnd ? (t.connectEnd - t.navigationStart) : 0, | ||
requestStart: t.requestStart ? (t.requestStart - t.navigationStart) : 0, | ||
responseStart: t.responseStart ? (t.responseStart - t.navigationStart) : 0, | ||
responseEnd: t.responseEnd ? (t.responseEnd - t.navigationStart) : 0 | ||
}); | ||
} | ||
} | ||
@@ -339,24 +437,276 @@ } | ||
/** | ||
* Converts a number to base-36 | ||
* Converts a number to base-36. | ||
* | ||
* If not a number or a string, or === 0, return "". This is to facilitate | ||
* compression in the timing array, where "blanks" or 0s show as a series | ||
* of trailing ",,,," that can be trimmed. | ||
* | ||
* If a string, return a string. | ||
* | ||
* @param {number} n Number | ||
* @returns {number|string} Base-36 number, or empty string if undefined. | ||
* @returns {string} Base-36 number, empty string, or string | ||
*/ | ||
ResourceTimingCompression.toBase36 = function(n) { | ||
return (typeof n === "number") ? n.toString(36) : ""; | ||
if (typeof n === "number" && n !== 0) { | ||
return n.toString(36); | ||
} else { | ||
return typeof n === "string" ? n : ""; | ||
} | ||
}; | ||
/** | ||
* Gathers performance entries and optimizes the result. | ||
* Finds all remote resources in the selected window that are visible, and returns an object | ||
* keyed by the url with an array of height,width,top,left as the value | ||
* | ||
* @param {Window} win Window to search | ||
* @returns {Object} Object with URLs of visible assets as keys, and Array[height, width, top, left] as value | ||
*/ | ||
ResourceTimingCompression.getVisibleEntries = function(win) { | ||
var els = ["IMG", "IFRAME"], entries = {}, x, y, doc = win.document; | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | ||
x = (win.pageXOffset !== undefined) | ||
? win.pageXOffset | ||
: (doc.documentElement || doc.body.parentNode || doc.body).scrollLeft; | ||
y = (win.pageYOffset !== undefined) | ||
? win.pageYOffset | ||
: (doc.documentElement || doc.body.parentNode || doc.body).scrollTop; | ||
// look at each IMG and IFRAME | ||
els.forEach(function(elname) { | ||
var elements = doc.getElementsByTagName(elname), el, i, rect; | ||
for (i = 0; i < elements.length; i++) { | ||
el = elements[i]; | ||
// look at this element if it has a src attribute, and we haven't already looked at it | ||
if (el && el.src && !entries[el.src]) { | ||
rect = el.getBoundingClientRect(); | ||
// Require both height & width to be non-zero | ||
// IE <= 8 does not report rect.height/rect.width so we need offsetHeight & width | ||
if ((rect.height || el.offsetHeight) | ||
&& (rect.width || el.offsetWidth)) { | ||
entries[el.src] = [ | ||
el.offsetHeight, | ||
el.offsetWidth, | ||
Math.round(rect.top + y), | ||
Math.round(rect.left + x), | ||
]; | ||
} | ||
} | ||
} | ||
}); | ||
return entries; | ||
}; | ||
/** | ||
* Determines if the value is in the specified array | ||
* | ||
* @param {object} val Value | ||
* @param {object[]} ary Array | ||
* | ||
* @returns {boolean} True if the value is in the array | ||
*/ | ||
ResourceTimingCompression.inArray = function(val, ary) { | ||
var i; | ||
if (typeof val === "undefined" || typeof ary === "undefined" || !ary.length) { | ||
return false; | ||
} | ||
for (i = 0; i < ary.length; i++) { | ||
if (ary[i] === val) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
/** | ||
* Gathers a filtered list of performance entries. | ||
* @param {Window} win The Window | ||
* @param {number} from Only get timings from | ||
* @param {number} to Only get timings up to | ||
* @param {string[]} initiatorTypes Array of initiator types | ||
* @returns {ResourceTiming[]} Matching ResourceTiming entries | ||
*/ | ||
ResourceTimingCompression.getFilteredResourceTiming = function(win, from, to, initiatorTypes) { | ||
var entries = this.findPerformanceEntriesForFrame(win, true, 0, 0), | ||
i, e, results = {}, initiatorType, url, data, | ||
navStart = this.getNavStartTime(win); | ||
if (!entries || !entries.length) { | ||
return []; | ||
} | ||
var filteredEntries = []; | ||
for (i = 0; i < entries.length; i++) { | ||
e = entries[i]; | ||
// skip non-resource URLs | ||
if (e.name.indexOf("about:") === 0 || | ||
e.name.indexOf("javascript:") === 0) { | ||
continue; | ||
} | ||
// TODO: skip URLs we don't want to report | ||
// if the user specified a "from" time, skip resources that started before then | ||
if (from && (navStart + e.startTime) < from) { | ||
continue; | ||
} | ||
// if we were given a final timestamp, don't add any resources that started after it | ||
if (to && (navStart + e.startTime) > to) { | ||
// We can also break at this point since the array is time sorted | ||
break; | ||
} | ||
// if given an array of initiatorTypes to include, skip anything else | ||
if (typeof initiatorTypes !== "undefined" && initiatorTypes !== "*" && initiatorTypes.length) { | ||
if (!e.initiatorType || !this.inArray(e.initiatorType, initiatorTypes)) { | ||
continue; | ||
} | ||
} | ||
filteredEntries.push(e); | ||
} | ||
return filteredEntries; | ||
}; | ||
/** | ||
* Gets compressed content and transfer size information, if available | ||
* | ||
* @param {ResourceTiming} resource ResourceTiming bject | ||
* | ||
* @returns {string} Compressed data (or empty string, if not available) | ||
*/ | ||
ResourceTimingCompression.compressSize = function(resource) { | ||
var sTrans, sEnc, sDec, sizes; | ||
// check to see if we can add content sizes | ||
if (resource.encodedBodySize || | ||
resource.decodedBodySize || | ||
resource.transferSize) { | ||
// | ||
// transferSize: how many bytes were over the wire. It can be 0 in the case of X-O, | ||
// or if it was fetched from a cache. | ||
// | ||
// encodedBodySize: the size after applying encoding (e.g. gzipped size). It is 0 if X-O. | ||
// | ||
// decodedBodySize: the size after removing encoding (e.g. the original content size). It is 0 if X-O. | ||
// | ||
// Here are the possible combinations of values: [encodedBodySize, transferSize, decodedBodySize] | ||
// | ||
// Cross-Origin resources w/out Timing-Allow-Origin set: [0, 0, 0] -> [0, 0, 0] -> [empty] | ||
// 204: [0, t, 0] -> [0, t, 0] -> [e, t-e] -> [, t] | ||
// 304: [e, t: t <=> e, d: d>=e] -> [e, t-e, d-e] | ||
// 200 non-gzipped: [e, t: t>=e, d: d=e] -> [e, t-e] | ||
// 200 gzipped: [e, t: t>=e, d: d>=e] -> [e, t-e, d-e] | ||
// retrieved from cache non-gzipped: [e, 0, d: d=e] -> [e] | ||
// retrieved from cache gzipped: [e, 0, d: d>=e] -> [e, _, d-e] | ||
// | ||
sTrans = resource.transferSize; | ||
sEnc = resource.encodedBodySize; | ||
sDec = resource.decodedBodySize; | ||
// convert to an array | ||
sizes = [sEnc, sTrans ? sTrans - sEnc : "_", sDec ? sDec - sEnc : 0]; | ||
// change everything to base36 and remove any trailing ,s | ||
return sizes.map(this.toBase36).join(",").replace(/,+$/, ""); | ||
} else { | ||
return ""; | ||
} | ||
}; | ||
/* Cleans up a URL by removing the query string (if configured), and | ||
* limits the URL to the specified size. | ||
* | ||
* @param {string} url URL to clean | ||
* @param {number} urlLimit Maximum size, in characters, of the URL | ||
* | ||
* @returns {string} Cleaned up URL | ||
*/ | ||
ResourceTimingCompression.cleanupURL = function(url, urlLimit) { | ||
var qsStart; | ||
if (!url || Object.prototype.toString.call(url) === "[object Array]") { | ||
return ""; | ||
} | ||
if (typeof urlLimit !== "undefined" && url && url.length > urlLimit) { | ||
// We need to break this URL up. Try at the query string first. | ||
qsStart = url.indexOf("?"); | ||
if (qsStart !== -1 && qsStart < urlLimit) { | ||
url = url.substr(0, qsStart) + "?..."; | ||
} else { | ||
// No query string, just stop at the limit | ||
url = url.substr(0, urlLimit - 3) + "..."; | ||
} | ||
} | ||
return url; | ||
}; | ||
/** | ||
* Trims the URL according to the specified URL trim patterns, | ||
* then applies a length limit. | ||
* | ||
* @param {string} url URL to trim | ||
* @param {string} urlsToTrim List of URLs (strings or regexs) to trim | ||
* @returns {string} Trimmed URL | ||
*/ | ||
ResourceTimingCompression.trimUrl = function(url, urlsToTrim) { | ||
var i, urlIdx, trim; | ||
if (url && urlsToTrim) { | ||
// trim the payload from any of the specified URLs | ||
for (i = 0; i < urlsToTrim.length; i++) { | ||
trim = urlsToTrim[i]; | ||
if (typeof trim === "string") { | ||
urlIdx = url.indexOf(trim); | ||
if (urlIdx !== -1) { | ||
url = url.substr(0, urlIdx + trim.length) + "..."; | ||
break; | ||
} | ||
} else if (trim instanceof RegExp) { | ||
if (trim.test(url)) { | ||
// replace the URL with the first capture group | ||
url = url.replace(trim, "$1") + "..."; | ||
} | ||
} | ||
} | ||
} | ||
// apply limits | ||
return this.cleanupURL(url, DEFAULT_URL_LIMIT); | ||
}; | ||
/** | ||
* Gathers performance entries and compresses the result. | ||
* @param {Window} [win] The Window | ||
* @param {number} [from] Only get timings from | ||
* @param {number} [to] Only get timings up to | ||
* @returns {object} Optimized performance entries trie | ||
*/ | ||
ResourceTimingCompression.getResourceTiming = function() { | ||
ResourceTimingCompression.getResourceTiming = function(win, from, to) { | ||
/* eslint no-script-url:0 */ | ||
var entries = this.findPerformanceEntriesForFrame(window, true, 0); | ||
if (typeof win === "undefined") { | ||
win = window; | ||
} | ||
var entries = ResourceTimingCompression.getFilteredResourceTiming(win, from, to); | ||
if (!entries || !entries.length) { | ||
return []; | ||
return {}; | ||
} | ||
return this.compressResourceTiming(entries); | ||
return ResourceTimingCompression.compressResourceTiming(win, entries); | ||
}; | ||
@@ -366,17 +716,16 @@ | ||
* Optimizes the specified set of performance entries. | ||
* @param {Window} win The Window | ||
* @param {object} entries Performance entries | ||
* @returns {object} Optimized performance entries trie | ||
*/ | ||
ResourceTimingCompression.compressResourceTiming = function(entries) { | ||
ResourceTimingCompression.compressResourceTiming = function(win, entries) { | ||
/* eslint no-script-url:0 */ | ||
var i, e, results = {}, initiatorType, url, data; | ||
var i, e, results = {}, initiatorType, url, data, visibleEntries = {}; | ||
// gather visible entries on the page | ||
visibleEntries = this.getVisibleEntries(win); | ||
for (i = 0; i < entries.length; i++) { | ||
e = entries[i]; | ||
if (e.name.indexOf("about:") === 0 || | ||
e.name.indexOf("javascript:") === 0) { | ||
continue; | ||
} | ||
// | ||
@@ -414,4 +763,10 @@ // Compress the RT data into a string: | ||
url = e.name; | ||
// add content and transfer size info | ||
var compSize = this.compressSize(e); | ||
if (compSize !== "") { | ||
data += SPECIAL_DATA_PREFIX + SPECIAL_DATA_SIZE_TYPE + compSize; | ||
} | ||
url = this.trimUrl(e.name, this.trimUrls); | ||
// if this entry already exists, add a pipe as a separator | ||
@@ -421,3 +776,17 @@ if (results[url] !== undefined) { | ||
} else { | ||
results[url] = data; | ||
// for the first time we see this URL, add resource dimensions if we have them | ||
if (visibleEntries[url] !== undefined) { | ||
// We use * as an additional separator to indicate it is not a new resource entry | ||
// The following characters will not be URL encoded: | ||
// *!-.()~_ but - and . are special to number representation so we don't use them | ||
// After the *, the type of special data (ResourceTiming = 0) is added | ||
results[url] = | ||
SPECIAL_DATA_PREFIX + | ||
SPECIAL_DATA_DIMENSION_TYPE + | ||
visibleEntries[url].map(this.toBase36).join(",").replace(/,+$/, "") | ||
+ "|" | ||
+ data; | ||
} else { | ||
results[url] = data; | ||
} | ||
} | ||
@@ -424,0 +793,0 @@ } |
@@ -85,3 +85,9 @@ // | ||
for (var i = 0; i < timings.length; i++) { | ||
resources.push(this.decodeCompressedResource(timings[i], nodeKey)); | ||
var resourceData = timings[i]; | ||
if (resourceData.length > 0 && resourceData[0] === "*") { | ||
// dimensions for this resource | ||
continue; | ||
} | ||
resources.push(this.decodeCompressedResource(resourceData, nodeKey)); | ||
} | ||
@@ -194,2 +200,57 @@ } else { | ||
/** | ||
* Decompresses size information back into the specified resource | ||
* | ||
* @param {string} compressed Compressed string | ||
* @param {ResourceTiming} resource ResourceTiming bject | ||
* @returns {ResourceTiming} ResourceTiming object with decompressed sizes | ||
*/ | ||
ResourceTimingDecompression.decompressSize = function(compressed, resource) { | ||
var split, i; | ||
if (typeof resource === "undefined") { | ||
resource = {}; | ||
} | ||
split = compressed.split(","); | ||
for (i = 0; i < split.length; i++) { | ||
if (split[i] === "_") { | ||
// special non-delta value | ||
split[i] = 0; | ||
} else { | ||
// fill in missing numbers | ||
if (split[i] === "") { | ||
split[i] = 0; | ||
} | ||
// convert back from Base36 | ||
split[i] = parseInt(split[i], 36); | ||
if (i > 0) { | ||
// delta against first number | ||
split[i] += split[0]; | ||
} | ||
} | ||
} | ||
// fill in missing | ||
if (split.length === 1) { | ||
// transferSize is a delta from encodedSize | ||
split.push(split[0]); | ||
} | ||
if (split.length === 2) { | ||
// decodedSize is a delta from encodedSize | ||
split.push(split[0]); | ||
} | ||
// re-add attributes to the resource | ||
resource.encodedBodySize = split[0]; | ||
resource.transferSize = split[1]; | ||
resource.decodedBodySize = split[2]; | ||
return resource; | ||
}; | ||
// | ||
@@ -196,0 +257,0 @@ // Export to the appropriate location |
52262
1026
186
22