?

Log in

No account? Create an account
November 2016   01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
cartoon

NestedTypes 2.0 RC: прямая поддержка агрегации и ОО в JS

Posted on 2016.11.04 at 22:47
Tags: , , , ,
Че-та сюда написать забыл.

Есть набор весьма неприятных проблем, которые возникают в JS в случае, если слой данных SPA действительно сложен. Знаете, например, как на раз делается memory leak в JavaScript? Для мемори лика в языке с GC нам нужны две вещи. Синглтон, и подписка на события. И иногда, утечки добиться настолько легко, что это выглядит как фокус. Я объясню на примере BackboneJS.

Пусть у нас есть глобальная коллекция-синглон каких-то объектов - неважно каких, скажем, users. И где-то на одной из страничек нашего SPA мы выбираем юзеров из списка, и выбранные элементы лежат в другой коллекции. Временной. Которая живет столько же, сколько живет страница (пусть у нас SPA состоит из разных "страниц", которые он переключает меняя рауты).

Ну все, у вас уже утечка памяти. Откуда она берется? Коллекция в Бэкбон подписывается на событие change от своих элементов. Подписка на событие на самом деле означает, что объект, который источник события, держит обратную ссылку на получателя. Чтобы его уведомлять.

Так вот, ваши users лежат в синглтоне, и держат ссылку на каждую из коллекций, в которых они лежат. А это означает, что каждая такая коллекция, в которой лежит хоть один юзер из users, не будет подобрана GC.

Ну конечно, если при смене страницы, которая ее создала (в случае SPA это не настоящее закрытие страницы, а лишь видимость, с точки зрения браузера страница одна), не удалить из всех этих коллекций все элементы, лежащие в синглоне. То есть, если у вас нет деструктора - у вас утечка памяти. И не важно, что у нас GC. Добро пожаловать в увлекательный мир, в котором живут программисты на С++.

Бэкбон - это был просто пример. Механизм этой проблемы таков, что ему все равно, бэкбон у вас или не бэкбон.

Так вот. Не знаю, как вы, но не хочу обратно в увлекательный мир С++, частью которого я уже был примерно 6 лет. Поэтому, NestedTypes 2.0 RC, который мы в Volicon/Verizon сейчас готовим к выпуску, и который сейчас лежит в основе наших приложений для мониторинга потоковых видео-трансляций, управляет памятью полностью автоматически. При условии, что вы отличаете агрегацию от ассоциации, и напишете об этом в декларации атрибута модели.

А вот о том, что такое агрегация в ОО - вот вам статья. С веселыми картинками и доступными примерами.

Статья с веселыми картинками


Comments:


mudasobwa
mudasobwa at 2016-11-05 07:42 (UTC) (Link)
При закрытии страницы браузер освободит ее песочницу целиком: users же лежат не как попало в куче. Или вы про какое-то другое закрытие страницы?
Gaperton
gaperton at 2016-11-05 08:05 (UTC) (Link)
Не в случае SPA. Там браузер не знает про закрытие страниц, несмотря на то, что адрес в строке навигации меняется. С точки зрения браузера, страница одна. Я, в общем, про SPA пишу.

Edited at 2016-11-05 08:07 am (UTC)
mudasobwa
mudasobwa at 2016-11-05 08:23 (UTC) (Link)
А, речь идет про переход между страницами в SPA? Тогда странно это называть утечкой памяти. Обычно люди называют то, что вы описали, кэшем.

Приложение загрузилось, и пока не выгрузится, держит users в памяти. Надо, чтобы их не было — выгрузи эксплицитно. Я не очень понимаю, почему от GC нужно ожидать чуткого отношения к переходам между псевдо-страницами. Так можно и при переходе по нормальному HTML3.2 анкору все перезагружать, но это как-то противоречит той логике, которая как раз породила SPA. Я не защищаю эту логику, я просто пытаюсь понять, почему вас удивляет, что утка и ходит, как утка, и квакает, как утка.
Gaperton
gaperton at 2016-11-05 17:48 (UTC) (Link)
> Тогда странно это называть утечкой памяти. Обычно люди называют то, что вы описали, кэшем.

Утекающую вторую коллекцию люди обычно называют утечкой памяти. "Кэшем" это никто не называет.

> Приложение загрузилось, и пока не выгрузится, держит users в памяти.

Речь не о синглтоне users, а о второй коллекции. Утекает она. Перечитайте пост внимательно.

> я просто пытаюсь понять, почему вас удивляет

Меня ничего не удивляет. Я довольно хорошо понимаю, что в данном примере происходит и почему оно себя ведет так.

Edited at 2016-11-05 05:49 pm (UTC)
aamonster
aamonster at 2016-11-05 10:29 (UTC) (Link)
Вроде в увлекательном мире крестиков эта проблема обычно решается через weak references?

И правильно ли я понимаю, что для composited-полей и будут доступны только слабые ссылки? (ну то есть если выбо
Gaperton
gaperton at 2016-11-05 17:45 (UTC) (Link)
В С++ этому в принципе соответствует включение поля по значению. Оно ведет себя как композиция. В результате, деструктор по умолчанию работает правильно и автоматически. Это, собственно, ровно то, что и сделано у нас.


Edited at 2016-11-05 06:14 pm (UTC)
aamonster
aamonster at 2016-11-05 18:29 (UTC) (Link)
Не вижу, как включение поля по значению в плюсах могло бы решить проблему утечки для listeners.
Если мы где-то сохранили ссылку на такое поле - она станет некорректной, когда объект умрёт. И, соответственно, если нам нужно гарантировать, что сохранённая ссылка корректна - придётся удерживать в памяти весь объект (привет, COM с его GetOuterUnknown).
Т.е. опять нужна или явная отписка, или weak references.
Gaperton
gaperton at 2016-11-05 23:15 (UTC) (Link)
> Не вижу, как включение поля по значению в плюсах могло бы решить проблему утечки для listeners.

Это позволяет автоматически сгенерировать корректный деструктор. Который пройдется по составному состоянию всех виджетов на странице, и рекурсивно отпишет агрегированные объекты от событий. Для этого системе надо отличать композицию от ассоциаций, чтобы не прибить то, что лежит в синглтоне.

Деструктор состояния в свою очередь вызывается автоматически при вынимании виджета из DOM. То есть, все надежно, и явной отписки нет.

Edited at 2016-11-06 02:11 am (UTC)
Gaperton
gaperton at 2016-11-05 23:23 (UTC) (Link)
Ну то есть, для данного примера состояние страницы будет выглядеть примерно так:

state : {
    selectedUsers : User.Collection.Refs
}


User.Collection - это конструктор коллекции юзеров. Добавляя к нему Refs - мы получаем коллекцию, которая не пытается брать ownership над своими элементами, и деструктор которой не разрушает объекты внутри нее (но отписывает от событий).

В итоге, когда состояние страницы будет прибито, объекты user внутри синглтона будут живы. Но selectedUsers отпишется от событий, и умрет.

Edited at 2016-11-06 02:11 am (UTC)
aamonster
aamonster at 2016-11-06 08:59 (UTC) (Link)
Так. Похоже, я запутался.
1. "Добавляя к нему Refs - мы получаем коллекцию, которая не пытается брать ownership над своими элементами" - а как делать enumerate объектов в коллекции, если weakrefs нету? Или мы ходим по всем объектам DOM и проверяем, не у нас ли он лежит? Но это ж ад...
2. Как достигается отсутствие ссылок от юзеров на коллекцию? Просто запрет ссылок на неё? Или перенаправление этих ссылок на родительский объект?

Или всё вообще сводится к "вот этой коллекцией я точно владею, поэтому на деструкторе выкинем из неё все элементы"? Т.е. суть композиции - один флажок, учитываемый деструктором? (а по сути - создание "дерева владения", в котором деструктор вышестоящего объекта обрывает все связи в дереве, причём деструктор даже не настоящий, а вызывается при удалении из DOM?)

P.S. Капча достала. Почему жж-программеры не могут задетектить, что юзер один и тот же (залогинен же) и раз за разом капчу успешно решает?
Gaperton
gaperton at 2016-11-06 19:12 (UTC) (Link)
У UI-виджетов во многих фреймворках уже есть деструктор, который вызывается при смене раута и вынимании виджета из DOM. В React это componentWillUnmount, например.

Этот "деструктор" перекрывается в базовом классе так, что он вызывает деструктор состояния виджета.

Состояние - составной объект, где часть полей агрегируется, а часть включена "по ссылке". Он будет делать dispose() всем агрегированным элементам рекурсивно.

Коллекция типа Collection.Refs выглядит точто так же, как обычная коллекция, и ведет себя почти так же, но она не агрегирует свои элементы. То есть, ее деструктор не вызовет dispose() для элементов, просто отпишется от событий от них.

Обычная коллекция предполагает, что все, что внутри - ее. И она это убьет.

То есть, ссылки от юзеров на коллекцию Refs есть, и они будут аккуратно убраны при ее dispose.

> Т.е. суть композиции - один флажок, учитываемый деструктором?

Там все немного сложнее (это не флажок, и он учитывается не только деструктором), но в целом - очень похоже. Композиция влияет на то, как происходит создание, удаление, копирование, сериализация, и валидация. Для композиции все эти операции рекурсивны.

И да, вся суть в создании "дерева владения". При композиции owner может быть один и только один. За этим фреймворк следит. Ну и да, деструктор ясен пень не настоящий, это метод dispose() который вызывается при удалении из DOM.

Edited at 2016-11-06 07:17 pm (UTC)
aamonster
aamonster at 2016-11-06 21:09 (UTC) (Link)
Печально это всё на самом деле. По сути, поверх одной модели владения (ссылки и gc) пришлось накрутить ещё одну, но уже не допускающую циклов.
В reference-counting модели (я за последние годы к ней привык - под macos gc не успел даже попробовать, как его задепрекейтили в пользу arc) всё несколько проще, но, конечно, даны другие способы выстрелить себе в ногу (любимый метод создания циклических ссылок - лямбды [blocks]).

Кстати, не в курсе - текущие реализации js используют чистый gc или reference-counting-based оптимизации применяются (т.е. порой объект может быть убит сразу, как кончились ссылки на него, не дожидаясь прохода gc)?
Gaperton
gaperton at 2016-11-07 02:50 (UTC) (Link)
Это все печально неимоверно. Я думал и о счетчике ссылок поверх, который бы включал деструкторы, но решил что не стоит. От явной агрегации много другой пользы - объекты правильно копируются by default, например.

Текущие реализации GC в JS используют mark-and-sweep, V8 использует stop-and-copy. Счетчиков ссылок, насколько мне известно, не использует никто.

В функциональных языках применяется оптимизация известная с LISP машин - однобитные счетчики ссылок. Специально для элементов однонаправленных списков. В JS это не поможет.
Gaperton
gaperton at 2016-11-05 17:55 (UTC) (Link)
А вик референсы - да, это хорошее решение. В принципе, в JS они есть.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet

Но не работают в IE. Совсем.

Edited at 2016-11-05 05:58 pm (UTC)
Gaperton
gaperton at 2016-11-05 18:03 (UTC) (Link)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap

Вот это должно как-то работать в IE11. В принципе, на этом можно собрать движок событий - достаточно пропустить все ссылки через WeakMap. И тогда ликов не будет.

Что отличная идея, вопрос только, насколько медленно это будет работать в сравнении с обычными ссылками. Я сделаю экспериментальный движок, посмотрим. Может, будет наоборот быстрее - с ними отписка от событий гораздо проще.

Ну, и поддержку IE10 мы пока убрать не можем.

Edited at 2016-11-05 06:20 pm (UTC)
Previous Entry