Как в моем UI приложении память гигабайтами текла

Лет 5 назад дело было, можно и рассказать, тем более, что весь код в опенсорсе. В те годы я работал техлидом фронтенда в компании Mesosphere. Пилил DC/OS UI. В сердце DC/OS стоял Mesos — система управления ресурсами кластера машин. Это конкурент кубернетиса, который над кубернетисом смеялся, а потом кубернетис его убил.

Все ресурсы кластера нам нужно было показывать на UI и долгое время нам хватало поллинга. Кажется, раз в 10 секунд мы дергали API, Mesos останавливал мир, чтобы собрать JSON внутреннего состояния и отправлял его нам. Очевидно, что останавливать мир кластера, чтобы отрисовать UI как-то неоптимально. К слову, на больших кластерах UI просто не работал. Mesos не успевал собрать состояние до следующего вызова. Даже если увеличить время запроса. К счастью, кроме синхронного API, у Мезоса был еще поток.

Этот поток был реализован по протоколу RecordIO. По сути это не поток, а вечное соединение. Вы начинаете скачивать файл, первой пачкой прилетает текущее состояние, а потом файл не кончается, Мезос продолжает дописывать сообщения, которые нужно парсить.

У такого подхода есть свои плюсы. Это обычное соединение и оно проходит через любой злой прокси, где вебсокеты не пройдут. Поэтому я быстро разобрался с документацией протокола и написал модуль @dcos/recordio. Все отлично работало, но в длительных тестах я заметил, что у браузера дичайше течет память.

Сперва я думал, что память текла, потому что браузер не очищал буфер. Да, и почему он должен был его очищать? Файл (поток) мы же еще не скачали. Техлид команды Мезос долго не мог понять, почему я просто не могу отбросить то, что уже зачитал. Монстр Си++ не знал, что у браузера нет сырых сокетов. Так бывает, это не его среда. Почему на самом деле текла память мы узнаем дальше. Договорились, что запланируем вебсокеты, но проблема оставалась.

Почему текла память. Держим в голове, что буфер не очищается и постоянно растет. Смотрим на строку выборки очередного сообщения из буфера:

record = rest.substring(recordStartPosition, recordEndPosition);

На этом месте растоманы и сикели заорут в голос. Мне и моему менеджеру пришлось сходить в исходники JavaScript движка V8, чтобы разобраться, что substring возвращает слайс большой строки, а не копирует маленький кусочек. Буквально указатели на начало и конец слайса из большой строки. Мы прокидывали весь поток через RxJs, но по пайплайну в итоге шли не маленькие блоки, а весь буфер скопированный по числу сообщений. В итоге после пары часов работы весь буфер начинал копироваться с каждым хартбитом от Мезоса. Память текла гигабайтами.

Я написал второй пакет @dcos/copychars который должен был форсом копировать строку. Для этого надо было прибавить к слайсу пробел и потом этот пробел отрезать 🤡 V8 понимал, когда ты пытаешься подклеить пустую строку и не копировал исходник. Вот поэтому.

Память перестала утекать так быстро, буфер то всё равно растет. Но уже на день работы оператору хватало. Потом я конечно забыл многобайтовые кодировки и пакет пришлось переписать еще разок. Когда меня спрашивают какой проект я вспоминаю с теплотой, то, если подумать, это был этот проект. Крутое было время.