Skip to content

Commit

Permalink
Queue (#2)
Browse files Browse the repository at this point in the history
feat(queue): rewrite queue logic so that it uses requestIdleCallback
  • Loading branch information
jthoms1 authored May 11, 2017
1 parent 46053da commit ce501f1
Show file tree
Hide file tree
Showing 16 changed files with 509 additions and 43 deletions.
25 changes: 25 additions & 0 deletions demos/fiber-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Ionic Example</title>
<script src="/dist/ionic-web/ionic.js"></script>
</head>
<body>
<h1>Ionic Example</h1>

<fiber-demo></fiber-demo>

<script type="text/javascript">
var start = Date.now();
var baseEl = document.querySelector('fiber-demo');

function update() {
baseEl.elapsed = Date.now() - start;
requestAnimationFrame(update);
}

requestAnimationFrame(update);
</script>
</body>
</html>
1 change: 1 addition & 0 deletions scripts/build-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const BUNDLES = [
{ components: ['ion-gesture', 'ion-scroll'], priority: 'low' },
{ components: ['ion-toggle'] },
{ components: ['ion-slides', 'ion-slide'] },
{ components: ['fiber-demo', 'fiber-triangle', 'fiber-dot'] },
];


Expand Down
1 change: 1 addition & 0 deletions scripts/externs.core.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function define(){};
function disconnectedCallback(){};
function observedAttributes(){};
function requestIdleCallback(){};
function timeRemaining(){};
function shadowRoot(){};


Expand Down
2 changes: 1 addition & 1 deletion src/bindings/web/src/ionic.core.es5.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConfigController } from '../../../util/config-controller';
import { DomClient } from '../../../client/dom-client';
import { IonicGlobal } from '../../../util/interfaces';
import { NextTickClient } from '../../../client/next-tick-client';
import { NextTickClient } from '../../../client/next-tick-client-optimized';
import { PlatformClient } from '../../../client/platform-client';
import { registerComponentsES5 } from '../../../client/registry.es5';
import { Renderer } from '../../../client/renderer/core';
Expand Down
4 changes: 3 additions & 1 deletion src/client/connected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { queueUpdate } from './update';


export function connectedCallback(plt: PlatformApi, config: ConfigApi, renderer: RendererApi, elm: ProxyElement, cmpMeta: ComponentMeta) {

if (!elm.$tmpDisconnected) {
plt.nextTick(() => {
const tag = cmpMeta.tag;

// console.log(elm.nodeName, 'connectedCallback nextTick');
const cmpMode = cmpMeta.modes.find(m => m.modeName === getMode(plt, config, elm, 'mode') || m.modeName === 'default');

plt.loadComponent(cmpMode.bundleId, cmpMeta.priority, function loadComponentCallback() {
plt.loadBundle(cmpMode.bundleId, cmpMeta.priority, function loadComponentCallback() {
queueUpdate(plt, config, renderer, elm, tag);
});
});
Expand Down
47 changes: 10 additions & 37 deletions src/client/next-tick-client.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,26 @@
import { NextTickApi } from '../util/interfaces';
import { noop } from '../util/helpers';
import { NextTickApi, RequestIdleCallback, IdleDeadline } from '../util/interfaces';


export function NextTickClient(window: Window): NextTickApi {
/* Adopted from Vue.js, MIT, https://github.com/vuejs/vue */
export function NextTickClient(window: any): NextTickApi {
const hostScheduleDeferredCallback: RequestIdleCallback = window.requestIdleCallback;
const callbacks: Function[] = [];
let pending = false;
let timerFunc: Function;
const isIOS = /iphone|ipad|ipod|ios/.test(window.navigator.userAgent.toLowerCase());


function nextTickHandler() {
pending = false;
const copies = callbacks.slice(0);

callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
function doWork(deadlineObj: IdleDeadline) {
while (deadlineObj.timeRemaining() > 0 && callbacks.length > 0) {
callbacks.shift()();
}
}


if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) {
const p = Promise.resolve();
const logError = (err: any) => { console.error(err); };

timerFunc = function promiseTick() {
p.then(nextTickHandler).catch(logError);
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop);
};

} else {
// fallback to setTimeout
timerFunc = function timeoutTick() {
setTimeout(nextTickHandler, 0);
};
if (pending = (callbacks.length > 0)) {
hostScheduleDeferredCallback(doWork);
}
}

function queueNextTick(cb: Function) {
callbacks.push(cb);

if (!pending) {
pending = true;
timerFunc();
hostScheduleDeferredCallback(doWork);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/client/platform-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function PlatformClient(win: Window, doc: HTMLDocument, IonicGbl: IonicGl
};


function loadComponent(bundleId: string, priority: string, cb: Function): void {
function loadBundle(bundleId: string, priority: string, cb: Function): void {
if (loadedBundles[bundleId]) {
// we've already loaded this bundle
cb();
Expand Down Expand Up @@ -322,7 +322,7 @@ export function PlatformClient(win: Window, doc: HTMLDocument, IonicGbl: IonicGl
return {
registerComponent: registerComponent,
getComponentMeta: getComponentMeta,
loadComponent: loadComponent,
loadBundle: loadBundle,

isElement: isElement,
isText: isText,
Expand Down
3 changes: 3 additions & 0 deletions src/client/renderer/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { vnode } from './vnode';

import { updateAttrs } from './modules/attributes';
import { updateClass } from './modules/class';
import { updateStyle } from './modules/style';
import { updateEventListeners } from './modules/eventlisteners';
import { updateProps } from './modules/props';
export { VNode, VNodeData, vnode };
Expand Down Expand Up @@ -122,6 +123,7 @@ export function Renderer(api: PlatformApi): RendererApi {

updateAttrs(emptyNode, vnode);
updateClass(emptyNode, vnode);
updateStyle(emptyNode, vnode);
updateEventListeners(emptyNode, vnode);
updateProps(emptyNode, vnode);

Expand Down Expand Up @@ -278,6 +280,7 @@ export function Renderer(api: PlatformApi): RendererApi {
if (vnode.vdata !== undefined) {
updateAttrs(oldVnode, vnode);
updateClass(oldVnode, vnode);
updateStyle(emptyNode, vnode);
updateEventListeners(oldVnode, vnode);
updateProps(oldVnode, vnode);

Expand Down
52 changes: 52 additions & 0 deletions src/client/renderer/modules/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {VNode, VNodeData} from '../../../util/interfaces';

export type VNodeStyle = Record<string, string> & {
delayed: Record<string, string>
remove: Record<string, string>
};

var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout;
var nextFrame = function(fn: any) { raf(function() { raf(fn); }); };

function setNextFrame(obj: any, prop: string, val: any): void {
nextFrame(function() { obj[prop] = val; });
}

export function updateStyle(oldVnode: VNode, vnode: VNode): void {
var cur: any, name: string, elm = vnode.elm,
oldStyle = (oldVnode.vdata as VNodeData).style,
style = (vnode.vdata as VNodeData).style;

if (!oldStyle && !style) return;
if (oldStyle === style) return;
oldStyle = oldStyle || {} as VNodeStyle;
style = style || {} as VNodeStyle;
var oldHasDel = 'delayed' in oldStyle;

for (name in oldStyle) {
if (!style[name]) {
if (name[0] === '-' && name[1] === '-') {
(elm as any).style.removeProperty(name);
} else {
(elm as any).style[name] = '';
}
}
}
for (name in style) {
cur = style[name];
if (name === 'delayed') {
for (name in style.delayed) {
cur = style.delayed[name];
if (!oldHasDel || cur !== oldStyle.delayed[name]) {
setNextFrame((elm as any).style, name, cur);
}
}
} else if (name !== 'remove' && cur !== oldStyle[name]) {
if (name[0] === '-' && name[1] === '-') {
(elm as any).style.setProperty(name, cur);
} else {
(elm as any).style[name] = cur;
}
}
}
}
150 changes: 150 additions & 0 deletions src/client/requestIdleCallbackPolly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@

/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMFrameScheduling
* @flow
*/

'use strict';

// This a built-in polyfill for requestIdleCallback. It works by scheduling
// a requestAnimationFrame, store the time for the start of the frame, then
// schedule a postMessage which gets scheduled after paint. Within the
// postMessage handler do as much work as possible until time + frame rate.
// By separating the idle call into a separate event tick we ensure that
// layout, paint and other browser work is counted against the available time.
// The frame rate is dynamically adjusted.

export type Deadline = {
timeRemaining: () => number,
};

// TODO: There's no way to cancel these, because Fiber doesn't atm.
export let rAF: (callback: (time: number) => void) => number;
export let rIC: (callback: (deadline: Deadline) => void) => number;

if (typeof requestAnimationFrame !== 'function') {
throw Error(
'React depends on requestAnimationFrame. Make sure that you load a ' +
'polyfill in older browsers.',
);
} else if (typeof requestIdleCallback !== 'function') {
// Wrap requestAnimationFrame and polyfill requestIdleCallback.

var scheduledRAFCallback: Function = null;
var scheduledRICCallback: Function = null;

var isIdleScheduled = false;
var isAnimationFrameScheduled = false;

var frameDeadline = 0;
// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
var previousFrameTime = 33;
var activeFrameTime = 33;

var frameDeadlineObject = {
timeRemaining: typeof performance === 'object' &&
typeof performance.now === 'function'
? function() {
// We assume that if we have a performance timer that the rAF callback
// gets a performance timer value. Not sure if this is always true.
return frameDeadline - performance.now();
}
: function() {
// As a fallback we use Date.now.
return frameDeadline - Date.now();
},
};

// We use the postMessage trick to defer idle work until after the repaint.
var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2);
var idleTick = function(event: any) {
if (event.source !== window || event.data !== messageKey) {
return;
}
isIdleScheduled = false;
var callback = scheduledRICCallback;
scheduledRICCallback = null;
if (callback) {
callback(frameDeadlineObject);
}
};
// Assumes that we have addEventListener in this environment. Might need
// something better for old IE.
window.addEventListener('message', idleTick, false);

var animationTick = function(rafTime: number) {
isAnimationFrameScheduled = false;
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If we get lower than that, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime = nextFrameTime < previousFrameTime
? previousFrameTime
: nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
if (!isIdleScheduled) {
isIdleScheduled = true;
window.postMessage(messageKey, '*');
}
var callback = scheduledRAFCallback;
scheduledRAFCallback = null;
if (callback) {
callback(rafTime);
}
};

rAF = function(callback: (time: number) => void): number {
// This assumes that we only schedule one callback at a time because that's
// how Fiber uses it.
scheduledRAFCallback = callback;
if (!isAnimationFrameScheduled) {
// If rIC didn't already schedule one, we need to schedule a frame.
isAnimationFrameScheduled = true;
requestAnimationFrame(animationTick);
}
return 0;
};

rIC = function(callback: (deadline: Deadline) => void): number {
// This assumes that we only schedule one callback at a time because that's
// how Fiber uses it.
scheduledRICCallback = callback;
if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;
requestAnimationFrame(animationTick);
}
return 0;
};
} else {
rAF = requestAnimationFrame;
rIC = requestIdleCallback;
}
Loading

0 comments on commit ce501f1

Please sign in to comment.