Здравствуйте, меня зовут Дмитрий Карловский и я.. тот ещё гурман. Мне нравится готовить изысканные блюда, которые элегантно и просто решают привычные уже набившие оскомину проблемы. Можно долго рассказывать о преимуществах тех или иных подходов, удешевлении поддержки, ускорении разработки, упрощения отладки, но всё это остаётся достаточно субъективными оценками, над которыми нужно размышлять. Поэтому рано или поздно (но как правило преждевременно), всё обсуждение скатывается к более-менее измеримым величинам - скорости работы, скорости загрузки и прочим скоростям. И мало того, что нужно сделать несколько реализаций на разных технологиях, чтобы было что сравнивать, так ещё и не плохо было бы нарисовать интерфейс с понятной человеку выдачей результатов. А всё это - время, которого всегда не хватает, особенно, если делать хорошо.
Чтобы упростить разработку бенчмарков мы выделили общую их часть в отдельное приложение, которое рисует весь интерфейс от выбора тестируемых вариантов до наглядного представления результатов, а вариативная часть подключается извне и реализует довольно простой интерфейс. Всё "накликанное" состояние сохраняется в ссылке, так что им легко поделиться с другими гурманами. Кроме того, поддерживается локализация и сортировка результатов по разным критериям - настоящий пир для всех любителей быстрой еды.
Далее вы узнаете:
Как замутить свой бенчмарк, практически не прилагая усилий.
Как запилить что-то по сложнее, с загрузкой целых приложений в отдельных фреймах.
Как устроен $mol_app_bench изнутри.
Как теперь жить-то со всем этим.
Холостяцкий завтрак
Допустим, мы хотим узнать сколько стоит добавление в DOM различных HTML элементов. Например, вы знали, что в Хроме элемент "Q" в 5 раз тяжелее чем "BLOCKQUOTE", а элемент "IFRAME" в 20 раз тяжелее чем "OBJECT"? Чтобы выяснить эти и другие интересные факты, мы и реализуем этот несложный бенчмарк.
Весь бенчмарк - это просто html страница, которую вы можете выложить на любом сайте. $mol_app_bench откроет её во фрейме и будет взаимодействовать с ней с помощью сообщений. Каркас страницы достаточно тривиален:
<!doctype html>
<meta charset="utf-8" />
<style>
* {
/* чтобы все добавляемые нами элементы накладывались друг на друга и не тратили время на позиционирование */
position: absolute;
}
</style>
<script>
// сюда будем помещать все наши скрипты
</script>
Следует заранее задать сколько мы планируем выводить элементов, это число нам пригодится в дальнейшем:
var count = 500
Если бенчмарк будет открыт напрямую, а не через $mol_app_bench, то просто редиректим на $mol_app_bench с указанием открыть текущий бенчмарк:
Общение бенчмарка и интерфейса происходит через простейший RPC вида [ 'имя функции' , ...[ параметры ] ]. Для простоты, будем просто вызывать соответствующую функцию с соответствующими параметрами:
Замер у нас тут всего один - fill, а варианты реализаций вообще не указаны, так как их нужно сгенерировать программно из списка имён элементов. Вот, кстати, и он:
Получив мета-информацию, $mol_app_bench нарисует меню реализаций с возможностью выбора, какие из них требуется замерить. Для каждой реализации он последовательно вызовет методы, соответствующие именам шагов с передачей в качестве аргумента имя варианта реализации. В нашем случае шаг всего один:
function fill( sample ) {
var body = document.body
while( body.firstChild ) {
body.removeChild( body.firstChild )
}
requestAnimationFrame( function() {
var start = Date.now()
var frag = document.createDocumentFragment()
for( var i = 0 ; i < count ; ++ i ) {
frag.appendChild( document.createElement( sample ) )
}
body.appendChild( frag )
setImmediate( function() {
done( Date.now() - start + ' ms' )
} )
} )
}
Тут мы первым делом очищаем тело документа, ждём следующего фрейма анимации, чтобы браузер закончил все свои дела, после чего создаём нужное число элементов и добавляем их все разом в DOM. Часть операций браузер выполняет асинхронно, поэтому через setImmediate мы дожидаемся, пока он всё закончит и возвращаем замеренное время.
Однако, setImmediate - сравнительно новое API, которое реализовано ещё не во всех браузерах, поэтому реализуем её простейший вариант через уже используемый нами postMessage:
var setImmediate_task
function setImmediate( task ) {
setImmediate_task = task
postMessage( [ 'setImmediate_task' ] , '*' )
}
Допустим, мы хотим узнать какой же JS фреймворк самый быстрый. Для этого нам надо реализовать одно и то же приложение на них всех и прогнать на каждом из них одни и те же сценарии использования, замеряя время их завершения. Фреймворков в JS настолько много, что разбираться в каждом из них и реализовывать даже простейшее приложение, - потребует не одного человекомесяца. Поэтому мы возьмём уже готовые приложения из проекта ToDoMVC. Для этого мы форкнем его, добавим наш бенчмарк и выложим на github.io (Соответствующий запрос на слияние всё ещё ждёт своего звёздного часа).
Бенчмарк наш будет открывать реализации в отдельном фрейме:
<iframe id="sandbox"></iframe>
Шага замеров у нас будет 3:
Время открытия приложения
Время последовательного создания пачки задач
Время последовательного удаления всех задач
var metaData = {
title : {
'en' : 'ToDoMVC workflow benchmark' ,
'ru' : 'ToDoMVC - производительность работы' ,
} ,
descr : {
'en' : 'Sample applications is [ToDOMVC](todomvc.com) implementations. Benchmark creates **' + count + ' tasks** in sequence and then removes them.' ,
'ru' : 'Варианты приложений являются реализациями [ToDOMVC](todomvc.com) приложения для управления списком дел. В тесте замеряется время последовательного создания **' + count + ' задач** с последующим их удалением.' ,
} ,
samples : {
} ,
steps : {
'start' : {
title : {
'en' : 'Load and init' ,
'ru' : 'Загрузка и запуск' ,
} ,
} ,
'fill' : {
title : {
'en' : 'Tasks creating' ,
'ru' : 'Создание задач' ,
} ,
} ,
'clear' : {
title : {
'en' : 'Tasks removing' ,
'ru' : 'Удаление задач' ,
} ,
} ,
} ,
}
Информация о вариантах реализаций находится в файле learn.json, который мы не парясь синхронно загрузим и дополним мета-информацию:
Для добавления задачи, мы симулируем все события, что происходят в браузере, когда пользователь вводит название и нажимает "ENTER":
function fill( sample ) {
var adder = sandbox.contentDocument.querySelector( selector.adder )
var adderForm = sandbox.contentDocument.querySelector( selector.adderForm )
var i = 1
var start = Date.now()
step()
function step() {
adder.value = 'Something to do ' + i
adder.dispatchEvent( new Event( 'input' , { bubbles : true } ) )
adder.dispatchEvent( new Event( 'change' , { bubbles : true } ) )
var event = new Event( 'keydown' , { bubbles : true } )
event.keyCode = 13
event.which = 13
event.key = 'Enter'
adder.dispatchEvent( event )
var event = new Event( 'keypress' , { bubbles : true } )
event.keyCode = 13
event.which = 13
event.key = 'Enter'
adder.dispatchEvent( event )
var event = new Event( 'compositionend' , { bubbles : true } )
event.keyCode = 13
event.which = 13
event.key = 'Enter'
adder.dispatchEvent( event )
var event = new Event( 'keyup' , { bubbles : true } )
event.keyCode = 13
event.which = 13
event.key = 'Enter'
adder.dispatchEvent( event )
var event = new Event( 'blur' , { bubbles : true } )
adder.dispatchEvent( event )
if( adderForm ) {
var event = new Event( 'submit' , { bubbles : true } )
event.keyCode = 13
event.which = 13
event.key = 'Enter'
adderForm.dispatchEvent( event )
}
if( ++i <= count ) setImmediate( step )
else done( Date.now() - start + ' ms' )
}
}
Столько всяких событий мы кидаем, так как разные фреймворки реагируют на разные события для реализации одной и той же функциональности. Набор и конфигурацию событий пришлось подбирать вручную просматривая реализации, которые косячили с бенчмарком. Сейчас большинство реализаций отрабатывают корректно, но некоторые всё ещё косячат, что видно невооружённым глазом, когда они запускаются. Пул-реквесты от сообщества, исправляющие работу бенчмарка с этими реализациями, были бы очень кстати ;-)
И последний шаг - последовательное удаление всех задач. Тут всё просто:
Сперва наметим структуру нашего приложения. Оно будет состоять из 2 панелей: основной с бенчмарком и результатами; и дополнительной с меню выбора реализаций:
$mol_app_bench $mol_view
sub /
<= Addon_page $mol_page
<= Main_page $mol_page
Для дополнительной зададим заголовок и собственно список реализаций, который в дальнейшем сформируем программно:
Addon_page $mol_page
title <= addon_title @ \Samples
body /
<= Menu $mol_list
rows <= menu_options /
Пусть вас не смущает, что мы зашили английский текст прямо тут. Благодаря собачке, при сборке он будет извлечён в файл с англоязычными строками.
Рядом, зададим, что каждый пункт меню будет ни чем иным, как чекбоксом, позволяющим включить или выключить замеры соответствующей реализации:
Как можно заметить, у нас тут применяются односторонний и двусторонний биндинги для взаимодействия со вложенными компонентами. Имя справа от стрелочки - имя свойства нашего компонента (в данном случае - компонента приложения), а слева - свойство вложенного компонента.
В основной панели мы выведем информационный блок с описанием и результатами, а также песочницу, в которую будут загружаться приложения:
Main_page $mol_page
title <= title -
body /
<= Inform $mol_view
<= Sandbox $mol_view
dom_name \iframe
Как можно заметить, заголовком этой панели будет заголовок всего приложения, который также будет выводиться и в названии вкладки браузера.
В информационном блоке выведем описание через компонент визуализации markdown, а результаты выведем через компонент вывода сравнительных таблиц:
Inform $mol_view sub /
<= Descr $mol_text
text <= description \
<= Result $mol_bench
result <= result null
col_head_label!id /
<= result_col_title!id /
col_sort?val <=> result_col_sort?val \
Последний штрих, зададим заголовок для колонки с названиями реализаций в выводимой сравнительной таблице, которым мы воспользуемся чуть позже:
Теперь можно браться за поведение, чтобы добавить которое, достаточно расширить автоматически генерируемый из описания структуры, класс:
namespace $.$mol {
export class $mol_app_bench extends $.$mol_app_bench {
// тут будем помещать объявления новых и переопределения уже заданных свойств
}
}
Первым делом нам нужно получить из адресной строки ссылку на бенчмарк:
Как видно, если бенчмарк не задан, то открывается бенчмарк вывода списков разными фреймворками, который лежит рядышком.
Далее, нам потребуется ссылка на песочницу, да не просто ссылка, а с уже загруженным туда бенчмарком, поэтому записываем значение свойства мы лишь дождавшись события 'load' во фрейме:
Код реализации свойства sandbox получился асинхронным, но эта асинхронность изолирована от окружающего кода, так что внешний интерфейс этого свойства остаётся по прежнему синхронным благодаря магии реактивности.
Наибольшая сложность сокрыта в свойстве "результат вызова удалённой процедуры". Помимо того, что она тоже инкапсулирует в себе асинхронность, у неё есть ещё одно ограничение - единовременно может быть вызвано не более одной процедуры, так как песочница всего одна, да и запускать несколько бенчмарков одновременно - плохая примета. Поэтому, текущая исполняемая команда записывается в свойство command_current, а command_result сначала проверяет, что сейчас не исполняется другая команда, и если исполняется, то сначала дожидается её завершения.
Знатоки многопоточности могут узнать тут примитив синхронизации "mutex", реализованный через механизм "compare and swap". И это не спроста, ведь $mol_atom по умолчанию пытается распараллелить задачи, если это возможно. Разобраться в этом коде может быть сложновато без понимания, магии реактивности, так что рекомендую почитать указанную выше статью. Но главное - вся сложность инкапсулирована в этом свойстве и дальнейшая работа будет простой и приятной. Например, получим мета-информацию из бенчмарка:
meta() {
type meta = {
title : { [ lang : string ] : string }
descr : { [ lang : string ] : string }
samples : { [ sample : string ] : {
title : { [ lang : string ] : string }
} }
steps : { [ step : string ] : {
title : { [ lang : string ] : string }
} }
}
return this.command_result< meta >([ 'meta' ])
}
А теперь, получим список всех реализаций, отсортированный по их названиям:
Состояние выбранности реализации будет зависеть от списка выбранных реализаций:
@ $mol_mem_key()
menu_option_checked( sample : string , next? : boolean ) {
if( next === void 0 ) return this.samples().indexOf( sample ) !== -1
if( next ) this.samples( this.samples().concat( sample ) )
else this.samples( this.samples().filter( s => s !== sample ) )
return next
}
Как можно заметить, реализация свойства - это и геттер и сеттер одновременно. Список же выбранных реализаций мы аналогичным образом будем хранить в ссылке:
А теперь пройдёмся по всем выбранным реализациям и сформируем общий результат, который и будет передан в компонент вывода сравнительных таблиц, как было указано в описании структуры приложения:
У компонента $mol_bench мы так же переопределяли свойство col_head_label, куда привязали свойство result_col_title, которое возвращает пустой массив. Давайте переопределим его, чтобы оно возвращало локализованный заголовок колонки:
Кроме того, мы предоставили ему в качестве свойства col_sort наше свойство result_col_sort. Чтобы оно тоже сохранялось в ссылке, переопределим его следующим образом:
Добавить фильтрацию вариантов реализаций (для быстрого поиска интересующих).
Поддержка маленьких экранов (сейчас приложение совсем не рассчитано на маленькие экраны).
Вывод результатов в виде наложенных друг на друга графиков (компактный модуль построения реактивных графиков вот-вот будет готов).
Не тормозите, предлагайте свои идеи что и как можно потестить, чего не хватает описанному тут обобщённому интерфейсу, пробуйте свои рецепты и делитесь ими с сообществом.
https://page.hyoo.ru/#!=5f2rpc_23npk3
Здравствуйте, меня зовут Дмитрий Карловский и я.. тот ещё гурман. Мне нравится готовить изысканные блюда, которые элегантно и просто решают привычные уже набившие оскомину проблемы. Можно долго рассказывать о преимуществах тех или иных подходов, удешевлении поддержки, ускорении разработки, упрощения отладки, но всё это остаётся достаточно субъективными оценками, над которыми нужно размышлять. Поэтому рано или поздно (но как правило преждевременно), всё обсуждение скатывается к более-менее измеримым величинам - скорости работы, скорости загрузки и прочим скоростям. И мало того, что нужно сделать несколько реализаций на разных технологиях, чтобы было что сравнивать, так ещё и не плохо было бы нарисовать интерфейс с понятной человеку выдачей результатов. А всё это - время, которого всегда не хватает, особенно, если делать хорошо.
Чтобы упростить разработку бенчмарков мы выделили общую их часть в отдельное приложение, которое рисует весь интерфейс от выбора тестируемых вариантов до наглядного представления результатов, а вариативная часть подключается извне и реализует довольно простой интерфейс. Всё "накликанное" состояние сохраняется в ссылке, так что им легко поделиться с другими гурманами. Кроме того, поддерживается локализация и сортировка результатов по разным критериям - настоящий пир для всех любителей быстрой еды.
Далее вы узнаете:
Холостяцкий завтрак
Допустим, мы хотим узнать сколько стоит добавление в DOM различных HTML элементов. Например, вы знали, что в Хроме элемент "Q" в 5 раз тяжелее чем "BLOCKQUOTE", а элемент "IFRAME" в 20 раз тяжелее чем "OBJECT"? Чтобы выяснить эти и другие интересные факты, мы и реализуем этот несложный бенчмарк.
Весь бенчмарк - это просто html страница, которую вы можете выложить на любом сайте. $mol_app_bench откроет её во фрейме и будет взаимодействовать с ней с помощью сообщений. Каркас страницы достаточно тривиален:
Следует заранее задать сколько мы планируем выводить элементов, это число нам пригодится в дальнейшем:
Если бенчмарк будет открыт напрямую, а не через $mol_app_bench, то просто редиректим на $mol_app_bench с указанием открыть текущий бенчмарк:
Общение бенчмарка и интерфейса происходит через простейший RPC вида
[ 'имя функции' , ...[ параметры ] ]
. Для простоты, будем просто вызывать соответствующую функцию с соответствующими параметрами:Когда $mol_app_bench запустится, он откроет наш бенчмарк во фрейме и пришлёт нам сообщение
[ 'meta' ]
, поэтому реализуем соответствующую функцию:Ответ на RPC вызов $mol_app_bench ожидает в виде сообщения
[ 'done' , ...[ данные ] ]
, так что тут тоже всё просто:Метод
meta
должен вернуть $mol_app_bench следующую мета-информацию о бенчмарке:Все тексты задаются с указанием языка. В нашем случае, мета информация будет представлена следующим образом:
Замер у нас тут всего один -
fill
, а варианты реализаций вообще не указаны, так как их нужно сгенерировать программно из списка имён элементов. Вот, кстати, и он:Теперь дополним мета-информацию, конфигом вариантов реализаций:
Получив мета-информацию, $mol_app_bench нарисует меню реализаций с возможностью выбора, какие из них требуется замерить. Для каждой реализации он последовательно вызовет методы, соответствующие именам шагов с передачей в качестве аргумента имя варианта реализации. В нашем случае шаг всего один:
Тут мы первым делом очищаем тело документа, ждём следующего фрейма анимации, чтобы браузер закончил все свои дела, после чего создаём нужное число элементов и добавляем их все разом в DOM. Часть операций браузер выполняет асинхронно, поэтому через
setImmediate
мы дожидаемся, пока он всё закончит и возвращаем замеренное время.Однако, setImmediate - сравнительно новое API, которое реализовано ещё не во всех браузерах, поэтому реализуем её простейший вариант через уже используемый нами postMessage:
Вот, собственно, и всё:
Ужин для всей семьи
Допустим, мы хотим узнать какой же JS фреймворк самый быстрый. Для этого нам надо реализовать одно и то же приложение на них всех и прогнать на каждом из них одни и те же сценарии использования, замеряя время их завершения. Фреймворков в JS настолько много, что разбираться в каждом из них и реализовывать даже простейшее приложение, - потребует не одного человекомесяца. Поэтому мы возьмём уже готовые приложения из проекта ToDoMVC. Для этого мы форкнем его, добавим наш бенчмарк и выложим на github.io (Соответствующий запрос на слияние всё ещё ждёт своего звёздного часа).
Бенчмарк наш будет открывать реализации в отдельном фрейме:
Шага замеров у нас будет 3:
Информация о вариантах реализаций находится в файле learn.json, который мы не парясь синхронно загрузим и дополним мета-информацию:
Прежде, чем браться за реализацию замеров, мы запомним ссылку на фрейм и запишем селекторы для поиска нужных элементов в приложениях:
Старт приложения мы детектируем по появлению элемента для добавления задач:
Для добавления задачи, мы симулируем все события, что происходят в браузере, когда пользователь вводит название и нажимает "ENTER":
Столько всяких событий мы кидаем, так как разные фреймворки реагируют на разные события для реализации одной и той же функциональности. Набор и конфигурацию событий пришлось подбирать вручную просматривая реализации, которые косячили с бенчмарком. Сейчас большинство реализаций отрабатывают корректно, но некоторые всё ещё косячат, что видно невооружённым глазом, когда они запускаются. Пул-реквесты от сообщества, исправляющие работу бенчмарка с этими реализациями, были бы очень кстати ;-)
И последний шаг - последовательное удаление всех задач. Тут всё просто:
Вот и всё, получилось не сильно-то и сложнее, не правда ли?
Самая мякотка $mol_app_bench
Сперва наметим структуру нашего приложения. Оно будет состоять из 2 панелей: основной с бенчмарком и результатами; и дополнительной с меню выбора реализаций:
Для дополнительной зададим заголовок и собственно список реализаций, который в дальнейшем сформируем программно:
Пусть вас не смущает, что мы зашили английский текст прямо тут. Благодаря собачке, при сборке он будет извлечён в файл с англоязычными строками.
Рядом, зададим, что каждый пункт меню будет ни чем иным, как чекбоксом, позволяющим включить или выключить замеры соответствующей реализации:
Как можно заметить, у нас тут применяются односторонний и двусторонний биндинги для взаимодействия со вложенными компонентами. Имя справа от стрелочки - имя свойства нашего компонента (в данном случае - компонента приложения), а слева - свойство вложенного компонента.
В основной панели мы выведем информационный блок с описанием и результатами, а также песочницу, в которую будут загружаться приложения:
Как можно заметить, заголовком этой панели будет заголовок всего приложения, который также будет выводиться и в названии вкладки браузера.
В информационном блоке выведем описание через компонент визуализации markdown, а результаты выведем через компонент вывода сравнительных таблиц:
Последний штрих, зададим заголовок для колонки с названиями реализаций в выводимой сравнительной таблице, которым мы воспользуемся чуть позже:
Объединим все эти кусочки кода и получим полное описание структуры приложения.
Теперь можно браться за поведение, чтобы добавить которое, достаточно расширить автоматически генерируемый из описания структуры, класс:
Первым делом нам нужно получить из адресной строки ссылку на бенчмарк:
Как видно, если бенчмарк не задан, то открывается бенчмарк вывода списков разными фреймворками, который лежит рядышком.
Далее, нам потребуется ссылка на песочницу, да не просто ссылка, а с уже загруженным туда бенчмарком, поэтому записываем значение свойства мы лишь дождавшись события 'load' во фрейме:
Код реализации свойства
sandbox
получился асинхронным, но эта асинхронность изолирована от окружающего кода, так что внешний интерфейс этого свойства остаётся по прежнему синхронным благодаря магии реактивности.Наибольшая сложность сокрыта в свойстве "результат вызова удалённой процедуры". Помимо того, что она тоже инкапсулирует в себе асинхронность, у неё есть ещё одно ограничение - единовременно может быть вызвано не более одной процедуры, так как песочница всего одна, да и запускать несколько бенчмарков одновременно - плохая примета. Поэтому, текущая исполняемая команда записывается в свойство
command_current
, аcommand_result
сначала проверяет, что сейчас не исполняется другая команда, и если исполняется, то сначала дожидается её завершения.Знатоки многопоточности могут узнать тут примитив синхронизации "mutex", реализованный через механизм "compare and swap". И это не спроста, ведь $mol_atom по умолчанию пытается распараллелить задачи, если это возможно. Разобраться в этом коде может быть сложновато без понимания, магии реактивности, так что рекомендую почитать указанную выше статью. Но главное - вся сложность инкапсулирована в этом свойстве и дальнейшая работа будет простой и приятной. Например, получим мета-информацию из бенчмарка:
А теперь, получим список всех реализаций, отсортированный по их названиям:
Названия реализаций, формируются на основе текущего языка:
Название и описание бенчмарка, получаем аналогичным образом:
Пришло время сформировать и список пунктов меню:
Состояние выбранности реализации будет зависеть от списка выбранных реализаций:
Как можно заметить, реализация свойства - это и геттер и сеттер одновременно. Список же выбранных реализаций мы аналогичным образом будем хранить в ссылке:
Прежде чем переходить к собственно замерам, выпишем все шаги, которые собираемся пройти:
Пройдёмся последовательно по всем шагам для каждой реализации:
А теперь пройдёмся по всем выбранным реализациям и сформируем общий результат, который и будет передан в компонент вывода сравнительных таблиц, как было указано в описании структуры приложения:
У компонента
$mol_bench
мы так же переопределяли свойствоcol_head_label
, куда привязали свойствоresult_col_title
, которое возвращает пустой массив. Давайте переопределим его, чтобы оно возвращало локализованный заголовок колонки:Кроме того, мы предоставили ему в качестве свойства
col_sort
наше свойствоresult_col_sort
. Чтобы оно тоже сохранялось в ссылке, переопределим его следующим образом:С поведением разобрались, теперь можно украсить блюдо стилями по автоматически сгенерированным BEM-атрибутам и можно подавать к столу.
Приятного аппетита
Документацию по написанию бенчмарков и ссылки на уже реализованные, вы можете найти на странице $mol_app_bench.
Несколько примеров:
В планах:
Не тормозите, предлагайте свои идеи что и как можно потестить, чего не хватает описанному тут обобщённому интерфейсу, пробуйте свои рецепты и делитесь ими с сообществом.