Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add instrumentation to embed iframe #1534

Merged
merged 1 commit into from
Jan 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions 3p/environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {listenParent} from './messaging';

/**
* Info about the current document/iframe.
* @type {boolean}
*/
let inViewport = true;

/**
* @param {boolean} inV
*/
export function setInViewportForTesting(inV) {
inViewport = inV;
}

let rafId = 0;
let rafQueue = {};

/**
* Add instrumentation to a window and all child iframes.
* @param {!Window} win
*/
export function manageWin(win) {
try {
manageWin_(win);
} catch (e) {
// We use a try block, because the ad integrations often swallow errors.
console./*OK*/error(e.message, e.stack);
}
}

/**
* @param {!Window} win
*/
function manageWin_(win) {
if (win.ampSeen) {
return;
}
win.ampSeen = true;
// Instrument window.
instrumentEntryPoints(win);

// Watch for new iframes.
installObserver(win);
// Existing iframes.
maybeInstrumentsNodes(win, win.document.querySelectorAll('iframe'));
}


/**
* Add instrumentation code to doc.write.
* @param {!Window} parent
* @param {!Window} win
*/
function instrumentDocWrite(parent, win) {
const doc = win.document;
const close = doc.close;
doc.close = function() {
parent.ampManageWin = function(win) {
manageWin(win);
};
doc.write('<script>window.parent.ampManageWin(window)</script>');
// .call does not work in Safari with document.write.
doc._close = close;
return doc._close();
};
}

/**
* Add instrumentation code to iframe's srcdoc.
* @param {!Window} parent
* @param {!Element} iframe
*/
function instrumentSrcdoc(parent, iframe) {
let srcdoc = iframe.getAttribute('srcdoc');
parent.ampManageWin = function(win) {
manageWin(win);
};
srcdoc += '<script>window.parent.ampManageWin(window)</script>';
iframe.setAttribute('srcdoc', srcdoc);
}

/**
* Instrument added nodes if they are instrumentable iframes.
* @param {!Window} win
* @param {!Array<!Node>} addedNodes
*/
function maybeInstrumentsNodes(win, addedNodes) {
for (let n = 0; n < addedNodes.length; n++) {
const node = addedNodes[n];
try {
if (node.tagName != 'IFRAME') {
continue;
}
const src = node.getAttribute('src');
const srcdoc = node.getAttribute('srcdoc');
if (src == null || /^(about:|javascript:)/i.test(src.trim()) ||
srcdoc) {
if (node.contentWindow) {
instrumentIframeWindow(node, win, node.contentWindow);
node.addEventListener('load', () => {
try {
instrumentIframeWindow(node, win, node.contentWindow);
} catch (e) {
console./*OK*/error(e.message, e.stack);
}
});
} else if (srcdoc) {
instrumentSrcdoc(parent, node);
}
}
} catch (e) {
console./*OK*/error(e.message, e.stack);
}
}
}

/**
* Installs a mutation observer in a window to look for iframes.
* @param {!Element} node
* @param {!Window} parent
* @param {!Window} win
*/
function instrumentIframeWindow(node, parent, win) {
if (win.ampSeen) {
return;
}
const doc = win.document;
instrumentDocWrite(parent, win);
if (doc.body && doc.body.childNodes.length) {
manageWin(win);
}
}

/**
* Installs a mutation observer in a window to look for iframes.
* @param {!Window} win
*/
function installObserver(win) {
if (!window.MutationObserver) {
return;
}
const observer = new MutationObserver(function(mutations) {
for (let i = 0; i < mutations.length; i++) {
maybeInstrumentsNodes(win, mutations[i].addedNodes);
}
});
observer.observe(win.document.documentElement, {
subtree: true,
childList: true,
});
}

/**
* Replace timers with variants that can be throttled.
* @param {!Window} win
*/
function instrumentEntryPoints(win) {
// Change setTimeout to respect a minimum timeout.
const setTimeout = win.setTimeout;
win.setTimeout = function(fn, time) {
time = minTime(time);
return setTimeout(fn, time);
};
// Implement setInterval in terms of setTimeout to make
// it respect the same rules
const intervals = {};
let intervalId = 0;
win.setInterval = function(fn, time) {
const id = intervalId++;
function next() {
intervals[id] = win.setTimeout(function() {
next();
return fn.apply(this, arguments);
}, time);
}
next();
return id;
};
win.clearInterval = function(id) {
win.clearTimeout(intervals[id]);
delete intervals[id];
};
// Throttle requestAnimationFrame.
const requestAnimationFrame = win.requestAnimationFrame ||
win.webkitRequestAnimationFrame;
win.requestAnimationFrame = function(cb) {
if (!inViewport) {
// If the doc is not visible, queue up the frames until we become
// visible again.
const id = rafId++;
rafQueue[id] = [win, cb];
// Only queue 20 frame requests to avoid mem leaks.
delete rafQueue[id - 20];
return id;
}
return requestAnimationFrame.call(this, cb);
};
const cancelAnimationFrame = win.cancelAnimationFrame;
win.cancelAnimationFrame = function(id) {
cancelAnimationFrame.call(this, id);
delete rafQueue[id];
};
if (win.webkitRequestAnimationFrame) {
win.webkitRequestAnimationFrame = win.requestAnimationFrame;
win.webkitCancelAnimationFrame = win.webkitCancelRequestAnimationFrame =
win.cancelAnimationFrame;
}
}

/**
* Run when we just became visible again. Runs all the queued up rafs.
* @visibleForTesting
*/
export function becomeVisible() {
for (const id in rafQueue) {
if (rafQueue.hasOwnProperty(id)) {
const f = rafQueue[id];
f[0].requestAnimationFrame(f[1]);
}
}
rafQueue = {};
}

/**
* Calculates the minimum time that a timeout should have right now.
* @param {number} time
* @return {number}
*/
function minTime(time) {
if (!inViewport) {
time += 1000;
}
// Eventually this should throttle like this:
// - for timeouts in the order of a frame use requestAnimationFrame
// instead.
// - only allow about 2-4 short timeouts (< 16ms) in a 16ms time frame.
// Throttle further timeouts to requestAnimationFrame.
return time;
}

listenParent('embed-state', function(data) {
inViewport = data.inViewport;
if (inViewport) {
becomeVisible();
}
});
36 changes: 11 additions & 25 deletions 3p/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {adsense} from '../ads/adsense';
import {adtech} from '../ads/adtech';
import {doubleclick} from '../ads/doubleclick';
import {facebook} from './facebook';
import {manageWin} from './environment';
import {nonSensitiveDataPostMessage, listenParent} from './messaging';
import {twitter} from './twitter';
import {register, run} from '../src/3p';
import {parseUrl} from '../src/url';
Expand Down Expand Up @@ -127,7 +129,13 @@ window.draw3p = function(opt_configCallback) {
window.context.reportRenderedEntityIdentifier =
reportRenderedEntityIdentifier;
delete data._context;
// Run this only in canary and local dev for the time being.
if (location.pathname.indexOf('-canary') ||
location.pathname.indexOf('current')) {
manageWin(window);
}
draw3p(window, data, opt_configCallback);
nonSensitiveDataPostMessage('render-start');
};

function triggerNoContentAvailable() {
Expand All @@ -148,17 +156,6 @@ function triggerResizeRequest(width, height) {
});
}

function nonSensitiveDataPostMessage(type, opt_object) {
if (window.parent == window) {
return; // Nothing to do.
}
const object = opt_object || {};
object.type = type;
object.sentinel = 'amp-3p';
window.parent./*OK*/postMessage(object,
window.context.location.origin);
}

/**
* Registers a callback for intersections of this iframe with the current
* viewport.
Expand All @@ -170,22 +167,11 @@ function nonSensitiveDataPostMessage(type, opt_object) {
* observes for intersection messages.
*/
function observeIntersection(observerCallback) {
function listener(event) {
if (event.source != window.parent ||
event.origin != window.context.location.origin ||
!event.data ||
event.data.sentinel != 'amp-3p' ||
event.data.type != 'intersection') {
return;
}
observerCallback(event.data.changes);
}
// Send request to received records.
nonSensitiveDataPostMessage('send-intersections');
window.addEventListener('message', listener);
return function() {
window.removeEventListener('message', listener);
};
return listenParent('intersection', data => {
observerCallback(data.changes);
});
}

/**
Expand Down
54 changes: 54 additions & 0 deletions 3p/messaging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Send messages to parent frame. These should not contain user data.
* @param {string} type Type of messages
* @param {*=} opt_object Data for the message.
*/
export function nonSensitiveDataPostMessage(type, opt_object) {
if (window.parent == window) {
return; // Nothing to do.
}
const object = opt_object || {};
object.type = type;
object.sentinel = 'amp-3p';
window.parent./*OK*/postMessage(object,
window.context.location.origin);
}

/**
* Listen to message events from document frame.
* @param {string} type Type of messages
* @param {function(*)} callback Called with data payload of message.
* @return {function()} function to unlisten for messages.
*/
export function listenParent(type, callback) {
const listener = function(event) {
if (event.source != window.parent ||
event.origin != window.context.location.origin ||
!event.data ||
event.data.sentinel != 'amp-3p' ||
event.data.type != type) {
return;
}
callback(event.data);
};
window.addEventListener('message', listener);
return function() {
window.removeEventListener('message', listener);
};
}
Loading