RxJS: Производительность и утечки памяти в большом приложении
Евгений Поздняков
Предыстория
Приложение
- Около 1000+ компонент
- Redux
- RxJS используется повсеместно
- Нагруженные страницы
- Angular 4/5
- NgRX 2/5
Предыстория
Память
до: 3.5гб
после: 600мб
Время загрузки
до: 1мин
после: 20сек
Рендеринг
до: 30-40сек
после: 1сек
Скорость ответа
до: 10сек
после: <1сек
Поговорим о
- Реактивное программирование
- RxJS и философия
- Откуда утечки?
- Боремся с утечками: помощь Google
- Под капот к RxJS
- Отписываемся с помощью декораторов
- Что в других языках?
- Как найти утечки?… И немного хаков
Реактивное программирование
Реактивное программирование
- Каждое событие - элемент массива
- Мир и события в нем - труба
- Данные можно обрабатывать и фильтровать как элементы массива
- Любая операция асинхронная
| 1 | args | |
|---|---|---|
| sync | value | array |
| async | promise | stream/pipe |
Async vs Sync
RxJS по запчастям

- Observable— это объект или функция, которая выдает последовательности данных во времени
- Observer — это объект или функция, которая знает, как обрабатывать последовательности данных
- Subscriber— это объект или функция, которая связывает Observable и Observer.
RxJS по запчастям
Просто messaging и pub-sub?
Модель Rx
Producer
Consumer
Data pipeline
Time
Declarative
Immutable
Pure, side effect -free
Map, reduce, iterator
Functional Programming
PROMISE VS OBSERVABLE?

Promise
- Thenable
- Разрешено, отклонено
- Невозможно остановить, Невозможно продолжить
- Один объект за раз
Observable
- Обработка нескольких значений во времени
- Легко остановить
- Легко продолжить
- Легко начать заново
Откуда утечки?
Немного примеров
Массивы как источник
source - массив или объект
from - создает Observable-последовательность из array-like или iterable объекта
of - создает Observable-последовательность из переданных аргументов
const task_stream =
// Стрим из всех данных в базе
getTasks().
// выбираем задания только для текущего пользователя
filter((task) => task.user_id == user_id).
// находим те, которые еще не завершились
filter((task) => !task.completed).
// берем только название
map((task) => task.name)Observable.from(source)
.operatorOne()
.operatorTwo()
.oneMoreOperator()
.aaanddMoreOperator()
.filter(value => value)
.subscribe(console.log);События как источник
Observer
- next() - вот тебе новое значение из потока;
- error() - вот тебе ошибка, произошедшая в потоке;
- complete() - поток завершен.

Все просто
Typeahead
return Observable.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));Хорош ли поиск?
А происходит следующее
closed = false
Утечка памяти!
return Observable.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));private subscription: Subscription;
onInit(){
this.subscription = this.listenAndSuggest();
}
listenAndSuggest(){
return Observable
.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
}
Мы забыли отписаться от Cold Pipe
Немного об Observer
Мы забыли отписаться от Cold Pipe
В коде выше некому сделать Unsubscribe
Как решить проблему?
interface Observer<T> {
closed?: boolean;
next: (value: T) => void;
error: (err: any) => void;
complete: () => void;
}
unsubscribe(): void {
let hasErrors = false;
let errors: any[];
if (this.closed) {
return;
}
let { _parent, _ parents, _unsubscribe, _subscriptions } = (<any> this);
this.closed = true;
this._parent = null;
this._parents = null;
this._subscriptions = null;
}Решение... Хорошее ли оно?
Мы забыли отписаться от Cold Pipe
Стрим нужен пока жива вся компонента, давайте отпишемся, когда компота будет больше не нужна
А что делать, если мы используем много Observable? Руками отписываться?

private subscription: Subscription;
onInit(){
this.subscription = this.listenAndSuggest();
}
listenAndSuggest(){
return Observable
.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
}
onDestroy(){
this.subscription.unsubscribe();
}Боремся с утечками
с помощью Google
RxJS не так просто, как кажется. Что говорит Google?
Google советует использовать следующие методы:
- take(n): возвращает N значений перед тем, как остановить Observable.
- takeWhile(predicate): проверяет каждое новое значение с помощью предиката, если предикат возвращает false, Observable complete.
- first(): возвращает первое значение, после чего останавливает Observable.
- takeUntil(Observable): не закрывает pipe до тех пор, пока внутренний Observable не произведет значение
Потому что:
- писать меньше кода
- проще управлять кодом
- меньшим количеством подписок нужно управлять
Под капот к RXJS
Ай-да в исходники
take ()

export function take<T> count: number): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => {
if (count === 0) {
return empty();
} else {
return source.lift(new TakeOperator(count));
}
};
}private nextOrComplete(value: T, predicateResult: boolean): void {
const destination = this.destination;
if (Boolean(predicateResult)) {
destination.next(value);
} else {
destination.complete();
}
}Ай-да в исходники
take ()
Все хорошо
Утечка памяти
Observable.from([1,2,3])
.filter(value => false)
.take(0)
.subscribe(console.log)Observable.from([1,2,3])
.filter(value => false)
.take(1)
.subscribe(console.log)Айда в исходники
takeWhile ()

protected _next(value: T): void {
const destination = this.destination;
let result: boolean;
try {
result = this.predicate(value, this.index++);
} catch (err) {
destination.error(err);
return;
}
this.nextOrComplete(value, result);
}
private nextOrComplete(value: T, predicateResult: boolean): void {
const destination = this.destination;
if (Boolean(predicateResult)) {
destination.next(value);
} else {
destination.complete();
}
}Айда в исходники
takeWhile ()
в случае, если через pipe ничего не пролетит, после того, как isDestroyed станет true, подписка никогда не закроется
takeWhile закроет подписку только если предикат isDestroyed= true в момент, когда через pipe пролетит значение
isDestroyed = false;
listenAndSuggest(){
return Observable
.fromEvent(this.element.nativeElement, 'keyup')
.takeWhile(() => this.isDestroyed)
.subscribe(console.log);
}
onDestroy(){
this.isDestroyed = true;
}Утечка памяти
Айда в исходники
first()
Одно но

protected _complete(): void {
const destination = this.destination;
if (!this.hasCompleted && typeof this.defaultValue !== 'undefined') {
destination.next(this.defaultValue);
destination.complete();
} else if (!this.hasCompleted) {
destination.error(new EmptyError);
}
}If called with no arguments, 'first' emits the first value of the sourse Observable, then completes. If called with a 'predicate' function, 'first' emits the first value of the source that matches the specified condition.
Айда в исходники
takeUntil ()
Все честно, главное сделать emit

notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.complete();
}- takeUntil subscribes and begins mirroring the source Observable. It also
- monitors a second Observable, notifier that you provide. If the notifier
- emits a value, the output Observable stops mirroring the source Observable
- and completes. If the notifier doesn't emit any value and completes
- then takeUntil will pass all values.
Место отписки тоже имеет значение
const timerOne = Rx.Observable.timer(1000, 4000);
const timerTwo = Rx.Observable.timer(2000, 4000)
const timerThree = Rx.Observable.timer(3000, 4000)
const combined = Rx.Observable
.timer(2, 1000)
.takeUntil(this.componentDestroy)
.merge(
timerOne,
timerTwo,
timerThree,
)
.subscribe(console.log) --> 0 0 1 2 3 4 1 5 6 7 8 2 9 10 11 12 3 13 14 15 16Утечка памяти
merge создает новый Observable, takeUntil закроет только все, что до merge
Место отписки тоже имеет значение
const timerOne = Rx.Observable.timer(1000, 4000);
const timerTwo = Rx.Observable.timer(2000, 4000)
const timerThree = Rx.Observable.timer(3000, 4000)
const combined = Rx.Observable
.timer(2, 1000)
.merge(
timerOne,
timerTwo,
timerThree,
)
.takeUntil(this.componentDestroy)
.subscribe(console.log) --> 0 0 1 2 3 4 1 5 6 7 8 2 9 10 11 12 3 13 14 15 16Все хорошо
merge закроет все подписки
Отписываемся с помощью декораторов
Способы отписаться и Decorators
Слишком много лишнего кода, еще некрасивые присваивания
export function unsubscribeManager( blackList = [] ) {
return function ( constructor ) {
const originalOnDestroy = constructor.prototype.ngOnDestroy;
constructor.prototype.ngOnDestroy = function () {
for ( let prop in this ) {
const property = this[ prop ];
if ( !blackList.includes(prop) ) {
if ( property && ( typeof property.unsubscribe === "function" ) ) {
property.unsubscribe();
}
}
}
original &&
typeof originalOnDestroy === 'function' &&
originalOnDestroy(this, arguments);
};
}
}Даем шанс takeUntil()
import { Subject } from 'rxjs/Subject';
export function TakeUntilDestroy(constructor: any) {
const originalNgOnDestroy = constructor.prototype.ngOnDestroy;
constructor.prototype.componentDestroy = function () {
this._takeUntilDestroy$ = this._takeUntilDestroy$ || new Subject();
return this._takeUntilDestroy$.asObservable();
};
constructor.prototype.ngOnDestroy = function () {
if (this._takeUntilDestroy$) {
this._takeUntilDestroy$.next(true);
this._takeUntilDestroy$.complete();
}
if (originalNgOnDestroy && typeof originalNgOnDestroy === 'function') {
originalNgOnDestroy.apply(this, arguments);
}
};
}
Что в других языках?
DISPOSABLE BAG RxSwift

Сomposite disposable RxJava


Как найти утечки...
Немного Хаков
Как найти утечки в RxJS и Angular?



18к
DETACHED NODES и angular
- Angular добавляет нужные ему метаданные в DOM
- Angular пытается уменьшить пересоздание в DOM, поэтому часть DOM не удаляется, чтобы в следующий раз быстрее создать компоненту
Angular Router Reuse Strategy
- Стратегия по умолчанию переиспользовать компоненты если текущий и новый rout одинаковые

id: 1
'/ route / 1'
'/ route / 2'




id: 1
'/ route / 1'
'/ route / 2'

id: 2




ANGULAR ROUTER REUSE STRATEGY
Небольшой Хак и ES6 WeakSet




Ищем место, где Angular добавляет все компоненты в дерево
Собираем мусор, чистим консоль. Запускаем сборщик мусора.
Время ответить на
ваши вопросы
Спасибо за Внимание!
RxJS: Improving Perfomance
By Yauheni Pozdnyakov
RxJS: Improving Perfomance
- 234