Virtual DOM (День 3)
Nov 20, 2016
5 minutes read

Сегодня хочется сделать хоть какой-то diff, и погонять dbmonster. Думаю что для начала не буду делать match детей вообще, а пропатчу их один в один. Модель данных для DBMonster генерирую каждый раз случайно - получаю примерно 3000 инстансов данных в секунду. Все замеры в этом блоге я делаю на слабеньком MacBook 12” Early 2015, компилирую с -d:release. В браузере все будет медленее (например в 10 раз) и мне нужно в принципе понять стоит ли овчина выделки.

DBMonster

Код готов, и конечно такая генерация DOM совсем не для ручек - выглядит оно вот так:

proc render*(data: Data, builder: var DOMBuilder) =
  builder.openTag(Tag.table) # table
  builder.attr(Attr.class, "table table-striped latest-data")
  builder.openTag(Tag.tbody) # tbody
  for db in data.databases:
    builder.openTag(Tag.tr) # tr

    builder.openTag(Tag.td) # td
    builder.attr(Attr.class, "dbname")
    builder.textString(db.name)
    builder.closeTag() # /td

    let length = db.queries.len
    builder.openTag(Tag.td) # td
    builder.attr(Attr.class, "query-count")
    builder.openTag(Tag.span) #span
    builder.attr(Attr.class, className(db))
    builder.textString($length)
    builder.closeTag() # /span
    builder.closeTag() # /td

    for i in 0..4:
      if i < length:
        let query = db.queries[i]
        builder.openTag(Tag.td) # td
        builder.attr(Attr.class, className(query))
        builder.textString(formatFloat(query.elapsed, ffDecimal, 2))
        builder.openTag(Tag.`div`) # div
        builder.attr(Attr.class, "popover left")
        builder.openTag(Tag.`div`) # div
        builder.attr(Attr.class, "popover-content")
        builder.textString(query.query)
        builder.closeTag() # /div
        builder.openTag(Tag.`div`) # div
        builder.attr(Attr.class, "arrow")
        builder.closeTag() # /div
        builder.closeTag() # /div
        builder.closeTag() # /td
      else:
        builder.openTag(Tag.td) # td
        builder.attr(Attr.class, "Query")
        builder.attr(Attr.width, "40")
        builder.text(" ")
        builder.closeTag() # /td

    builder.closeTag() # /tr

  builder.closeTag() # /tbody
  builder.closeTag() # /table

Немного текущей статистики: дервяха занимает 325К в памяти включая 25К нестатичных строчек (текстовые ноды). Генерирую 1000 таких деревях в секунду и похоже уже подхожу к пределу (если замедлится в 10 раз в браузере - это будет 10ms на деревяху, а нам еще делать diff). Сейчас подстрахуюсь и посмотрю как все это будет работать в браузере, а пока надо пообедать.

Скорость

Начинаю напрягаться. Во первых пришлось отключить Nim GC - просто падало. Без GC моя node.js v4.1.1 (не знаю пока насколько она стара) показала 7.2 ms на итерацию (а значит я уже приближаюсь к установленному мной пределу в 160-ю секунды). Пока не знаю сколько из этих миллисекунд занимает генерация модели. Сафари порадовал - 5.5 ms на итерацию. Скрещиваю пальцы и пробую Chrome (54.0.2840.98) - 7-8 ms. Ебаный в рот, думаю под это дело скачать FireFox. Chrome 57.0.2926.0 canary - 7-8 ms. Тем временем выясняю что модель генерируется половину времени (3.5ms на V8 и 2.3ms Safari). Столько же генерирую деревяху.

Скачал Firefox 50.0 - ведет себя как полная жопа - подвисает и 15ms на итерацию. Либо я его не умею готовить, либо с Firefox мне не по пути… Вообще ситуация с FireFox странная - прогнал перформанс тесты asm.js http://kripken.github.io/Massive - Firefox у меня показывает гораздо лучшие результаты по сравнению с Chrome. Может в моем случае, и поскольку мой скрипт пускается сразу просле загрузки страницы Firefox “не успел” включить свой JIT, AOT, или чо он там включает. Как бы там ни было, и пока у меня остается теоретическая возможность выдать 60 FPS на Safari и Chrome, я продолжу. Остается за 8ms сделать diff, применить patch, и дать браузеру возможность все это дело отрисовать :). Звучит unreal, но буду пробовать.

Баг (или Data alignment is still important)

Сделал печать рендеренного HTML - надо же постореть наконец что там генерится. И конечно не обошлось без багов :). Nim (умница) делает enum длины достаточной для того чтобы хранить все значения. Я пока использую enum для тегов и аттрибутов и при их записи в выхлоп писал один байт (в башке я держал что будет int). В результате все перекособочилось, но радость не в этом - дело в том что из за бага я читал unaligned data. Где то мне попадалась инфа что современным процам плевать - но у меня проц видимо старый. Нативный dbmonster теперь рендерит модель и деревяху за 0.9ms. NodeJS за 6.2ms. Мачо Safari за 4.2 :) Да, кстати бага не повлияла на количество памяти которое жрет деревяха (размер ноды я считал сам).

Заход со стороны браузера

До сих пор я не трогал непоянтный мне JavaScript, но руки вдруг зачесались увидить что-нибудь в браузере. Построить DOM по моей деревяхе будет тривиальной задачей, но есть варианты в интеграции с JavaScript миром. Для начала попробую самое простое - читать asm.js heap из JS и строить DOM. Должно быть не намного сложнее простой его распечатки:

proc toHtml*(e: Element, output: var string, level: int) =
  output.add spaces(level * 2)
  output.add '<'
  output.add $Tag(e.tag)
  let attrs = e.attrs
  for i in 0..< e.nAttrs:
    output.add ' '
    output.add $Attr(attrs[i].attr)
    output.add "=\""
    output.add attrs[i].value
    output.add '"'
  if e.nKids == 0:
    output.add "/>\n"
  else:
    output.add ">\n"
    let kids = e.kids
    for i in 0..< e.nKids:
      toHtml(kids[i], output, level + 1)
    output.add spaces(level * 2)
    output.add "</"
    output.add $Tag(e.tag)
    output.add ">\n"

Выглядить это будет примерно так: добавляем библиотку к emscripten runtime:

mergeInto(LibraryManager.library, {
  JSrender: function(p) {
    p = p|0
    console.log("Hello from emscripten: ", HEAP32[((p)>>2)])
    p += 4
    console.log("Hello from emscripten: ", HEAP32[((p)>>2)])    
  }
})

Из nim все просто:

proc JSrender(p: ptr int) {.importc.}
var x = [42, 777, 100]
JSrender(cast[ptr int](addr x))

Ебать мой хуй сука, Карл

Думал перед сном накидать рендеринг на JS - но хуй то там. Порт toHtml в тупую не работает - сносит крышу на некотором уровне рекурсии. Я уж подумал почитать как там у JavaScript дела с рекурсией (но по всем понятиям должно быть хорошо), и тут понимаю что портится переменная цикла:

  function renderElement(p) {
    ...
        for (i=0; i<kids; i++) {
          renderElement(HEAP32[((p)>>2)]);
          p += 4;
        }
    ...
  }

Еб вашу мать - эта i оказывается глобальная. Исправил на for (var i=0; i<kids; i++) - все хорошо. Буду остерегаться. А еще говорили программируя на С можно легко выстрелить себе в ногу - тут с JS можно яйца себе в два счета отстрелить.

Результаты под тегом day-3 https://github.com/platoff/faxma/tree/day-3.


Назад, к записям


comments powered by Disqus