Gilles has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/392381 )
Change subject: Collect RUMSpeedIndex with NavigationTiming
......................................................................
Collect RUMSpeedIndex with NavigationTiming
Bug: T180667
Change-Id: I32234807e29686d29013a9eb9ef3f6ab20278970
---
M extension.json
A modules/RUM-SpeedIndex/LICENSE
A modules/RUM-SpeedIndex/rum-speedindex.js
M modules/ext.navigationTiming.js
4 files changed, 325 insertions(+), 2 deletions(-)
git pull
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/NavigationTiming
refs/changes/81/392381/1
diff --git a/extension.json b/extension.json
index 787ca0a..ce6a716 100644
--- a/extension.json
+++ b/extension.json
@@ -31,6 +31,15 @@
"desktop",
"mobile"
]
+ },
+ "ext.navigationTiming.rumSpeedIndex": {
+ "scripts": [
+ "RUM-SpeedIndex/rum-speedindex.js"
+ ],
+ "targets": [
+ "desktop",
+ "mobile"
+ ]
}
},
"ResourceFileModulePaths": {
@@ -49,7 +58,7 @@
]
},
"EventLoggingSchemas": {
- "NavigationTiming": 17216284,
+ "NavigationTiming": 17446117,
"SaveTiming": 15396492
},
"config": {
diff --git a/modules/RUM-SpeedIndex/LICENSE b/modules/RUM-SpeedIndex/LICENSE
new file mode 100644
index 0000000..f451eb6
--- /dev/null
+++ b/modules/RUM-SpeedIndex/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 WPO Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/modules/RUM-SpeedIndex/rum-speedindex.js
b/modules/RUM-SpeedIndex/rum-speedindex.js
new file mode 100644
index 0000000..e1f380b
--- /dev/null
+++ b/modules/RUM-SpeedIndex/rum-speedindex.js
@@ -0,0 +1,291 @@
+/******************************************************************************
+Copyright (c) 2014, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the <ORGANIZATION> nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+******************************************************************************/
+
+/******************************************************************************
+*******************************************************************************
+ Calculates the Speed Index for a page by:
+ - Collecting a list of visible rectangles for elements that loaded
+ external resources (images, background images, fonts)
+ - Gets the time when the external resource for those elements loaded
+ through Resource Timing
+ - Calculates the likely time that the background painted
+ - Runs the various paint rectangles through the SpeedIndex calculation:
+
https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index
+
+ TODO:
+ - Improve the start render estimate
+ - Handle overlapping rects (though maybe counting the area as multiple paints
+ will work out well)
+ - Detect elements with Custom fonts and the time that the respective font
+ loaded
+ - Better error handling for browsers that don't support resource timing
+*******************************************************************************
+******************************************************************************/
+
+var RUMSpeedIndex = function(win) {
+ win = win || window;
+ var doc = win.document;
+
+ /****************************************************************************
+ Support Routines
+ ****************************************************************************/
+ // Get the rect for the visible portion of the provided DOM element
+ var GetElementViewportRect = function(el) {
+ var intersect = false;
+ if (el.getBoundingClientRect) {
+ var elRect = el.getBoundingClientRect();
+ intersect = {'top': Math.max(elRect.top, 0),
+ 'left': Math.max(elRect.left, 0),
+ 'bottom': Math.min(elRect.bottom, (win.innerHeight ||
doc.documentElement.clientHeight)),
+ 'right': Math.min(elRect.right, (win.innerWidth ||
doc.documentElement.clientWidth))};
+ if (intersect.bottom <= intersect.top ||
+ intersect.right <= intersect.left) {
+ intersect = false;
+ } else {
+ intersect.area = (intersect.bottom - intersect.top) * (intersect.right
- intersect.left);
+ }
+ }
+ return intersect;
+ };
+
+ // Check a given element to see if it is visible
+ var CheckElement = function(el, url) {
+ if (url) {
+ var rect = GetElementViewportRect(el);
+ if (rect) {
+ rects.push({'url': url,
+ 'area': rect.area,
+ 'rect': rect});
+ }
+ }
+ };
+
+ // Get the visible rectangles for elements that we care about
+ var GetRects = function() {
+ // Walk all of the elements in the DOM (try to only do this once)
+ var elements = doc.getElementsByTagName('*');
+ var re = /url\(.*(http.*)\)/ig;
+ for (var i = 0; i < elements.length; i++) {
+ var el = elements[i];
+ var style = win.getComputedStyle(el);
+
+ // check for Images
+ if (el.tagName == 'IMG') {
+ CheckElement(el, el.src);
+ }
+ // Check for background images
+ if (style['background-image']) {
+ re.lastIndex = 0;
+ var matches = re.exec(style['background-image']);
+ if (matches && matches.length > 1)
+ CheckElement(el, matches[1].replace('"', ''));
+ }
+ // recursively walk any iFrames
+ if (el.tagName == 'IFRAME') {
+ try {
+ var rect = GetElementViewportRect(el);
+ if (rect) {
+ var tm = RUMSpeedIndex(el.contentWindow);
+ if (tm) {
+ rects.push({'tm': tm,
+ 'area': rect.area,
+ 'rect': rect});
+ }
+ }
+ } catch(e) {
+ }
+ }
+ }
+ };
+
+ // Get the time at which each external resource loaded
+ var GetRectTimings = function() {
+ var timings = {};
+ var requests = win.performance.getEntriesByType("resource");
+ for (var i = 0; i < requests.length; i++)
+ timings[requests[i].name] = requests[i].responseEnd;
+ for (var j = 0; j < rects.length; j++) {
+ if (!('tm' in rects[j]))
+ rects[j].tm = timings[rects[j].url] !== undefined ?
timings[rects[j].url] : 0;
+ }
+ };
+
+ // Get the first paint time.
+ var GetFirstPaint = function() {
+ // Try the standardized paint timing api
+ try {
+ var entries = performance.getEntriesByType('paint');
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i]['name'] == 'first-paint') {
+ navStart = performance.getEntriesByType("navigation")[0].startTime;
+ firstPaint = entries[i].startTime - navStart;
+ break;
+ }
+ }
+ } catch(e) {
+ }
+ // If the browser supports a first paint event, just use what the browser
reports
+ if (firstPaint === undefined && 'msFirstPaint' in win.performance.timing)
+ firstPaint = win.performance.timing.msFirstPaint - navStart;
+ if (firstPaint === undefined && 'chrome' in win && 'loadTimes' in
win.chrome) {
+ var chromeTimes = win.chrome.loadTimes();
+ if ('firstPaintTime' in chromeTimes && chromeTimes.firstPaintTime > 0) {
+ var startTime = chromeTimes.startLoadTime;
+ if ('requestTime' in chromeTimes)
+ startTime = chromeTimes.requestTime;
+ if (chromeTimes.firstPaintTime >= startTime)
+ firstPaint = (chromeTimes.firstPaintTime - startTime) * 1000.0;
+ }
+ }
+ // For browsers that don't support first-paint or where we get insane
values,
+ // use the time of the last non-async script or css from the head.
+ if (firstPaint === undefined || firstPaint < 0 || firstPaint > 120000) {
+ firstPaint = win.performance.timing.responseStart - navStart;
+ var headURLs = {};
+ var headElements = doc.getElementsByTagName('head')[0].children;
+ for (var i = 0; i < headElements.length; i++) {
+ var el = headElements[i];
+ if (el.tagName == 'SCRIPT' && el.src && !el.async)
+ headURLs[el.src] = true;
+ if (el.tagName == 'LINK' && el.rel == 'stylesheet' && el.href)
+ headURLs[el.href] = true;
+ }
+ var requests = win.performance.getEntriesByType("resource");
+ var doneCritical = false;
+ for (var j = 0; j < requests.length; j++) {
+ if (!doneCritical &&
+ headURLs[requests[j].name] &&
+ (requests[j].initiatorType == 'script' || requests[j].initiatorType
== 'link')) {
+ var requestEnd = requests[j].responseEnd;
+ if (firstPaint === undefined || requestEnd > firstPaint)
+ firstPaint = requestEnd;
+ } else {
+ doneCritical = true;
+ }
+ }
+ }
+ firstPaint = Math.max(firstPaint, 0);
+ };
+
+ // Sort and group all of the paint rects by time and use them to
+ // calculate the visual progress
+ var CalculateVisualProgress = function() {
+ var paints = {'0':0};
+ var total = 0;
+ for (var i = 0; i < rects.length; i++) {
+ var tm = firstPaint;
+ if ('tm' in rects[i] && rects[i].tm > firstPaint)
+ tm = rects[i].tm;
+ if (paints[tm] === undefined)
+ paints[tm] = 0;
+ paints[tm] += rects[i].area;
+ total += rects[i].area;
+ }
+ // Add a paint area for the page background (count 10% of the pixels not
+ // covered by existing paint rects.
+ var pixels = Math.max(doc.documentElement.clientWidth, win.innerWidth ||
0) *
+ Math.max(doc.documentElement.clientHeight, win.innerHeight ||
0);
+ if (pixels > 0 ) {
+ pixels = Math.max(pixels - total, 0) * pageBackgroundWeight;
+ if (paints[firstPaint] === undefined)
+ paints[firstPaint] = 0;
+ paints[firstPaint] += pixels;
+ total += pixels;
+ }
+ // Calculate the visual progress
+ if (total) {
+ for (var time in paints) {
+ if (paints.hasOwnProperty(time)) {
+ progress.push({'tm': time, 'area': paints[time]});
+ }
+ }
+ progress.sort(function(a,b){return a.tm - b.tm;});
+ var accumulated = 0;
+ for (var j = 0; j < progress.length; j++) {
+ accumulated += progress[j].area;
+ progress[j].progress = accumulated / total;
+ }
+ }
+ };
+
+ // Given the visual progress information, Calculate the speed index.
+ var CalculateSpeedIndex = function() {
+ if (progress.length) {
+ SpeedIndex = 0;
+ var lastTime = 0;
+ var lastProgress = 0;
+ for (var i = 0; i < progress.length; i++) {
+ var elapsed = progress[i].tm - lastTime;
+ if (elapsed > 0 && lastProgress < 1)
+ SpeedIndex += (1 - lastProgress) * elapsed;
+ lastTime = progress[i].tm;
+ lastProgress = progress[i].progress;
+ }
+ } else {
+ SpeedIndex = firstPaint;
+ }
+ };
+
+ /****************************************************************************
+ Main flow
+ ****************************************************************************/
+ var rects = [];
+ var progress = [];
+ var firstPaint;
+ var SpeedIndex;
+ var pageBackgroundWeight = 0.1;
+ try {
+ var navStart = win.performance.timing.navigationStart;
+ GetRects();
+ GetRectTimings();
+ GetFirstPaint();
+ CalculateVisualProgress();
+ CalculateSpeedIndex();
+ } catch(e) {
+ }
+ /* Debug output for testing
+ var dbg = '';
+ dbg += "Paint Rects\n";
+ for (var i = 0; i < rects.length; i++)
+ dbg += '(' + rects[i].area + ') ' + rects[i].tm + ' - ' + rects[i].url +
"\n";
+ dbg += "Visual Progress\n";
+ for (var i = 0; i < progress.length; i++)
+ dbg += '(' + progress[i].area + ') ' + progress[i].tm + ' - ' +
progress[i].progress + "\n";
+ dbg += 'First Paint: ' + firstPaint + "\n";
+ dbg += 'Speed Index: ' + SpeedIndex + "\n";
+ console.log(dbg);
+ */
+ return SpeedIndex;
+};
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = RUMSpeedIndex;
+}
+
+// Expose it for Wikimedia purposes
+window.RUMSpeedIndex = RUMSpeedIndex;
\ No newline at end of file
diff --git a/modules/ext.navigationTiming.js b/modules/ext.navigationTiming.js
index 84aa956..e89a14b 100644
--- a/modules/ext.navigationTiming.js
+++ b/modules/ext.navigationTiming.js
@@ -168,6 +168,8 @@
}
}
+ timingData.RSI = Math.round( window.RUMSpeedIndex() );
+
return timingData;
}
@@ -361,7 +363,7 @@
isInSample = inSample();
if ( isInSample ) {
// Preload EventLogging and schema modules
- loadEL = mw.loader.using( [ 'schema.NavigationTiming',
'schema.SaveTiming' ] );
+ loadEL = mw.loader.using( [ 'schema.NavigationTiming',
'schema.SaveTiming', 'ext.navigationTiming.rumSpeedIndex' ] );
}
// Ensure we run after loadEventEnd.
--
To view, visit https://gerrit.wikimedia.org/r/392381
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I32234807e29686d29013a9eb9ef3f6ab20278970
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/NavigationTiming
Gerrit-Branch: master
Gerrit-Owner: Gilles <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits