by Gerard Sans | @gerardsans
Bending time
with Schedulers and RxJS 5
Bending time
with Schedulers and RxJS 5
Google Developer Expert
Google Developer Expert
International Speaker
Spoken at 69 events in 23 countries
Blogger
Blogger
Community Leader
900
1.4K
Trainer
Master of Ceremonies
Master of Ceremonies
Woot!? Event Loop?
Macro & microtasks Quiz
const log = console.log;
const macro = v => setTimeout(() => log(v));
const micro = v => Promise.resolve().then(() => log(v));
log(1);
macro(2);
micro(3);
log(4);
a) 1 2 3 4
b) 1 4 2 3
c) 1 4 3 2
Event Loop
waits microtasks
JavaScript: call me maybe?
setTimeout Quiz
setTimeout(() => log(1));
setTimeout(() => log(2), 0);
log(3)
// a) 1 2 3
// b) 3 2 1
// c) 3 1 2
YAY!
setTimeout Quiz
setTimeout(() => log(1), 1000);
setTimeout(() => log(2), 2000);
setTimeout(() => log(3), 3000);
// a) 1 2 3 (wait aprox. 1s)
// b) 1 2 3 (wait aprox. 3s)
// c) 1 2 3 (wait aprox. 6s)
setInterval(task, 50)
Time
50ms
task 1
task 2
task 3
task < delay
setInterval(task, 50)
Time
50ms
task 1
task 2
task 3
task > delay
task 1
task 2
task 3
Reality
Ideal
delay is only
time to enter
Event Loop
RxJS help me!
ajax, audit, auditTime, bindCallback, bindNodeCallback, buffer, bufferCount, bufferTime, bufferToggle, bufferWhen, cache, catch, combineAll, combineLatest, concat, concatAll, concatMap, concatMapTo, count, create, debounce, debounceTime, defaultIfEmpty, defer, delay, delayWhen, dematerialize, distinct, distinctKey, distinctUntilChanged, distinctUntilKeyChanged, do, elementAt, empty, every, exhaust, exhaustMap, expand, filter, finally, find, findIndex, first, forkJoin, from, fromEvent, fromEventPattern, fromPromise, generate, groupBy, ignoreElements, interval, isEmpty, last, let, map, mapTo, materialize, max, merge, mergeAll, mergeMap, mergeMapTo, mergeScan, min, multicast, never, observeOn, of, pairwise, partition, pluck, publish, publishBehavior, publishLast, publishReplay, race, range, reduce, repeat, repeatWhen, retry, retryWhen, sample, sampleTime, scan, share, single, skip, skipLast, skipUntil, skipWhile, startWith, subscribeOn, switch, switchMap, switchMapTo, take, takeLast, takeUntil, takeWhile, throttle, throttleTime, throw, timeInterval, timeout, timeoutWith, timer, timestamp, toArray, toPromise, Utility Operators, window, windowCount, windowTime, windowToggle, windowWhen, withLatestFrom, zip, zipAll
120 operators
Synchronous Operators
Synchronous by default
- Examples: of, from, range
- Default Scheduler: queue
Asynchronous Operators
Asynchronous by default
- Examples: timer, interval
- Scheduler: async
- Primitive: setInterval
Changing default Scheduler
of(1).subscribe(v => l(v));
l(2);
// 1 2
import { async } from 'rxjs/scheduler/async';
of(1, async).subscribe(v => l(v));
l(2);
// 2 1
Least Concurrency Principle
new in RxJS 5.5!
New pipeable* Operators
// before
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/filter';
Observable.interval(1000)
.filter(x => x%2!==0) // --1--3--5--
// after
import { Observable } from 'rxjs/Observable';
import { interval } from 'rxjs/observable/interval';
import { filter } from 'rxjs/operators';
interval(1000)
.pipe(
filter(x => x%2!==0)
)
Scheduler operators
1
1
1
Timeline
Emitted values
complete
next
next
next
Observable
Sync Implementation
// (111|)
let a$ = Rx.Observable.create(observer => {
observer.next(1);
observer.next(1);
observer.next(1);
observer.complete();
});
let subscription = a$.subscribe({
next: v => log(v),
complete: () => log('|')
});
Async Implementation
// --1--1--1|
let a$ = Rx.Observable.create(observer => {
setTimeout(() => observer.next(1));
setTimeout(() => observer.next(1));
setTimeout(() => observer.next(1));
setTimeout(() => observer.complete());
});
let subscription = a$.subscribe({
next: v => log(v),
complete: () => log('|')
});
subscribeOn
1
1
1
complete
next
next
next
1
1
1
.map(v => v)
.subscribeOn(async)
.subscribe()
subscribeOn
- Changes Source Execution
- Only used once
observeOn
1
1
1
complete
next
next
next
1
1
1
.map(v => v)
.observeOn(async)
.subscribe()
observeOn
-
Changes Notifications Execution
- Next, Error, Complete
- Can be used before each operator
subscribeOn Quiz
of(1).subscribeOn(async)
.subscribe({
next: x => log(x),
complete: () => log('3')
});
log('2');
// a) 1 2 3
// b) 2 1 3
// c) 1 3 2
YAS!
Scheduler
Main properties
- Data Structure
- Execution context
- Virtual clock
Schedulers Overview
Type | Execution | Primitives |
---|---|---|
queue | Sync | scheduler.schedule(task, delay) scheduler.flush() |
asap | Async (micro) | Promise.resolve().then(() => task) |
async | Async (macro) | id = setInterval(task, delay) clearInterval(id) |
animationFrame | Async | id = requestAnimationFrame(task) cancelAnimationFrame(id) |
Queue Scheduler
Overview
- Executes Synchronously
- Tasks execute in order
- Waits until current task ends before executing next one
- Performant (precedes Event Loop)
QueueScheduler Example
import { queue } from 'rxjs/scheduler/queue';
queue.schedule(() => log(1));
log(2);
queue.schedule(() => log(3));
// 1 2 3
queue.schedule(() => {
queue.schedule(() => log(1));
log(2);
queue.schedule(() => log(3));
});
// 2 1 3
Asap Scheduler
Overview
- Executes Asynchronously (micro)
- Tasks execute before next tick
- Relays on Promises
- Performant (precedes Event Loop)
AsapScheduler Example
import { asap } from 'rxjs/scheduler/asap';
import { queue } from 'rxjs/scheduler/queue';
setTimeout(() => log(1));
asap.schedule(() => log(2));
queue.schedule(() => log(3));
// 3 2 1
Async Scheduler
Overview
- Executes Asynchronously (macro)
- Relays on setInterval
- Less performant (uses Event Loop)
AsyncScheduler Example
import { async } from 'rxjs/scheduler/async';
import { queue } from 'rxjs/scheduler/queue';
asap.schedule(() => log(2));
async.schedule(() => log(1));
queue.schedule(() => log(3));
// 3 2 1
Cancelling tasks
import { AsyncScheduler } from 'rxjs/scheduler/AsyncScheduler';
import { AsyncAction } from 'rxjs/scheduler/AsyncAction';
const s = new AsyncScheduler(AsyncAction);
const DELAY = 0;
let subscription;
subscription = s.schedule(v => log(v), DELAY, 1);
s.schedule(v => log(v), DELAY, 2);
log(3);
subscription.unsubscribe();
// 3
// 2
Internal Time
import { AsyncScheduler } from 'rxjs/scheduler/AsyncScheduler';
import { AsyncAction } from 'rxjs/scheduler/AsyncAction';
const s = new AsyncScheduler(AsyncAction);
const DELAY = 2000;
const start = Date.now();
s.schedule(v => log(v), DELAY, 1);
s.schedule(v => log(v), DELAY, 2);
s.schedule(() => log(`${s.now()-start}ms`), DELAY);
log(3);
// 3
// 1
// 2
// 2008ms
Animations Scheduler
Overview
- Executes Asynchronously
- Relays on requestAnimationFrame
- Adapts to Device Frame Rate
- Slows down when is not active
- Balances CPU/GPU load
Animations
FRAMES
Frame Rates
60 FPS
Time
16.66ms
16.66ms
16.66ms
1000/60ms
paint
frame
paint
frame
paint
frame
paint
frame
paint
frame
Stutter or Losing Frames!
60 FPS
Time
16.66ms
16.66ms
16.66ms
using setInterval
const token;
const paintFrame = () => {
// animation code
token = setInterval(paintFrame, 1000/60);
}
paintFrame();
setTimeout(() => clearInterval(token), 2000);
Issues
- Ignores Device Frame Rate
- Runs all the time (batteries enemy)
- Ignores current CPU/GPU load
requestAnimationFrame
60 FPS
Time
16.66ms
16.66ms
16.66ms
1000/60ms
paint
frame
paint
frame
paint
frame
requestAnimationFrame
const token;
const paintFrame = (timestamp) => {
// animation code
token = requestAnimationFrame(paintFrame)
}
requestAnimationFrame(paintFrame);
setTimeout(() => cancelAnimationFrame(token), 2000);
AnimationFrameScheduler
import { animationFrame } from 'rxjs/scheduler/animationFrame';
const DELAY = 0;
const state = { angle: 0 }
const div = document.querySelector('.circle');
const work = state => {
let {angle} = state;
div.style.transform = `rotate(${angle}deg)`;
animationFrame.schedule(work, DELAY, { angle: ++angle%360 });
}
animationFrame.schedule(work, DELAY, state);
Testing
VirtualTime
Scheduler
Overview
- Executes Synchronously
- Queues all actions sorting by delay
- Requires manual execution
VirtualTime Example
import { VirtualTimeScheduler } from 'rxjs/scheduler/VirtualTimeScheduler';
import { VirtualAction } from 'rxjs/scheduler/VirtualTimeScheduler';
const s = new VirtualTimeScheduler(VirtualAction);
const start = Date.now();
s.schedule(v => log(v), 2000, 2); // tasks are sorted by delay
s.schedule(v => log(v), 50, 1);
s.flush(); // manual execution (synchronous)
log(3);
log(`VirtualTimeScheduler: ${s.now()}ms`) // virtual time
log(`Execution: ${Date.now()-start}ms`) // instant execution
// 1
// 2
// 3
// VirtualTimeScheduler: 2000ms
// Execution: 6ms
VirtualTime Example
import { VirtualTimeScheduler } from 'rxjs/scheduler/VirtualTimeScheduler';
import { VirtualAction } from 'rxjs/scheduler/VirtualTimeScheduler';
const s = new VirtualTimeScheduler(VirtualAction);
const start = Date.now();
interval(3600000, s).pipe(take(24)) // 1 hour (3600 x 1000)ms x 24 hours
.subscribe(v => log(v));
s.flush();
log(3);
log(`VirtualTimeScheduler: ${s.now()}ms`)
log(`Execution: ${Date.now()-start}ms`)
// 0...23
// 3
// VirtualTimeScheduler: 86400000ms (1 day)
// Execution: 25ms
Test
Scheduler
Jasmine Example
import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
describe("basic marbles", () => {
it("should support simple values as strings", marbles(m => {
const values = { a: 1 };
const input = m.cold("--a--a--a|", values);
const expected = m.cold("--1--1--1|");
const output = input.map(v => v);
m.expect(output).toBeObservable(expected);
}));
});
});
More
Thanks
Bending time with Schedulers and RxJS 5
By Gerard Sans
Bending time with Schedulers and RxJS 5
Observables have been very popular because of their many qualities: asynchronous processing, composition, performance, powerful operators. But usually there's a less covered feature that lies beneath. That is: Schedulers. In this talk we are going to cover Schedulers in depth, going from the basic APIs to more obscure features to bend time to our will!
- 3,568