вторник, 25 июня 2019 г.

Похоронил GoRelic

       Сегодня похоронил один из своих open source проектов - GoRelic. Когда я его начал NewRelic был стандартом де факто для мониторинга, а никакой поддержки для приложений на Golang у них не было. Когда NewRelic анонсировал открытие API для плагинописателей - я быстренько налабал этот проект.  Но как показал опыт писать плагины для закрытых систем (одной из которых без сомнения является NewRelic) это бесперспективное занятие. 
        Их API тогда был убогим и не покрывал и 10% функциональности доступной их собственным плагинам. А когда он стал нормальным - появился официальный Golang агент.  Сегодня я выложил последнюю, 7-ю по счету версию. Больше обновлений не будет.          

пятница, 21 июня 2019 г.

Секретное поле в PerformanceNodeTiming

     Пока разбирался с PerformanceNodeTiming обнаружил там недокументированное поле environment, содержащее время когда закончилась инициализация структуры environment (относительно времени старта процесса). В очередной раз наконтрибьютил в ноду - https://github.com/nodejs/node/pull/28280  Этот pull request прошел в мастер довольно быстро. Мой предыдущий pull request c новым методом в модуле v8 до сих пор ждет мерджа.

вторник, 18 июня 2019 г.

PerformanceTiming API

      PerformanceTimingAPI - довольно занятное API, появившееся еще в версии 8.5 но почему-то обделенное вниманием js программистов. К примеру это API позволяет относительно легко (без погружения в C++ код) замониторить GC, ну и много чего еще.  Возможно причина тому - не очень хорошая документация, да и сам API нельзя назвать интуитивно понятным. Я считаю себя в общем-то не глупым парнем, но я разобрался в нем не с первого раза. Поэтому я и решил потратить время на написание этой статьи.
В основе интерфейса PerformanceTimingAPI (или perf_hooks как оно называется в исходникахлежит иерархия классов основанная на PerformanceEntry. PerformanceEntry - это нечто что ты можешь измерить. У PerformanceEntry есть поле entryType которое собственно показывает что же ты измерил. Для того чтобы получать PerformanceEntry тебе нужно с помощью PerformanceObserver подписаться на них. 
       Если у PerformanceEntry поле entryType="mark" то мы имеем дело с отметиной на timeline.  startTime = время когда произошло это событие, duration = 0, name = имя события. Единственный способ получить mark - самим создать его с помощью фукнции performance.mark("Start of something")
       Единственный сценарий использования performance mark - создать два performance.mark и измерить время между ними используя performance.measure(measureName, startMark, endMark). Этот вызов возвращает нам еще один PerformanceEntry с типом measure. В общем такая красивая и бестолковая обертка вокруг timestamp. 
    Далее следует PerformanceEntry с типом function. Этот тип PerformanceEntry можно получить если обернуть некоторую функцию в мониторинг (через performance.timerify(fn)). В этом случае каждый раз когда функция вызовется будет создан новый PerformanceEntry с типом function описывающий время выполнения функции. Функция timrify использует кастомные атрибуты Object.defineProperty() что негативно сказывается на скорости работы с объектом.
        PerformanceEntry с типом gc - описывает запуск сборщика мусора. В этом случае поле kind будет указывать на тип сборки- major, minor, incremental и тд. На самом деле PerformanceEntry  с типом gc это отдельный класс в C++ коде - GCPerformanceEntry. По каким-то причинам разработчики решили не экспортировать его в мир JS. 
        PerforamanceEntry с типом http2 предназначены для мониторинга http2 сессий и потоков. Если name = HTTP2Stream то этот PerformanceEntry описывает создание нового http2 stream. Если name = HTTP2Session - то соответсвенно сессии.
      Напоследок расскажу о самом сомнительном на мой взгляд архитектурном решении - PerformanceNodeTiming. Он наследуется от PerformanceEntry но у своего родителя он использует только  два поля - entityType = node и duration. Все остальные поля его собственные - они описывают процесс инициализации Node.js: сколько времени потратили на инициализацию процесса ноды(nodeStart), сколько потратили на инициализацию v8 (v8Start), сколько потратили на инициализацию environment-а(поле так и называется environment и оно почему-то не документировано),  сколько времени потратили на bootstrap (bootstrapComplete), сколько времени потребовалось для запуска event loop (loopStart). 
        В отличии от всех остальных PerformanceEntry, работа с PerformanceNodeTiming ведется  не через PerformanceObserver, а через статическое поле performance.nodeTiming. Если ты подпишешься на PerformanceObserver с типом "node" то ты не получишь ничего.  Второй момент - мне не совсем понятно как мониторить инициализацию воркеров. Ведь у них как минимум свой environment и свой eventLoop который также будет инициализироваться. А если я два воркера запускаю одновременно ? В общем это лишний раз доказывает что singleton это антипаттерн проектирования. 

вторник, 11 июня 2019 г.

libuv internals и как эта библиотека интегрирована в Node.js

       С libuv сложилась такая парадоксальная ситуация - про нее все одновременно знают и не знают. Все знают что это нечто используемое Node.js для работы с сокетами и файловой системой и нечто обладающее магическим event loop. Ну а дальше показания расходятся. Причем даже у "экспертов" выступающих на конференциях. 
        В документации libuv есть довольно красивая и но малоинформативная диаграмма:

На мой взгляд функциональность предоставляемая libuv очень просто и понятно описана вот тут https://github.com/danbev/learning-libuv libuv можно использовать для того чтобы:
  • Работать с сетью (tcp, upd, pipe и тд)
  • Асинхронно работать с файловой системой
  • Асинхронно работать с DNS
  • Работать с нитями
  • Обрабатывать сигналы
  • Работать с таймерами

Это довольно большой список, но на самом деле интерфейс libuv основывается всего на трех концепциях: event loop(uv_loop_t), handle (uv_handle_t), request(uv_request_t):
  • event loop - это то, чем собственно занят поток выполнения. бегаем по кругу, дергаем callback-и. Детальнее опишу ниже.
  • handle  представляет собой некий ресурс. Это может быть обертка вокруг ресурса операционной системы (сокет, файловый дескриптор) или просто нечто вроде таймера умеющее дергать callback   
  • request - это некий запрос. Он может использовать хендлер (запрос на запись в файл) или может не использовать хендлеры.
Также внутри libuv сидит thread pool но я бы рассматривал его как деталь реализации а не часть интерфейса. Thread pool используется для превращения синхронных действий в асинхронные и в настоящее время используется только при работе с файловой системой и DNS. Ну еще мы сами момжем попросить libuv выполнить какую-то работу асинхронно в thread pool а не в основном потоке исполнения.
        Так вот, что же из себя представляет event loop ? На мой взгляд его проще всего описать вот такой диаграммой: 
Все прямоугольнички нарисованные на этой диаграмме детально описаны вот тут - https://github.com/libuv/libuv/blob/v1.x/docs/src/design.rst#the-io-loop
Я же хочу остановится на том как это все интегрировано в Node.js. Практически вся работа с libuv сосредоточена в evn.cc. Начинается все с void Environment::InitializeLibuv() - тут мы инициализируем таймеры, check handles и тд. 
        Теперь собственно посмотрим что из прямоугольничков оригинального event loop'а используется в Node.js:
  • Первое это таймеры. При инициализации Node.js создает один libuv таймер который запускает все JS таймеры. Время срабатывания таймера - минимальное среди всех установленных JS таймеров
  • Idle handlers в мир JavaScript никак не транслируются. Они используются внутри для управления логикой которую libuv использует для рассчета времени которое она должна провести заблокированной в poll вызове
  • Prepare handles - также в мир JavaScript не транслируются. Там стоит системный хендлер который служит для измерения времени которое мы фактически проведем заблокированными в poll вызове
  • Check handles - здесь Node.js также вешает один хэндлер который выполняет все callback-и установленные через setImmediate()
  • Close handlers - также не транслируются в JS. Здесь libuv запускает on close callback-и закрываемых ресурсов (хендлеров). 
В общем как-то так. Вообще по моему скроменому мнению - libuv одна из самых понятно написанных библиотек. Тут довольно интересно организорвано наследование структур - через макросы. Все поля структуры объявляются внутри макроса. Соответсвенно в дочерней структуре сначала идет макрос объявляющий родительские поля, а потом макрос объявляющий собственные поля:







пятница, 7 июня 2019 г.

V8 internals - отслеживание ссылок на объекты в куче V8

       То что спецификацию (как минимум первые версии) для языка JavaScript писали не очень умные люди с лихвой компенсируется мозгами тех кто пишет V8.  Чем больше разбираюсь в проекте - тем больше восхищаюсь этими людьми. И так - как же GC V8 узнает о ссылках на объекты в куче из C++ кода ? 
         Вся работа с объектами размещенными в куче построена на так называемых хендлерах. Так как GC может перемещать объекты размещенные в куче во время сборки мусора прямая работа с этими объектами (по указателю) запрещена. Все обращения только через хендлеры. Этих хендлеров есть несколько типов, все они объявлены в v8.h
  • Local - это хендлер время жизни которого ограничено временем жизни области видимости в которой он объявлен.  Как только мы выходим из области видимости - стэк отматывается и хендлер становится недействительным.
  • Persistent - это хендлер время жизни которого не контролируется областью видимости. Живет до тех пока явно не вызовешь Reset()
  • Global - тоже самое что и Persistent но c поддержкой move семантики
  • Eternal - это хендлер используемый для "вечных" ссылок. То есть ссылок живущих до тех пор пока жив Isolate. Они немного более удобны с точки зрения GC чем Persistent 
  • TracedGlobal - тоже самое что и Global но с подержкой трассировки GC. Для чего он может использоваться честно до конца не разобрался
Вся магия по управлению Local хэндлерами через область видимости реализуюется в классе HandleScope и Isolate.  Инстанс HandleScope обычно создается до создания первого хендлера. Все созданные после этого Local хэндлеры создаются в этой области видимости. HandleScope могут быть вложенными. При выходе за границы области видимости вызывается деструктор HandleScope  в котором происходит вызов HandleScope::CloseScope() который подчищает Local хэндлеры созданные в этой области видимости.  
        Тут собственно и начинается самое интересное. Выясняется что внутри Isolate (экземпляр виртуальной машины V8 со своей кучей и всем остальным) есть список буферов в которых собственно размещаются указатели на объекты размещенные в куче. val_ который хранится в Local хендлере это указатель на этот адрес, размещенный в буфере Isolate. Каждый раз когда мы создаем новый хендлер HandleScope::CreateHandle() выделает в буфере класса Isolate новый указатель который указывает на кучу. А возвращает указатель на этот указатель. Собственно когда нужно собрать мусор, GC пробегает по этому списку буферов заполнгенному указателями на кучу и помечает объекты как используемые. При перемещении объектов GC также меняет указатели если кто-то ссылается на этот объект. В общем получается весьма эффективная система.

четверг, 6 июня 2019 г.

V8 internals - хранение объектов и массивов. Инициализация глобального контекста

       Итак, я продолжаю свои упражнения в археологии. Для начала меня заинтересовал вопрос - вот есть у меня виртуальная машина, а как в ней появляются все эти прекрасные "встроенные функции" ? Собственно в этой статье это довольно подробно объясняется - https://v8.dev/blog/custom-startup-snapshots  Если в кратце то  в папочке tools есть утилита js2c.py которая конвертирует javascript код из папочки lib в бинарный массив который можно напрямую загрузить в V8 Heap.  Таким образом инициализация контекста (объект global) очень сильно ускоряется.
        Что касается хранения объектов в памяти V8 (memory layout) - по этой теме есть много статей, и даже доклад от Fedor Indutnyhttps://www.youtube.com/watch?v=tLyIs_0cUyc но на этот раз меня его доклад не впечатлил. Он прошелся по верхушкам и прорекламировал llnode - node.js плагин для lldb. Большое спасибо Федору за инструмент, но тема memory layout в его докладе была раскрыта на 10% не больше. Лучше всего это объяснено в этой статье - https://v8.dev/blog/fast-properties В ней акцент сделан на хранении свойств, а в следующей статье https://v8.dev/blog/elements-kinds более подробно объяснено хранение элементов массива.   Вообще в статьях очень много полезной информации по оптимизации - как писать код так чтобы V8 его быстрее исполнял. Ведь благодаря создателям JavaScript - одну и ту же вещь там можно написать 20 способами. Вообще мне кажется что люди проектировавшие JavaScript изначально - прямо-таки ненавидили разработчиков компиляторов/интерпретаторов и сделали все чтобы затруднить разработку компиляторов/интерпретаторов для JavaScript. 
В общем обе статьи я бы рекомендовал прочитать каждому JS разработчику стремящемуся писать оптимальный код. Для тех кто не в ладах с английским языком перескажу в кратце:
  • Если вы хотите чтобы доступ к свойствам объекта происходил быстро - явно определяйте все его свойства при создании объекта. Определяйте их в одном и том же порядке(в конструкторе). Не добавляйте новые свойства по ходу использования объекта. Не используйте Object.defineProperty() если у вас нет для этого большой необходимости. 
  • Избегайте полиморфизма. То есть передайвайте в функцию только аргументы одного типа. 
  • Не храните элементы разных типов в одном массиве. Явно приводите их к одному виду.
  • Избегайте разряженных массивов (массивов с пропусками). Сплошные массивы работают намного быстрее
  • Не пытайтесь обращаться к несуществующим элементам массива (требует обращения к prototype)

среда, 5 июня 2019 г.

V8 internals/Кишки V8

       Продолжаю разбираться как внутри устроен V8. Из интересного - доклад про Ignition - интерпретатор JS который V8 использует при первом исполнении JS кода:

При многократном выполнении код компилируется оптимизирующим компилятором TurboFan (с ним пока не разбирался).
    Начал копать memory layout по исходникам. Начал с src/objects.h - там общая иерархия объектов описана.  Если в кратце то V8 имеет следующие типы объектов(OBJECT_TYPE_LIST):

  • Smi - immediate small integer 
  • LayoutDescriptor
  • HeapObject - superclass for everything allocated in the heap
  • Primitive
  • Number
  • Numeric
В V8 используется хитрая схема тэгирования указателей (tagging scheme) -  так как все что они читают/пишут из памяти выровненно по границе машинного слова (4/8 байт) то младшие биты указателя всегда должны быть равны нулю. Они используют их для тэгирования значения хранящегося в области памяти. Если младший бит ноль - то это вовсе не указатель а Smi -целочисленное значение хранящиеся в верхних 31 бите указателя. Если младший бит равен 1 - то это указатель на HeapObject. Второй с конца бит используется для тэгирования WeakObject. Если второй бит равен 1 то это указатель на WeakHeapObject. 
Реализация лежит в include/v8-internal.h
 В добавок ко всему этому если архитектура 64-битная то Node.js умеет сжимать указатели (также как и Java Runtime).  
       Из источников информации отдельно хочется отметить https://github.com/danbev/learning-v8 Автор провел просто фантастическую работу по разбору кишков V8 с отладчиком в руках. И не поленился все это задокументировать. Вообще сама по себе идея документировать то что ты исследуешь в github очень крутая. У этого же автора еще штук пять подобных репозиториев где он разбирает libuv, node.js и прочее. 
      Еще для понимания очень полезным оказался V8’s Object Model Using Well-Defined C++  

понедельник, 3 июня 2019 г.

Этот день мы приближали как могли...

    Этот пост я пишу из поезда Ульяновск-Москва. Мало кто знает но я как минимум раньше очень часто ездил на поездах. Практически каждые выходные катал из Москвы в Ульяновск и обратно. Это продолжалось долгие семь лет (с перерывом на полтора года Вьетнама). И все это время я думал - когда же в поезде появится интернет ??? Так вот в купейных вагонах ТКС он наконец-то появился. Бесплатный. Я практически уверен что на просмотр фильма его не хватит - но для серфинга более чем достаточно. Я не знаю каких усилий потребовала реализация подобного проекта в условиях бюрократии РЖД - но я думаю не малых. В общем респект тем кто это сделал. А то что это еще и бесплатно - это в двойне приятно!