"Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

79
October 2nd, Moscow Yet another Conference 2013 Welcome to SoundCloud! Sunday, September 29, 13

description

Больше года назад мы запустили новую версию SoundCloud. На время доклада вы сможете стать её разработчиком и понять, какие решения формируют клиентскую архитектуру основных проектов SoundCloud (главного сайта, мобильной версии и других), какие инструменты используются для сборки проектов и измерения производительности, где и как применяются некоторые HTML5 API.

Transcript of "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Page 1: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

October 2nd, Moscow

Yet another Conference 2013

Welcome to SoundCloud!

Sunday, September 29, 13

Page 2: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Alexander KovalevSoftware Engineer @ SoundCloud

Sunday, September 29, 13

Добрый день! Меня зовут Александр Ковалев.Последние 2,5 года я работаю в Берлинской компании SoundCloudОФициально моя должность называется Software Engineer.Но в основном я фокусируюсь на клиентских технологиях. За это время успел поработать почти на всех главных проектах.

Page 3: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

SoundCloud

• Social audio platform

• 55 mln users and growing

• 240 mln unique visitors a month

• 12 hours of audio are uploaded every minute

• 15 engineering teams

Sunday, September 29, 13

Я думаю, многие из вас слышали о нашей компании, но вот несколько фактов и цифр. SoundCloud это социальная платформа, позволяющая музыкантам делиться своей музыкой, а обычным пользователям - ее слушать. В настоящее время мы очень быстро растем. По данным нащей разведки, у нас более 50 млн пользователей. более 240 млн уникальных посетителей в месяц. Каждую минуту мы обрабатываем порядка 12 часов нового аудил контента.В SoundCloud порядка 20 команд, занимающихся разнымим сервисами, порой невидимыми снаружи. Но в своей презентации я сконцентрируюсь на клиентских проектах - главный сайт, мобильная версия и виджет.

Page 4: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Quick assumptions

• Awareness of MVC pattern

• you know that SPA is not a mineral spring

• why and when SPA makes a difference

• you heard of web app frameworks

Sunday, September 29, 13

Готовясь к этому докладу, я естественно делал некоторые допущения. Я думаю, - что вы слышали о паттерне MVC, - вы знаете о таком подходе к созданию веб приложений, как Single Page Application. - вы понимаете где и зачем, его уместно применять - и вы сами пытались применить один из современных фреймворков на практике

Page 5: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Sunday, September 29, 13

Мой доклад называется Добро пожаловать в SoundCloud! И за время доклада я постсраюсь рассказать об основных идеях и решениях, формирующих архитектуру новой версии SoundCloud, изначально известной внутри компании как NEXT.

Page 6: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

What is NEXT?

• default production app since December, 2012

• SoundCloud API client

• the long running app

• ~45,000 LOC, 80% of that is JS

• supports only modern browsers

• better experience

Sunday, September 29, 13

Что такое NEXT? Это новая версия, доступная в production с декабря 2012 года, по сути является клиентом нашего собственного API, за редкими исключениями. Это Single Page Application, протяженность сессии которой иногда достигает и превышает сутки. В разы увеличила кол-во прослушиваний и вовлеченность пользователей, что несомненно можно расценивать как положительный результат.

Page 7: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Our Stack

• Require.js (Almond.js)

• Node.js

• Uglify.js/Esprima

• Klang.js

• Backbone (Trombone)

• Handlebars (with plenty of helpers)

• Home-grown development tools

Sunday, September 29, 13

В разработке NEXT и практически всех client-side проектов используется следующий, я бы сказал, типичный хипстерский стэк -- ну разве что на coffeescript не пишем.Тут нет ничего необычного - RequireJs и Almond.js используются для загрузки модулей во время разработки и в продакшене соответсвенно. Node.js/Uglify.js используется для инструментов сборки, разработческого сервера и статического анализа кода._

В продакшене Node.js сервера при этом нет. так как являясь клиентом собственного API, сайт абсолютно статический. Важной компонентой является библиотека Klang.js, предоставлюящая лучший способ для воспроизведения аудио в зависимости от конкретной платформы. Ну и не менее важной состовляющей нашего стека является Backbone, вернее то во что мы его превратили. Но об этом попозже.

Page 8: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Why Backbone?

Sunday, September 29, 13

_Но почему Backbone спрсите вы? Ведь Backbone это не фреймворк, а всего лишь небольшая библиотека, абсолютно не даюшая каких-либо рекоммандаций о том, как ваше приложение должно быть структурировано, какие практики применять, чтобы эффективно работать с памятью, на худой конец, просто, чтобы работа над приложением не превратилась в кошмар. А сначала так оно и было.

Page 9: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Sunday, September 29, 13

Мы имели довольно печальный опыт работы с Backbone в первой мобильной версии, где мы собрали все возможные подводные камни. Это было больше 2.5 лет назад. Тогда все это движение вокруг SPA, сейчас набравшее уже очень большую скорость, только начиналась. Никто толком не знал, как это делать правильно. Мы тоже не знали. Ни Chaplin, ни Marionette, ни прочих фреймворков еще не было. Ember.js был только в зачаточном состоянии (только-только лишь был создан форк от SproutCore, который обещал тоже стать чем-то большим и громоздким, чтобы стать фундаментом для проектов под разные платформы.

Page 10: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Sunday, September 29, 13

_Но, в следующем проекте, виджете, мы опять выбрали Backbone, потому что он имеет ряд достоинств

Page 11: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Backbone

• small footprint

• Observable mixin

• doesn’t depend on third-party libraries

• helpful methods for data manipulation

• easy to understand and extend

• but not opinionated about app structure

Sunday, September 29, 13

- он небольшой, что довольно важно для мобильных устройств - в нем хорошо реализован паттерн Observable, что важно для коммуникации между модулями приложения - в нем есть компактный, но вполне достаточный, арсенал для работы с данными - практически не зависит от сторонних библиотек, что опять-таки немаловажно для мобильных платформ_

_Но для организации большого приложения этого недостаточно. Нужен дополнительный уровень абстракции, особенно на уровне view компоненты. Из-за необходимости иметь более высокйи уровень абстракции и появился Trombone

Page 12: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Trombone

Sunday, September 29, 13

_Что такое Trombone? Это наш форк backbone'а.

Он превращает небольшую библиотеку в небольшой, но полноценный фреймворк

Trombone удобен для создания как небольших проектов (виджет, мобильная версию), так и довольно крупных - главная версия сайта, которая была написана на его основе, и до сих пор развивается без особых проблем.

Page 13: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Trombone > Backbone

• dev tools

• build tools

• core view component

• view life cycle management

• layout management

• data management

More details here - snd.sc/16i6mhU

Sunday, September 29, 13

Trombone (в отличие от Backbone) предоставляет - удобный набором инструментов для разработки - и сборки проекта - четко описывает подход для структурирования приложения - и предлагает более понятный подход к работе с view компонентой,

на которой я и сфокусируюсь в своем докладе

Page 14: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Distribution of modules in NEXT

315 views

50collections

37models

Sunday, September 29, 13

потому что это самая важная часть клиентской архитектуры, пожалуй, любого проекта.

Но при этом ее базовая реализация в Backbone черезчур минималистична.

В этом легко убедиться, взглянув в исходный код Backbone'а -- это всего лишь 70 строчек кода, без комментариев и того меньше. По сути это заглушка, с которой можно делать, все что угодно. Но что делать не совсем понятно. Backbone не вносит ясности в работу с view, хотя она очень нужна. Эта чрезмерная гибкость - слабость и одновременно сила Backbone'а.

Page 15: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Trombone Views — they do a lot

• define a standard view declaration

• rendering of templates

• establish a data source

• support the nested views

• setup and teardown event listeners

• don’t depend on the context

Sunday, September 29, 13

Trombone убирает эту гибкость и вносит ясность в работу с view.

Базовая view компонента в Trombone делает много того, о чем не позаботился Backbone, а именно: - более формализованный способ создания компоненты - отрисовка шаблонов - определение источника входных данных - поддержка вложенных view - установление и корректное удаление связи с DOM элеменотом и моделью/коллекцией - обеспечение независимоти от контекста использования

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

Page 16: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

View declaration in Trombone

var Sound = require('models/sound');var View = require('lib/view');

var WaveformView = module.exports = View.extend({ template: require('views/sound/waveform.tmpl'), css: require('views/sound/waveform.css'), ModelClass: Sound, requiredAttributes: [‘waveform_url’]});

Sunday, September 29, 13

Давайте рассмотрим создание простой view компоненты.

Кроме отдельного CSS, JS файла и шаблона, view так же в явном виде задает тип данных, которые она отображает. Не сами данные, а именно тип.

_Модули определяются в стиле CommonJS,который во время разработки и сборки проекта на лету компилируются в формат AMD, совместимый с RequireJS. Использование стиля CommonJS позволяет очень легко переиспользовать модули как на клиенте, так и на сервере (во время сборки, к примеру), и избавляет от необходимости писать лишний код.

CSS стили и шаблон тоже запрашиваются как отдельные модули, так же компилируемые в AMD формат. С шаблоном все просто, так как скомпилированный handlebars-шаблон есть просто функция, которая и возвращается модулем,

Page 17: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

¿ ?CSS ↝ AMD

Sunday, September 29, 13

а как же быть с CSS?

Page 18: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Write CSS but serve AMD

.myView { padding: 5px; color: #f0f;}.myView__foo { border: 1px solid #0f0;}

Result is an AMD module

define("views/myView.css", [], function (require, exports, module) { var style = module.exports = document.createElement('style'); style.appendChild( document.createTextNode('.myView { padding: 5px; color ... } ...'); );})

Sunday, September 29, 13

Приминив похожий подход, В результате компиляции исходный CSS файл загружается в виде AMD модуля. Единственная функция, определяемая данным модулем, это функция, которая динамически добавляет стили данной view компоненты в документ.

Page 19: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Write CSS but serve AMD

var WaveformView = module.exports = View.extend({

// it’s a `style` element css: require('views/sound/waveform.css'),

render: function() { if (!this.css.parentNode) { document.body.appendChild(this.css); } // ... }

});

Sunday, September 29, 13

Стили для данной компоненты активируются в момент, когда данная view отрисовывывается впервые.

Page 20: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Nested views

<h1>{{title}}</h1>{{view "view/waveform" id=123}}

var Waveform = require('views/waveform');var waveform = new Waveform({ resource_id: 123 });

soundView.addSubview(waveform);soundView.$el.append(waveform.render().el);

You can add subviews both in a template and in js file

Sunday, September 29, 13

View компоненты могут быть составными и включать в себя дочерние view. Глубина их вложенности произвольная.

В нужный момент Trombone позаботиться о том, что все дочерние view корректно осовободят занимаемые ими ресурсы и так далее.

Как уже было сказано, декларация view пытается свести к минимуму набор входных параметров, необходимых для ее создания. Данный слайд иллюстрирует 2 возможных способа создания вложенной view компоненты. Один способ прогаммный, а другой декларативный с помощью view helper'а прямо в шаблоне.

В обоих случаях достаточно указать лишь идентификатор трека, для которого мы хотим создать waveform'у. Не нужно передавать ссылку на уже созданную Sound модель, или создавать новую, если таковой нет. Об этом позаботиться сама view компонента, сам Trombone.

Page 21: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Looks good, right?

var Sound = require('models/sound');var View = require('lib/view');

var WaveformView = module.exports = View.extend({ template: require('views/sound/waveform.tmpl'), css: require('views/sound/waveform.css'), ModelClass: Sound, initialize: function(args) { // new instance is created for each view this.model = new this.ModelClass({ id: args.id }); this.setupModelListeners(); }});

Sunday, September 29, 13

Такой подход очень удобен, не правда ли?! Но к сожалению содержит одну большую проблему.__Каждый раз будет создаваться новый экземпляр модели. Много экземпляров ведет к чрезмерному потреблению пямяти, что приводит к более частому срабатыванию GC. Ну и наконец, создание нового экземпляра разрушает саму идею синхронизации состояний между view компонентами, представляющими один и тот же объект, одни и те же данные

Page 22: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Sharing is caring

Sunday, September 29, 13

Другими словами, нам надо найти способ использовать один и тот же экземпляр модели в контексте разных view компонент.

Page 23: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Many views, one model

Sunday, September 29, 13

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

Page 24: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Identity Map

IdentityMap.applyTo(Sound, { hashFn: function(attrs) { return attrs.id || null; } });

var s1 = new Sound({ id: 123, genre: 'Ambient' }) , s2 = new Sound({ id: 123, artist: 'Brian Eno’ }); s1 === s2; // true - these are the exact same objects.

Sunday, September 29, 13

_Очень просто__На помощь приходит паттерн IdentityMap, впервые описанный Мартином Фаулером., который, учитывая всю выразительность JavaScript'а, позволяет очень элегантно его реализовать и решить нашу проблему._ _В очень очень очень упрощенной версии -- IdentityMap просто изменяет поведение конструктора таким образом, что он возвращает экземпляр нужной нам модели, если такая модель уже существует, в противном случае -- просто создает ее. Чтобы определить наличие или отсутствие нужной нам модели, каждый ресурс имеет уникальный индентификатор, hash, который высчитывается на основе входных аттрибутов. По умолчанию это аттрибут id._

Page 25: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Don’t use it in production :)var store = {}; Sound = function(attributes.id) { var id = attributes.id; // check if this model has already been created if (store[id]) { return store[id]; // ← return the other one } // otherwise, store this instance store[id] = this; // regular instantiation... this.initialize(); }};

Sunday, September 29, 13

_В крайне упрощенной форме, это то, как выглядит конструктор модели, к которой применен IdentityMap. Повторюсь, что это очень упрощенная версия, приведенная лишь для иллюстрации основной идеи.

Page 26: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

IdentityMap is applied to subclasses

IdentityMap.applyTo(SoundModel, { hashFn: function(attrs) { return attrs.id || null; } });

// Subclasses inherit the “IdentityMap” behaviourPromoSound = SoundModel.extend({});

s1 = new PromoSound({id : 123}),s2 = new PromoSound({id : 123});

s1 === s2; // true

Sunday, September 29, 13

При этом нет необходимости применять этот миксин к каждой модели в вашем проекте.

Примение IdentityMap только к базовым классу модели автоматически добавляет аналогичное поведение на все его подклассы.

Page 27: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Identity Map

var s1 = new Sound({ id: 123, genre: 'Ambient' }) , s2 = new Sound({ id: 123, artist: 'Brian Eno’ }); s1 === s2; // true - these are the exact same objects.

s1.get('genre'); // 'Ambient's2.get('artist'); // 'Music for Airports'

Sunday, September 29, 13

_Применив IdentityMap миксин, мы добились того, что хотели -- оператор new возвращает нужный нам экземпляр модели._

Page 28: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Wait a secondIsn’t it a massive memory leak?

Sunday, September 29, 13

_Но по-прежнему, такое решение не идеально. В случае SPA такой подход может банально привести к большой утечке памяти,

Page 29: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Wait a secondvar store = {};

Sound = function(attrs) { var id = attributes.id; // check if this model has already been created if (store[id]) { return store[id]; } // otherwise, store this instance store[id] = this; // ← it’s a massive a memory leak // regular instantiation... this.initialize(); }};

Sunday, September 29, 13

потому что модель, сохраненная в кеше, никогда не будет подчищена GC._ _Каждый созданный экземпляр будет оставаться в памяти на протяжении всей сессии, протяженность которой в случае SoundCloud иногда может составлять сутки._

Page 30: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Sunday, September 29, 13

_А если это так, то через какое-то время приложение просто станет недоступным._

Page 31: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Release it!var map = {}, counts = {}; // <-- usage counter

function Sound(id) { if (map[id]) { ++counts[id]; return map[id]; } map[id] = this; counts[id] = 1; this.initialize();}Sound.prototype.release = function () { --counts[this.id];}

Sunday, September 29, 13

_Но на самом деле все не так уж страшно. Как я уже сказал на предыдущем слайде была показана упрощенная версия модели, к которой был применен IdentityMap. На самом деле, реальная, а не упрощенная реализация этого миксина, добавляет несколько служебных методов.

Один из них это метод *release*.

Его цель очень проста -- оповестить хранилище созданных моделей о том, что данный экземпляр больше не используется. Все это может показаться довольно запутанным, но на самом деле, корректное освобождение ресурсов в Trombone происходит автоматически. Это делается на уровне базового view класса, к примеру, при переходе с одной страницы на другую._

Page 32: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Garbage Collectiononce a minute

Sunday, September 29, 13

_Затем, когда счетчик использования конкретной модели достиг нуля, она удаляется из памяти. Величина интервала срабатывания GC равна 1 минуте.

Page 33: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Transition between layouts

Sunday, September 29, 13

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

Этот подход очень полезен, так как он не заставляет нас запрашивать данные, необходимые для отрисовки страницы, в случае частых переходов назад-вперед

Page 34: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Browsers are getting better

function leak() { var A = {}, B = {}; A.b = B; B.a = A;}

// Circular reference does exist// but A and B will be cleaned up// as you can't reach either from global scopeleak();

Sunday, September 29, 13

_Мы знаем, что браузеры не стоят на месте._

_И, к примеру, круговая ссылка, пример которой здесь приведен, уже не является проблемой для современных JavaScript движков. Они не влекут утечек памяти, но это по-прежнему JavaScript, который в силу своей природы, всегда оставляет возможность их создать._

Page 35: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

No longer used != No longer reachable

var View = require('lib/view'), Sound = require('models/sound');

var WaveformView = module.exports = View.extend({

setup: function() { this.sound = new SoundModel({ id: 123 }); },

// sound model is not disposed properly later on // as we mistakenly redefined `dispose` method

dispose: function() {}

});

Sunday, September 29, 13

_По-прежнему очень велика вероятность, что, к примеру, экземпляр модели или коллекции не был в конечном итоге корректно "освобожден". Это значит, что он останется в памяти на протяжении всего времени работы приложения.

Данный, возможно, довольно искуственный пример, иллюстрирует вполне жизненный случай. Все делают ошибки. В такие моменты на помощь приходят тесты и прочие инструменты_

Page 36: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Static analysis tools to the rescue

$ ./tools/memory-leak-finder views/waveform.js

Warnings: 1Sound model is not disposed. Release it in `dispose` method.

A very nice read - http://research.google.com/pubs/pub40738.html

Global leaking detector - https://github.com/kesla/node-leaky

Sunday, September 29, 13

_Кроме unit-test'ов, проверяющих корректное освобождение ресурсов, частью нашего предрелизного тестирования является коммандная утилита, которая производит статический анализ кода. Она строит AST c помощью Esprima, анализирует его, находит объекты, создание которых может привести к утечке памяти и проверяет, были ли они корректно освобождены. Если это не так, то оповещает об этом и предлагает решение. Это далеко не идеальный инструмент. В основе ее работы лежит знание основных паттернов, которые мы используем для создания моделей или коллекций, а так же некоторая степень эвристики. Вдохновила на создание такого инструмента утилита JSWhiz - расщирение для Google Closure compiler. Это утилита, которой активно пользуются разработчики команды Google Mail, столкнувашиеся с проблемами утечки памяти. Это довольно занятный инструмент, ссылка на подробное описание того, как он работает, приведена на этом слайде._

Page 37: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Monitor memory infowindow.peformance.memory = { // memory JS heap is limited to jsHeapSizeLimit : 793000000, // memory allocated by JS (including free space) usedJSHeapSize : 18200000, // memory currently being used totalJSHeapSize : 39600000}

setInterval(function() { if (usedJSHeapSize/jsHeapSizeLimit > 0.9) { console.log(‘He is almost dead, Jim!’); }}, 30 * 1000);

Sunday, September 29, 13

_Но естественно, нет идельаного инструмента, особенно для такой нетревиально задачи, как обнаружение и предотвращение утечек памяти. В этой связи, имеет смысл следить за количеством потребляемой памяти. Благо сейчас есть нужные API для таких целей._

_Google Chrome предоставляет доступ к информации об использованной памяти. Поэтому грех этим не пользоваться. Мы используем эту информацию для отслеживания динамики использования памяти на определенной выборке пользователей с разной степенью активности._

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

_jsHeapSizeLimit - максимальное доступный размер памяти__totalJSHeapSize - общий размер памяти, выделенный на процесс, включая освобожденную память, не занимаемую никакими объектами__usedJSHeapSize - общий размер памяти, используемый в текущий момент, включая внутренние объекты V8._

_Когда _usedJSHeapSize -> jsHeapSizeLimit, есть шанс увидеть прекрасную страницу. He's dead, Jim!_

Page 38: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Sunday, September 29, 13

_Чтобы избежать такой ситуации, есть ряд простых рекоммендаций, о которых было сказано уже тысячу раз, но о которых есть смысл напомнить в контексе этого слайда._

Page 39: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Don’t modify the shape of an object

Sunday, September 29, 13

_Изменяя структуру объекта в рантайме, мы переводим объект из категории быстрых объектов в категорию медленных.

Page 40: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Inline caching in V8function Point(x, y, z) { // introduce all properties in the constructor // to enable “fast-object” mode. this.x = x; this.y = y; this.z = z;}

var point = new Point(11, 22, 33);

point112233

Point‘x’‘y’

Point Point‘x’‘y’

‘z’

Point‘x’

x y z

Sunday, September 29, 13

Одной из особенностей двигателя V8 является то, что он для доступа к свойствам объекта пытается использовать не hash-образную структуру данных, а линейную последовательность аттрибутов, которая содержит ссылку на описание layout’а объекта. Такой подход значительно увеличивает скорость доступа к свойствам и методам JS объекта.

Page 41: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Dictionary mode in V8

var point = new Point(11, 22, 33);

// forcing the object into dictionary modedelete point.z;

// accessing a hash table is much slower // than accessing a field at a known offset.console.log(point.x);

point1122

HashMapx : 11y : 22

Sunday, September 29, 13

Но есть ряд действий, которые могут перевести объект из разряда быстрых в разряд медленных.Одно из тахик действий - это удаление свойства объекта в рантайме. После этого структура данных, используемая для представления данного объекта меняется на HashMap, что в разы замедляем доступ к его свойствам и делает работу с такими объектами с точки зрения потребления памяти не эффективной. Рассмотрим пример.

Page 42: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Less is morevar FastSound = function (title, duration) { this.title = title; this.duration = duration;}var SlowSound = function (title, duration) { this.title = title; this.duration = duration;};

var fast = new FastSound('Hommage a Rameau', '350000');var slow = new SlowSound('Hommage a Rameau', '350000');

delete slow.duration;

Sunday, September 29, 13

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

Это довольно интересная тема, безусловно достойная более чем одного слайда, но чтобы не быть голосовным, вот цифры._

Page 43: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Less is more

Constructor Objects count Shallow size Retained size--------------------------------------------------------SlowSound 10000 120 000 4 240 000FastSound 10000 200 000 200 000

Slow object is using up 20 times more memory

Sunday, September 29, 13

После изменения структуры объекта, он занимает памяти почти в 20 раз больше. Это факт, с которым тяжело не считаться._

Page 44: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Unbind event listeners

Sunday, September 29, 13

Не менее важно удалять все обработчики событий перед тем, как view компонента (DOM element) будет удалена.

Page 45: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Trombone takes care of it

View = module.exports = Backbone.View.extend({

✂ ✂ ✂ ✂ ✂ dispose: function() { // remove event listeners this.teardownModelListeners(); this.teardownCollectionListeners(); // tell GC that we don't need these resources anymore this.model.release(); this.collection.release(); }

✂ ✂ ✂ ✂ ✂})

Sunday, September 29, 13

Trombone это делает автоматически, удаляя все обработчики событий, добавленные к модели, коллекции и DOM элементу.

Page 46: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Manage local cache properly

Sunday, September 29, 13

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

Page 47: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Recycle objects

Sunday, September 29, 13

Иногда, можно переиспользовать уже созданный объект, вместо того, чтобы создавать новый.

Page 48: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Use Object Poolsvar soundPool = [], activeSounds = [];var sound = getSound();

releaseSound(sound);sound = null;

function getSound() { var sound; if (soundPool.length) { sound = soundPool.pop();

sound.reset(); // important!!! } else { sound = new Sound(); } return activeSounds.push(sound);}

function releaseSound(sound) { var index = activeSounds.indexOf(soundPool); if (index > -1) { activeSounds.splice(index, 1); }}

Sunday, September 29, 13

Cоздание нового объекта напрямую связанно с процессом выделения памяти, что приближает нас к срабатыванию GC.

Этот паттерн нашел большое применение среди разработчиков игр, где часто приходиться иметь дело с очень большим кол-вом объектов при довольно ограниченном кол-ве памяти.

Единственное о чем не стоит забывать при использовании такого подхода, что ранее использованный объект нужно вернуть в исходное состояние.

Page 49: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Premature optimization is the root of all evil

Sunday, September 29, 13

Но, даже зная все это, никогда не стоит забывать, что не надо заниматься оптимизацией ради оптимизации. Ей стоит заниматься только тогда, когда в конечном итоге от этого выигрывает пользователь.

И в этой связи я хотел бы рассказать вам об истории одной оптимизации, которая имела место прямо перед запуском новой версии SoundCloud в продакшен.

Page 50: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Umm yeah, comments

Sunday, September 29, 13

Это история о способе отображения комментариев поверх waveform'ы -- это своего рода визитная карточка SoundCloud'а.

Page 51: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Many men, many minds

"SoundCloud users comments attached to songs are iconic and life changing"

Sunday, September 29, 13

Кто-то не может представить SoundCloud без этого элемента.

Page 52: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

When N gets big

2600 comments

2450 comments

Sunday, September 29, 13

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

Page 53: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

When N gets big

Template-based approach

• based on ListView component

• list item is 4 nodes: li > a > img + span

• 3 sounds: 4080 * 4 = 16,320 nodes

• 272,000 nodes per stream (50 sounds)

Sunday, September 29, 13

Мы понимали, что с таким решением в продакшен идти нельзя. Но в тоже самое время причина этой медленности была очевидна и лежала на поверхности -- cтандартный, основанный на классиеческом шаблоне способ отрисовки waveform'ы и комментрариев приводил к большому количеству DOM элементов, что и служило причиной медленного скролирования.

Page 54: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

New waveform

Sunday, September 29, 13

ну а после появления еще одного требования -- сделать вейвформу абсолютно гибкой, для того чтобы вписать ее в произвольный фон -- стало ясно

Page 55: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

yes we canvas!

Sunday, September 29, 13

что пришло время портировать отрисовку вейформы и комментариев на элемент canvas.и тем самым попробовать решить две проблемы - максимально минимизировать число DOM элементов (в иделе в одному) - сделать waveform’у более гибкой

Page 56: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Old waveform

Sunday, September 29, 13

Стоит заметить, что изображение с которым нам приходилось работатать раньше, это всего лишь маска, которая накладывалась поверх нужного фона.

Page 57: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Old waveform

Sunday, September 29, 13

Стоит заметить, что изображение с которым нам приходилось работатать раньше, это всего лишь маска, которая накладывалась поверх нужного фона.

Page 58: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Solution: Canvas!Problem:

Data!

Sunday, September 29, 13

Поэтому первой проблемой, с которой мы столкнулись, было отсутствие пиксельных данных, которые можно использовать для рисования waveform’ы на сanvas.

Page 59: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Solution: Images as data source!

Problem:Performance!

Sunday, September 29, 13

Решение было тривиально -- использовать пискельные данные исходного негативного изображения. Мы решили просто находить положение первого непрозрачного пикселя для каждой из частот, чтобы воссоздать нужный нам образ, но здесь нас поджидали большие проблемы по производительности.

Page 60: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Gradual improvements

• Most efficient detection algorithm

• Cached scaled image

• Typed Arrays

• WebWorkers (Chromium Issue #51171)

Sunday, September 29, 13

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

И мы уже было почти сдались, пока не решили зайти к этой проблеме с другой стороны

Page 61: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Think about what’s important

• Moved calculation to the back end

• Ported algorithm to Go

• Front end recieves JSON, everyone happy

Sunday, September 29, 13

а именно -- перенести всю логику анализа пиксельных данных на backend, портировав алгоритм обнаружения грани или первого непрозрачного пикселя на Go. В итоге Frontend просто получал готовый JSON, удобный для того, что отрисовать waveform’у в произвольном виде.

Page 62: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Before: 1352msAfter: 10ms

Sunday, September 29, 13

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

Оптмизировав скорость скроллирования с помощью отрисовки waveform'ы в canvas, мы невооруженным взглядом почувстовали улчшение.

Page 63: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Is it smooth enough?

Sunday, September 29, 13

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

Page 64: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Telemetry

$ cd $CHROMIUM_SOURCE/tools/perf/page-sets$ cat soundcloud.json{R "description" : "Is it smooth enough?",R "smoothness" : { action : "scroll" },R "pages"RR : [R R { "url" : "https://soundcloud.com/akovalev" }R ]}

$ ../run-measurement smoothness soundcloud.json -o result.csv

Telemetry is a Python wrapper around of the DevTools Remote Debugging Protocol

Sunday, September 29, 13

Telemetry - это фреймворк для кроссплатформенного тестирования производительности в Chrome браузере. По сути это обертка над удаленным протоколом Chromium DevTools, написанная на Python. Предоставляет набор готовых бенчмарков, используемых командой разработчиков Chromium для тестирования производительноси отрисовки интерфейса. Так же позволяет реализовать механизм залогирования на сайт, что для тестирования некоторых случаев очень полезно.

Тесты описываются в json формате, где указывается адрес тестируемой страницы и действие, которое будет выполнено. Запуск осуществляется из коммнадной строки. Результат сохраняется в текстовый файл в формате CSV, который не сложно распарсить и отобразить в виде графика.

Page 65: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Telemetry

We're particularly interested in:

• mean_frame_time• dropped_percent• jank_count

Sunday, September 29, 13

_Среди предоставляемых метрик есть ряд очень полезных и наиболее интересных с точки зрения анализа поведения интерфейса. Это__mean_frame_time - среднее значение fps__dropped_percent - кол-во неудачных фреймов в процентах_ _jank_count - и в абсолютной величине_

Telemetry - это очень интересный и полезный инструмент, полный потенциал которого пока не раскрыт. Очень обидно, что на его тему пока очень мало материалов, и что он незаслуженно обделен вниманием веб разработчиков, т.к. он предоставляет широкий арсенал для анализа производительности вашего интерфейса.

Насколько мне известно, кроме SoundCloud это инструмент используется в проекте Adobe Topcoat, которые замеряет производительность веб компонент.

Page 66: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Ship it!

Sunday, September 29, 13

_В самом начале доклада я уже сказал, что наш сайт в продакшене является абсолютно статическим. Превращение проекта в абсолютно статический сайт происходит во время сборки, о который я и хочу сейчас рассказать чуть-чуть подробно. Этап сборки абсолютно автоматизирован и не требует ручной поддержки, что позволяет нам производить релизы по несколько раз в день безо всяких проблем. Особенно когда под рукой у нас есть такой замечательный инструмент как Bazooka_

Page 67: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Bazooka (An In House Heroku)

$ git push bazooka stableremote: change-set accepted...remote: building......remote: build finished

Bazooka is a deployment solution for 12 factors apps - http://12factor.net/More details on bazooka - goo.gl/pfSTAf

Sunday, September 29, 13

_В двух словах, Bazooka это наш внутренний аналог Heroku, с помощью которого деплоятся все сервисы SoundCloud. Делается это одной командой, в результате которой проект собирается и раскатывается по нужному количеству хостов._

Page 68: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Structure of Trombone project

app!"" lib!"" models!"" collections!"" layouts!"" views!"" vendor!"" application.js#"" index.html

var coreDeps = getDeps('application.js'), layoutSeeds = getLayoutSeeds('layouts');

writePackage(coreDeps);

layoutSeeds.forEach(function(layoutSeed) { var layoutDeps = getLayoutDeps(layoutSeed); writePackage(layoutDeps);});

function getLayoutDeps(layoutSeed) { var layoutDeps = getDeps(layoutSeed); return _.difference(layoutDeps, coreDeps)}

$ trombone build

Sunday, September 29, 13

_А что же происходит в процессе самой сборки?_

_Я уже упоминал Uglify.js, который мы используем, к примеру, для компиляции CommonJs/CSS файлов в AMD модули и для нахождения утечек памяти. Но наибольшую пользу этот инструмент приносит нам на этапе сборки проекта. Благодаря Uglify.js наш build script обладает огромной гибкостью, в него можно добавлять и реализовать почти все что угодно._

_Вот так вот выглядит структура любого Trombone проекта. Весь проект разбит на независимые модули. Входной точкой для приложения является файл application.js, с анализа которого все и начинается. Build script строит AST деревро этого файла, рекурсивно находит всего его зависимости, и разбивает их на 3 проектных сборки: вендорные файлы, шаблоны, и все остальное. _

Page 69: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

SOUNDCLOUD

Building

app!"" lib!"" models!"" collections!"" layouts!"" views!"" vendor!"" application.js#"" index.html

public!"" layouts$   !"" home-f3c2-842c8906.js$   !"" listen-2ED1-abef2203.js$   #"" stream-KJsr-540a7923.js#"" core !"" sc-wfJV-abef2203.js !"" vn-iAHt-e1684d31.js #"" tm-idSh-540a7923.js

$ trombone build

Sunday, September 29, 13

Затем аналогичная процедура проделыватеся для каждой из страниц сайта: собираются зависимости для каждой из страниц, за вычетом базовых, которые уже были включены в проектную сборку, в результате чего и создается постраничная сборка.

Page 70: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

var client_id = __ENV__ === ‘prod’ ? ‘abc123’ : ‘def456’;if (__DEBUG_MODE__) { console.log(a);}

uglify.ast_mangle(ast, { defines: { __ENV__ : ‘prod’, __DEBUG_MODE__ : false}});uglify.ast_squeeze(ast);

var a = ‘abc123’;

Dead code removal

Sunday, September 29, 13

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

Page 71: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Transform it!

Sunday, September 29, 13

Вообще идея транфсормации кода содержит всебе очень большой потенциал, который особенно уместно применять на пути вашего кода в продакшен.

Page 72: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

AST is easy

• Meaningful code vs Regex magic

• Easy to change "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "soundId" }, "init": { "type": "Literal", "value": 123, "raw": "123" } } ]

var soundId = 123;

Sunday, September 29, 13

_Возможность работать с кодом на уровне AST -- это уровень абстракции, с которым куда удобнее работать, чем с исходным кодом, представленным в виде строки._

_Трансформации, которые вы можете применить к коду на его пути от вашего редактора к браузеру, практически не ограничены, по крайней мере они не ограничены знанием регулярных выражений,

_Особенно, если у вас под рукой Esprima._

Page 73: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Esprima

• ECMAScript parser written in ECMAScript

• Accepts ECMAScriptcode and generates AST

• Meaningful code vs Regex magic

• compatible with Mozilla Parser AST

• Harmony branch

Sunday, September 29, 13

К примеру если вы вооружены парсером Esprima, Это парсер JavaScript, написанный на JavaScript.

Так исторически сложилось, что на текущий момент для анализа кода мы используем Uglify.Js

Но сейчас я бы советовал работать с Esprima, в сторону который мы тоже смотрим. Она более активно развивается и предоставляет более удобный формат дерева, совместимый с Mozilla Parser AST.

Page 74: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Esprima’s best friends

escodegen

escodegenAST Code

estraverse

estraverse.traverse(ast, { enter: function(node, parent) { }, leave: function(node, parent) { }});

SourceMap

Sunday, September 29, 13

Незаменнимым дополнением в работе с Esprima являются две библиотеки, позволяющие - делать обход дерева- и генерировать JS код на основе AST

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

Page 75: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Function instrumentationfunction addLogging(code) { var ast = esprima.parse(code); estraverse.traverse(ast, { enter: function() { if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') { addBeforeCode(code); } } }); return escodegen.generate(ast);}

function addBeforeCode(node) { var name = node.id ? node.id.name : '<anonymous function>', beforeCode = "console.log('Entering " + name + "()')" beforeNodes = esprima.parse(beforeCode).body;

node.body.body = beforeNodes.concat(node.body.body);}

Sunday, September 29, 13

Вот с помощью всего каких-то 15 строчек читаемого кодамы достигли своей цели -- добавили логирование всех функций.

Page 76: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Function instrumentation

FunctionDeclaration

Identifier BlockStatement

Array

Statement Statement...

Sunday, September 29, 13

По сути же мы просто - создали дерево, представляющее исходный код, - видоизменили его, - и сгенерировали на основе нового дерева нужный нам код

Page 77: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Function instrumentation

FunctionDeclaration

Identifier BlockStatement

Array

Statement Statement...Statement

esprima

estraverse

escodegen

Code

AST

Modified AST

Code

Sunday, September 29, 13

По сути же мы просто - создали дерево, представляющее исходный код, - видоизменили его, - и сгенерировали на основе нового дерева нужный нам код

Page 78: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Thanks!

Sunday, September 29, 13

За время доклада мы проследили - как формировались основные решений, сформировавшие архитектуру новой версии SoundCloud’а - какие паттерны мы применяем для ее реализации - какие инструменты и практкики мы используем, чтобы приложение оставалось отзывычивыми на протяжении долгой сессии

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

Page 79: "Добро пожаловать в SoundCloud!". Александр Ковалёв, SoundCloud

Alexander Kovalevsasha[at]soundcloud[dot]com

Sunday, September 29, 13

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