Глава 25 Отслеживание выполнения с помощью событий выполнения
В этой главе описывается система отслеживания событий выполнения, которая позволяет непрерывно извлекать информацию о производительности из среды выполнения OCaml с очень низкой нагрузкой. Система и интерфейсы имеют низкий уровень и тесно связаны с реализацией среды выполнения, она предназначена для конечных пользователей, которые полагаются на инструменты для использования и визуализации интересующих данных.
Выводимые данные включают:
- Время событий сборщика мусора и фаз среды выполнения
- Изменение и использование размеров кучи при малых и больших объёмах
- Скорость выделения и повышения памяти между кучами
Дополнительные события могут быть объявлены и обработаны, предоставляя возможности мониторинга высокого уровня для библиотек OCaml. Они называются пользовательскими событиями.
1 Обзор
Существует три основных класса событий, генерируемых системой событий среды выполнения:
- Промежутки
- События, охватывающие временной интервал. Например, система отслеживания событий выполнения генерирует событие промежутка, которое начинается, когда в сборщике мусора OCaml начинается малая коллекция, и заканчивается, когда коллекция завершена. Промежутки могут содержать другие промежутки, например, могут генерироваться другие события промежутков, которые начинаются после начала малой коллекции и заканчиваются до её завершения.
- События жизненного цикла
- События, происходящие в момент времени. Например, при завершении домена генерируется соответствующее событие жизненного цикла.
- Счётчики
- События, которые включают измерение некоторой интересующей величины. Например, количество слов, повышенных из малой кучи в большую кучу во время последней малой коллекции мусора, генерируется как событие счётчика.
Система отслеживания событий среды выполнения разработана для использования в различных контекстах:
- Самоконтроль
- Программы и библиотеки OCaml могут устанавливать свои собственные обработчики для прослушивания событий среды выполнения и реагирования на них программно, например, для экспорта событий в файл или через сеть.
- Внешний мониторинг
- Внешний процесс может использовать события среды выполнения программы OCaml, система отслеживания событий выполнения которой была включена путём установки соответствующей переменной среды.
Система отслеживания событий выполнения записывает события в кольцевой буфер. Вследствие этого старые события перезаписываются новыми. Потребители могут либо непрерывно получать события, либо получать их только в ответ на какое-либо обстоятельство, например, если определённый запрос или операция занимает больше времени, чем ожидалось.
2 Архитектура
Система отслеживания среды выполнения концептуально состоит из двух частей: 1) источников событий, которые генерируют события, и 2) транспорта событий, который обрабатывает и передает эти события.
2.1 Источники событий
Источники событий являются точками генерации событий. В системе выполнения OCaml существует множество таких источников. Они далее разделены на две группы: 1) источники событий, которые всегда доступны, и 2) источники событий, которые доступны только в инструментированной среде выполнения. Источники событий в инструментированной среде выполнения в первую очередь представляют интерес для разработчиков среды выполнения и сборщика мусора OCaml и, на данный момент, включают только события счётчиков размера выделения большой кучи.
Полный набор событий, генерируемых источниками событий, и их документация находятся в Модуле Runtime_events.
2.2 Транспорт событий
Часть системы транспорта событий обрабатывает события, генерируемые источниками событий, и делает их доступными для потребителей.
Кольцевые буферы
События передаются с помощью структуры данных, известной как кольцевой буфер. Эта структура данных состоит из двух указателей в линейный массив, указатель хвоста указывает на место, куда можно записать новые события, а указатель головы указывает на самое старое событие в буфере, которое можно прочитать. Когда в массиве нет места для записи новых событий, указатель головы перемещается вперёд, и самые старые события перезаписываются новыми.
Реализация кольцевого буфера, используемая в событиях среды выполнения, может быть записана не более чем одним производителем за раз, но может быть прочитана одновременно несколькими потребителями без координации производителем. Для каждого работающего домена существует уникальный кольцевой буфер, и при завершении домена кольцевые буферы могут быть повторно использованы для вновь запущенных доменов. Кольцевые буферы выделены только (включая для основного домена), когда события среды выполнения включены. Сами кольцевые буферы хранятся в файле с отображением памяти с идентификатором процесса в качестве имени и расширением .events, что позволяет их считывать вне основного процесса OCaml. Дополнительную информацию см. в Runtime_events.
Интерфейсы потребления
Система отслеживания событий среды выполнения предоставляет как OCaml, так и C интерфейсы, основанные на курсорах и опросе. Основной процесс потребления событий следующий:
- Курсор создаётся с помощью Runtime_events.create_cursor для текущего процесса или внешнего процесса (указанного путём и идентификатором процесса).
- Вызывается Runtime_events.Callbacks.create для регистрации функции обратного вызова для получения событий.
- Курсор опрашивается с помощью Runtime_events.read_poll с использованием созданных на предыдущем шаге функций обратного вызова. Для каждого соответствующего события в кольцевых буферах вызываются предоставленные функции обратного вызова. В дополнение к генерируемым событиям, функциям обратного вызова передаются идентификатор домена-излучателя и отметка времени выдачи.
3 Использование
3.1 С использованием API OCaml
Начнём с простого примера, который выводит имя, начальное и конечное время событий, генерируемых системой отслеживания событий среды выполнения:
let runtime_begin _ ts phase =
Printf.printf "Begin\t%s\t%Ld\n"
(Runtime_events.runtime_phase_name phase)
(Runtime_events.Timestamp.to_int64 ts)
let runtime_end _ ts phase =
Printf.printf "End\t%s\t%Ld\n"
(Runtime_events.runtime_phase_name phase)
(Runtime_events.Timestamp.to_int64 ts)
let () =
Runtime_events.start ();
let cursor = Runtime_events.create_cursor None in
let callbacks = Runtime_events.Callbacks.create ~runtime_begin ~runtime_end ()
in
while true do
let list_ref = ref [] in (* for later fake GC work *)
for _ = 1 to 100 do
(* here we do some fake GC work *)
list_ref := [];
for _ = 1 to 10 do
list_ref := (Sys.opaque_identity(ref 42)) :: !list_ref
done;
Gc.full_major ();
done;
ignore(Runtime_events.read_poll cursor callbacks None);
Unix.sleep 1
done
Следующим шагом является компиляция и компоновка программы с библиотекой runtime_events. Это можно сделать следующим образом:
ocamlopt -I +runtime_events -I +unix unix.cmxa runtime_events.cmxa
example.ml -o example
При использовании системы сборки dune этот пример можно собрать следующим образом:
(executable (name example) (modules example) (libraries unix runtime_events))
Запуск скомпилированной версии примера даёт вывод, похожий на:
Begin explicit_gc_full_major 24086187297852 Begin stw_leader 24086187298594 Begin minor 24086187299404 Begin minor_global_roots 24086187299807 End minor_global_roots 24086187331461 Begin minor_remembered_set 24086187331631 Begin minor_finalizers_oldify 24086187544312 End minor_finalizers_oldify 24086187544704 Begin minor_remembered_set_promote 24086187544879 End minor_remembered_set_promote 24086187606414 End minor_remembered_set 24086187606584 Begin minor_finalizers_admin 24086187606854 End minor_finalizers_admin 24086187607152 Begin minor_local_roots 24086187607329 Begin minor_local_roots_promote 24086187609699 End minor_local_roots_promote 24086187610539 End minor_local_roots 24086187610709 End minor 24086187611746 Begin minor_clear 24086187612238 End minor_clear 24086187612580 End stw_leader 24086187613209 ...
Это пример самоконтроля, где программа явно начинает прослушивать события среды выполнения и контролирует себя.
Для внешнего мониторинга программе не нужно знать о существовании событий среды выполнения. События среды выполнения можно контролировать с помощью переменной окружения OCAML_RUNTIME_EVENTS_START, которая при установке запустит систему отслеживания событий среды выполнения во время инициализации среды выполнения.
Мы можем удалить Runtime_events.start (); из предыдущего примера и, вместо этого, вызвать программу, как показано ниже, чтобы получить тот же результат:
OCAML_RUNTIME_EVENTS_START=1 ./example
Переменные окружения
Переменные окружения могут использоваться для управления различными аспектами системы отслеживания событий среды выполнения. Доступны следующие переменные окружения:
- OCAML_RUNTIME_EVENTS_START, если установлена, запустит систему событий среды выполнения как часть инициализации среды выполнения OCaml.
- OCAML_RUNTIME_EVENTS_DIR задаёт директорию, в которой будут располагаться файлы .events, содержащие кольцевые буферы системы отслеживания событий среды выполнения. Если отсутствует, будет использоваться текущая рабочая директория.
- OCAML_RUNTIME_EVENTS_PRESERVE, если установлена, заставит среду выполнения OCaml сохранить файлы кольцевых буферов событий среды выполнения после завершения программы OCaml. Это может быть полезно для мониторинга программ с очень коротким временем выполнения. Если не установлена, файлы .events программы OCaml будут удалены при завершении программы.
Размер кольцевых буферов событий среды выполнения можно настроить через OCAMLRUNPARAM, см. раздел 15.2 для получения дополнительной информации.
Компиляция с инструментированной средой выполнения
Некоторые события генерируются только инструментированной средой выполнения. Для их получения программа OCaml должна быть скомпилирована и скомпонована с инструментированной средой выполнения. Для нашей программы-примера из предыдущего примера это делается следующим образом:
ocamlopt -runtime-variant i -I +runtime_events -I +unix unix.cmxa runtime_events.cmxa example.ml -o example
И для dune:
(executable (name example) (modules example) (flags "-runtime-variant=i") (libraries unix runtime_events))
3.2 С инструментальными средствами
Прямой доступ к событиям предназначен в первую очередь для авторов библиотек наблюдаемости и инструментальных средств для конечных пользователей. Гибкий API позволяет использовать данные производительности из событий выполнения для целей протоколирования и мониторинга.
В этом разделе мы рассмотрим несколько утилит в пакете runtime_events_tools, которые предоставляют простые способы извлечения и обобщения данных из событий выполнения. Утилита трассировки, в частности, генерирует данные, аналогичные системе инструментирования «eventlog», доступной в OCaml 4.12–4.14.
Сначала установите runtime_events_tools в переключателе opam OCaml 5.0+:
opam install runtime_events_tools
Это должно установить инструмент olly в вашу среду. Теперь вы можете генерировать трассировки выполнения для программ, скомпилированных с помощью OCaml 5.0+, используя подкоманду trace:
olly trace trace.json 'your_program.exe .. args ..'
Данные трассировки выполнения будут генерироваться в формате JSON Trace Event Format в trace.json. Затем их можно загрузить в просмотрщик трассировок Chrome или в Perfetto для визуализации собранной трассировки.
Измерение задержки GC
Утилита olly также включает подкоманду latency, которая потребляет данные о событиях выполнения и по завершении программы выводит обобщенный гистограмный отчет о продолжительности пауз. Ее можно запустить следующим образом:
olly latency 'your_program.exe .. args ..'
Это должно вывести вывод, аналогичный следующему:
GC latency profile: #[Mean (ms): 2.46, Stddev (ms): 3.87] #[Min (ms): 0.01, max (ms): 9.17] Percentile Latency (ms) 25.0000 0.01 50.0000 0.23 60.0000 0.23 70.0000 0.45 75.0000 0.45 80.0000 0.45 85.0000 0.45 90.0000 9.17 95.0000 9.17 96.0000 9.17 97.0000 9.17 98.0000 9.17 99.0000 9.17 99.9000 9.17 99.9900 9.17 99.9990 9.17 99.9999 9.17 100.0000 9.17
4 Пользовательские события
4.1 Обзор
Система событий выполнения поддерживает определение пользовательских событий. Определяемые глобально уникальным именем, они генерируются с полезной нагрузкой встроенных типов (unit, int, span) и пользовательских типов. Для понимания манипулирования пользовательскими событиями полезно знать, как они передаются и хранятся: их представление состоит из строкового имени (фактически, индекса в массиве всех пользовательских имён) и произвольной последовательности байтов. Типы пользовательских событий могут быть зарегистрированы путём предоставления функций кодирования и декодирования в и из последовательности байтов посредством функции Runtime_events.Type.register.
Определение нового пользовательского события (независимо от того, является ли его полезная нагрузка встроенного типа или пользовательского типа) выполняется с помощью другой функции регистрации Runtime_events.User.register. Эта функция записывает связь между именем пользовательского события, его типом и меткой. Затем метка используется при генерации или потреблении пользовательских событий; она служит сокращенным обозначением имени события.
После регистрации события значения для этого события генерируются с помощью Runtime_events.User.write.
Вкратце, для генерации и потребления пользовательскими событиями с использованием пользовательского типа пользователю необходимо:
- зарегистрировать пользовательский тип
- расширить вариант Runtime_events.User.tag с новым конструктором
- зарегистрировать новое пользовательское событие, связав имя пользовательского события, новую метку и новый пользовательский тип
- генерировать экземпляры события с помощью Runtime_events.User.write
- указать обратный вызов, который должен получать все события пользовательского типа с помощью Runtime_events.Callbacks.add_user_event для регистрации его внутри объекта Runtime_events.Callbacks.t, который будет использоваться при опросе (см. раздел 25.3.1 выше).
- (Необязательно, если зарегистрированы разные события с тем же пользовательским типом:) В этом обратном вызове сопоставьте метку события, чтобы различать разные метки событий одного и того же пользовательского типа.
Обратите внимание, что если эмиттер и потребитель — это разные программы, обе должны выполнить шаги от 1 до 3 для регистрации пользовательских событий и пользовательских типов (если таковые имеются). Обратите внимание, что значения меток не обязательно должны быть одинаковыми в обеих программах; единственные значения, которые должны совпадать, — это имена.
Незарегистрированные события для пользовательских типов в настоящее время игнорируются. В качестве резервного варианта незарегистрированные пользовательские события встроенного типа доступны, но все помечены UNK (неизвестно).
Таким образом, у потребителя есть три случая при получении события:
- событие зарегистрировано: доступна полезная нагрузка и метка.
- событие не зарегистрировано и имеет встроенный тип события (unit, int, span): доступна только полезная нагрузка.
- событие не зарегистрировано и имеет пользовательский тип события: событие отбрасывается.
Обратите внимание, что потребители событий привязывают обратные вызовы к типам событий, поэтому они могут работать как универсальные слушатели, интерпретирующие полезные нагрузки, поступающие от событий, которые не были зарегистрированы. Поскольку это работает только для событий встроенных типов, это может быть полезно для генерации пар событий: события пользовательского типа с событием встроенного типа, что позволяет разработать специализированного потребителя, сохраняя совместимость с универсальными средствами трассировки.
4.2 Трассировка пользовательских событий: пример
Следующая программа использует модули Runtime_events.User и Runtime_events.Type для объявления двух пользовательских событий, предоставляющих значения span и int. Вариант tag расширяется с CustomSpan и CustomInt.
type Runtime_events.User.tag += CustomSpan | CustomInt
let count_span =
Runtime_events.User.register "count.span" CustomSpan
Runtime_events.Type.span
let count_value =
Runtime_events.User.register "count.value" CustomInt
Runtime_events.Type.int
let count () =
Runtime_events.User.write count_span Begin;
for i = 1 to 5 do
Runtime_events.User.write count_value i
done;
Runtime_events.User.write count_span End
let () =
Runtime_events.start ();
for _ = 1 to 3 do
count ()
done
Со стороны потребителя можно использовать предоставленную метку и тип события для сопоставления с соответствующими событиями. Для полноты картины мы предполагаем, что потребитель — это отдельная программа, и повторяем определение новых конструкторов меток, а также регистрацию пользовательских событий.
type Runtime_events.User.tag += CustomSpan | CustomInt
let count_span =
Runtime_events.User.register "count.span" CustomSpan
Runtime_events.Type.span
let count_value =
Runtime_events.User.register "count.value" CustomInt
Runtime_events.Type.int
let span_event_handler domain_id ts event value =
(* we're only interested in our CustomSpan event *)
match Runtime_events.User.tag event, value with
| CustomSpan, Runtime_events.Type.Begin -> Printf.printf "> count begin\n"
| CustomSpan, End -> Printf.printf "< count end\n"
| _ -> ()
let int_event_handler domain_id ts event value =
(* we're only interested in our CustomInt event *)
match Runtime_events.User.tag event with
| CustomInt -> Printf.printf "| count %d\n" value
| _ -> ()
let () =
let open Runtime_events in
let cursor = create_cursor None in
let callbacks =
Callbacks.create ()
|> Callbacks.add_user_event Type.span span_event_handler
|> Callbacks.add_user_event Type.int int_event_handler
in
for _ = 0 to 100 do
ignore(read_poll cursor callbacks None)
done
Вы даёте следующий вывод:
> count begin | count 1 | count 2 | count 3 | count 4 | count 5 < count end > count begin | count 1 | count 2 [...]
© 1995-2024 INRIA.
https://ocaml.org/manual/5.2/runtime-tracing.html