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