Skip to content

Commit

Permalink
GREEN: CPU idle fix
Browse files Browse the repository at this point in the history
  • Loading branch information
lynchbomb committed Dec 15, 2017
1 parent 9c3e619 commit 59e1ed1
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 50 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"test": "tsc && npm run build && testem ci && node test/headless/run",
"test:headless": "mocha --compilers js:babel-core/register test/headless/specs/**/*.js",
"serve": "node test/headless/server/app",
"serve": "npm run build && node test/headless/server/app",
"watch": "broccoli-timepiece exports",
"build": "./scripts/build.sh",
"stats": "node scripts/size-calc",
Expand Down
11 changes: 7 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import {
Frame
} from './metal/index';

import w from './metal/window-proxy';

export {
on,
off,
Expand All @@ -55,16 +57,17 @@ export {
SpanielObserver,
SpanielTrackedElement,
setGlobalEngine,
getGlobalEngine
getGlobalEngine,
w as __w__
};

export function queryElement(el: Element, callback: (bcr: ClientRect, frame: Frame) => void) {
export function queryElement(el: Element, callback: (clientRect: ClientRect, frame: Frame) => void) {
getGlobalScheduler().queryElement(el, callback);
}

export function elementSatisfiesRatio(el: Element, ratio: number = 0, callback: (result: Boolean) => void, rootMargin: DOMMargin = { top: 0, bottom: 0, left: 0, right: 0}) {
queryElement(el, (bcr: ClientRect, frame: Frame) => {
let entry = generateEntry(frame, bcr, el, rootMargin);
queryElement(el, (clientRect: ClientRect, frame: Frame) => {
let entry = generateEntry(frame, clientRect, el, rootMargin);
callback(entrySatisfiesRatio(entry, ratio));
});
}
Expand Down
26 changes: 13 additions & 13 deletions src/intersection-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ export class SpanielIntersectionObserver implements IntersectionObserver {

let id = trackedTarget.__spanielId = trackedTarget.__spanielId || generateToken();

this.scheduler.watch(target, (frame: Frame, id: string, bcr: DOMRectReadOnly) => {
this.onTick(frame, id, bcr, trackedTarget);
this.scheduler.watch(target, (frame: Frame, id: string, clientRect: DOMRectReadOnly) => {
this.onTick(frame, id, clientRect, trackedTarget);
}, trackedTarget.__spanielId);
return id;
}
private onTick(frame: Frame, id: string, bcr: DOMRectReadOnly, el: Element) {
let { numSatisfiedThresholds, entry } = this.generateEntryEvent(frame, bcr, el);
private onTick(frame: Frame, id: string, clientRect: DOMRectReadOnly, el: Element) {
let { numSatisfiedThresholds, entry } = this.generateEntryEvent(frame, clientRect, el);
let record: EntryEvent = this.records[id] || (this.records[id] = {
entry,
numSatisfiedThresholds: 0
Expand All @@ -109,9 +109,9 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
takeRecords(): IntersectionObserverEntry[] {
return [];
}
private generateEntryEvent(frame: Frame, bcr: DOMRectReadOnly, el: Element): EntryEvent {
private generateEntryEvent(frame: Frame, clientRect: DOMRectReadOnly, el: Element): EntryEvent {
let count: number = 0;
let entry = generateEntry(frame, bcr, el, this.rootMarginObj);
let entry = generateEntry(frame, clientRect, el, this.rootMarginObj);
let ratio = entry.intersectionRatio;

for (let i = 0; i < this.thresholds.length; i++) {
Expand Down Expand Up @@ -179,8 +179,8 @@ export class IntersectionObserverEntry implements IntersectionObserverEntryInit
};
*/

export function generateEntry(frame: Frame, bcr: DOMRectReadOnly, el: Element, rootMargin: DOMMargin): IntersectionObserverEntry {
let { top, bottom, left, right } = bcr;
export function generateEntry(frame: Frame, clientRect: DOMRectReadOnly, el: Element, rootMargin: DOMMargin): IntersectionObserverEntry {
let { top, bottom, left, right } = clientRect;
let rootBounds: ClientRect = {
left: rootMargin.left,
top: rootMargin.top,
Expand All @@ -190,11 +190,11 @@ export function generateEntry(frame: Frame, bcr: DOMRectReadOnly, el: Element, r
height: frame.height - (rootMargin.bottom + rootMargin.top)
};

let intersectX = Math.max(rootBounds.left, bcr.left);
let intersectY = Math.max(rootBounds.top, bcr.top);
let intersectX = Math.max(rootBounds.left, clientRect.left);
let intersectY = Math.max(rootBounds.top, clientRect.top);

let width = Math.min(rootBounds.left + rootBounds.width, bcr.right) - intersectX;
let height = Math.min(rootBounds.top + rootBounds.height, bcr.bottom) - intersectY;
let width = Math.min(rootBounds.left + rootBounds.width, clientRect.right) - intersectX;
let height = Math.min(rootBounds.top + rootBounds.height, clientRect.bottom) - intersectY;

let intersectionRect: ClientRect = {
left: width >= 0 ? intersectX : 0,
Expand All @@ -209,7 +209,7 @@ export function generateEntry(frame: Frame, bcr: DOMRectReadOnly, el: Element, r
time: frame.timestamp,
rootBounds,
target: <SpanielTrackedElement>el,
boundingClientRect: marginToRect(bcr),
boundingClientRect: marginToRect(clientRect),
intersectionRect
});
}
4 changes: 2 additions & 2 deletions src/metal/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default QueueElement;

export class QueueDOMElement implements QueueDOMElementInterface {
el: Element;
callback: (frame: FrameInterface, id: string, bcr: ClientRect) => void;
callback: (frame: FrameInterface, id: string, clientRect: ClientRect) => void;
id: string;
bcr: ClientRect;
clientRect: ClientRect;
}
11 changes: 9 additions & 2 deletions src/metal/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export interface QueueElementInterface {

export interface QueueDOMElementInterface {
id: string;
callback: (frame: FrameInterface, id: string, bcr: ClientRect) => void;
callback: (frame: FrameInterface, id: string, clientRect: ClientRect) => void;
el: Element;
clientRect: ClientRect;
}

export interface QueueInterface {
Expand All @@ -42,7 +43,7 @@ export interface SchedulerInterface extends BaseSchedulerInterface {
}

export interface ElementSchedulerInterface extends BaseSchedulerInterface {
watch: (el: Element, callback: (frame: FrameInterface, id: string, bcr: ClientRect) => void, id?: string) => string;
watch: (el: Element, callback: (frame: FrameInterface, id: string, clientRect: ClientRect) => void, id?: string) => string;
}

export interface FrameInterface {
Expand All @@ -59,3 +60,9 @@ export interface MetaInterface {
scrollLeft: number;
scrollTop: number;
}

export interface OnWindowIsDirtyInterface {
fn: any;
scope: any;
id: string;
}
41 changes: 32 additions & 9 deletions src/metal/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const TOKEN_SEED = 'xxxx'.replace(/[xy]/g, function(c) {
});
let tokenCounter = 0;

function generateRandomToken() {
return Math.floor(Math.random() * (9999999 - 0o0)).toString(16);
}

export class Frame implements FrameInterface {
constructor(
public timestamp: number,
Expand Down Expand Up @@ -75,6 +79,7 @@ export abstract class BaseScheduler {
}
this.toRemove = [];
}

this.applyQueue(Frame.generate());
this.engine.scheduleRead(this.tick.bind(this));
}
Expand All @@ -85,15 +90,15 @@ export abstract class BaseScheduler {
scheduleRead(callback: Function) {
this.engine.scheduleRead(callback);
}
queryElement(el: Element, callback: (bcr: ClientRect, frame: Frame) => void) {
let bcr: ClientRect = null;
queryElement(el: Element, callback: (clientRect: ClientRect, frame: Frame) => void) {
let clientRect: ClientRect = null;
let frame: Frame = null;
this.engine.scheduleRead(() => {
bcr = el.getBoundingClientRect();
clientRect = el.getBoundingClientRect();
frame = Frame.generate();
});
this.engine.scheduleWork(() => {
callback(bcr, frame);
callback(clientRect, frame);
});
}
unwatch(id: string| Element | Function) {
Expand Down Expand Up @@ -144,29 +149,47 @@ export class PredicatedScheduler extends Scheduler implements SchedulerInterface

export class ElementScheduler extends BaseScheduler implements ElementSchedulerInterface {
protected queue: DOMQueue;
protected isDirty: boolean = false;
protected id: string = '';

constructor(customEngine?: EngineInterface) {
super(customEngine);
this.queue = new DOMQueue();
this.id = generateRandomToken();
W.onWindowIsDirtyListeners.push({ fn: this.windowIsDirtyHandler, scope: this, id: this.id });
}

applyQueue(frame: Frame) {
for (let i = 0; i < this.queue.items.length; i++) {
let { callback, el, id } = this.queue.items[i];
let bcr = el.getBoundingClientRect();
let { callback, el, id, clientRect } = this.queue.items[i];

if (this.isDirty || !clientRect) {
clientRect = this.queue.items[i].clientRect = el.getBoundingClientRect();
}

callback(frame, id, bcr);
callback(frame, id, clientRect);
}

this.isDirty = false;
}
watch(el: Element, callback: (frame: FrameInterface, id: string, bcr: ClientRect) => void, id?: string): string {

watch(el: Element, callback: (frame: FrameInterface, id: string, clientRect?: ClientRect) => void, id?: string): string {
this.startTicking();
id = id || generateToken();
let clientRect = el.getBoundingClientRect();

this.queue.push({
el,
callback,
id
id,
clientRect
});
return id;
}

windowIsDirtyHandler() {
this.isDirty = true;
}
}

let globalScheduler: Scheduler = null;
Expand Down
19 changes: 18 additions & 1 deletion src/metal/window-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

// detect the presence of DOM
import {
MetaInterface
MetaInterface,
OnWindowIsDirtyInterface
} from './interfaces';

const nop = () => 0;
Expand All @@ -25,6 +26,8 @@ interface WindowProxy {
getWidth: Function;
rAF: Function;
meta: MetaInterface;
onWindowIsDirtyListeners: OnWindowIsDirtyInterface[];
__destroy__: Function;
}

const hasDOM = !!((typeof window !== 'undefined') && window && (typeof document !== 'undefined') && document);
Expand All @@ -40,12 +43,16 @@ let W: WindowProxy = {
getScrollLeft: nop,
getHeight: nop,
getWidth: nop,
onWindowIsDirtyListeners: [],
rAF: hasRAF ? window.requestAnimationFrame.bind(window) : (callback: Function) => { callback(); },
meta: {
width: 0,
height: 0,
scrollTop: 0,
scrollLeft: 0
},
__destroy__() {
this.onWindowIsDirtyListeners = [];
}
};

Expand Down Expand Up @@ -73,6 +80,11 @@ function resizeThrottle() {
resizeTimeout = window.setTimeout(() => {
windowSetDimensionsMeta();
}, throttleDelay);

W.onWindowIsDirtyListeners.forEach((obj) => {
let { fn, scope } = obj;
fn.call(scope);
});
}

// Only invalidate window scroll on scroll
Expand All @@ -82,6 +94,11 @@ function scrollThrottle() {
scrollTimeout = window.setTimeout(() => {
windowSetScrollMeta();
}, throttleDelay);

W.onWindowIsDirtyListeners.forEach((obj) => {
let { fn, scope } = obj;
fn.call(scope);
});
}

if (hasDOM) {
Expand Down
2 changes: 2 additions & 0 deletions src/spaniel-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ export class SpanielObserver implements SpanielObserverInterface {
off('unload', this.onWindowClosed);
off('hide', this.onTabHidden);
off('show', this.onTabShown);

w.__destroy__();
}
}
unobserve(element: SpanielTrackedElement) {
Expand Down
15 changes: 14 additions & 1 deletion test/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ const constants = {
RAF_THRESHOLD : 16,
SMALL: 5
},
ITEM_TO_OBSERVE: 5
ITEM_TO_OBSERVE: 5,
MAC_WINDOW_BAR_HEIGHT: 22,
VIEWPORT: {
WIDTH: 400,
HEIGHT: 400
},
NIGHTMARE: {
TIMEOUT: 10,
OPTIONS: {
show: false,
openDevTools: false,
waitTimeout: 0
}
}
};


Expand Down
20 changes: 12 additions & 8 deletions test/headless/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ Unless required by applicable law or agreed to in writing, software 
distribut
*/

import { assert } from 'chai';
import rsvp from 'rsvp';
import constants from '../constants';
import Nightmare from 'nightmare';
import rsvp from 'rsvp';

const TIMEOUT = 10;
const MAC_WINDOW_BAR_HEIGHT = 22; // See https://github.com/segmentio/nightmare/issues/722
const {
VIEWPORT,
NIGHTMARE,
MAC_WINDOW_BAR_HEIGHT
} = constants;

export default class Context {
constructor() {
this._nightmare = Nightmare({ show: false }),
this._nightmare.viewport(400, 400 + MAC_WINDOW_BAR_HEIGHT);
this._nightmare = Nightmare(NIGHTMARE.OPTIONS),
this._nightmare.viewport(VIEWPORT.WIDTH, VIEWPORT.HEIGHT + MAC_WINDOW_BAR_HEIGHT);
this._events = [];
this._results = [];
this._assertions = [];
this._execution = this._root = this._nightmare.goto('http://localhost:3000/').wait(10).evaluate(function() {
this._execution = this._root = this._nightmare.goto('http://localhost:3000/').wait(NIGHTMARE.TIMEOUT).evaluate(function() {
window.STATE = {};
});
}
Expand All @@ -41,12 +45,12 @@ export default class Context {
}

viewport(width, height) {
this._execution = this._execution.viewport(width, height).wait(TIMEOUT);
this._execution = this._execution.viewport(width, height).wait(NIGHTMARE.TIMEOUT);
return this;
}

scrollTo(top, left) {
this._execution = this._execution.scrollTo(top, left).wait(TIMEOUT);
this._execution = this._execution.scrollTo(top, left).wait(NIGHTMARE.TIMEOUT);
return this;
}

Expand Down
Loading

0 comments on commit 59e1ed1

Please sign in to comment.