Spec-Zone.ru › OCaml
☰Инструменты OCaml
  • Пакетная компиляция (ocamlc)
  • Интерактивная система, или REPL (ocaml)
  • Система времени выполнения (ocamlrun)
  • Компиляция в машинный код (ocamlopt)
  • Генераторы лексических анализаторов и парсеров (ocamllex, ocamlyacc)
  • Генератор зависимостей (ocamldep)
  • Генератор документации (ocamldoc)
  • Отладчик (ocamldebug)
  • Отладка машинного кода (gdb, lldb)
  • Профилирование (ocamlprof, perf)
  • Взаимодействие C с OCaml
  • Оптимизация с помощью Flambda
  • Фаззинг с помощью afl-fuzz
  • Трассировка времени выполнения с помощью событий среды выполнения
  • Преобразование программы «хвост по модулю конструктора»
  • Обнаружение гонок данных во время выполнения с помощью ThreadSanitizer

Глава 26. Трассировка времени выполнения с помощью событий среды выполнения

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

Выводимые данные включают:

  • Время событий сборщика мусора и фаз среды выполнения
  • Размеры и использование малой и большой куч
  • Скорости выделения памяти и продвижения объектов между кучами

Можно объявлять и использовать дополнительные события, предоставляя библиотекам OCaml возможности мониторинга на более высоком уровне. Они называются пользовательскими событиями.

1 Обзор

Система событий среды выполнения формирует три основных класса событий:

Интервалы
События, охватывающие некоторый промежуток времени. Например, система трассировки событий среды выполнения формирует событие-интервал, которое начинается при запуске малой сборки мусора в сборщике мусора OCaml и заканчивается по завершении сборки. Интервалы могут содержать другие интервалы: например, могут формироваться другие события-интервалы, которые начинаются после начала малой сборки мусора и заканчиваются до её завершения.
События жизненного цикла
События, происходящие в определённый момент времени. Например, при завершении домена формируется соответствующее событие жизненного цикла.
Счётчики
События, содержащие измерение интересующей величины. Например, число слов, перемещённых из малой кучи в большую во время последней малой сборки мусора, выводится как событие-счётчик.

Система трассировки событий среды выполнения предназначена для использования в разных контекстах:

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

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

2 Архитектура

Концептуально система трассировки времени выполнения состоит из двух частей: 1) источников событий, которые формируют события, и 2) транспорта событий, который принимает и передаёт эти события.

2.1 Источники событий

Источники событий — это точки, в которых формируются события. В системе времени выполнения OCaml есть несколько таких источников. Они делятся на два набора: 1) источники событий, доступные всегда, и 2) источники событий, доступные только в инструментированной среде выполнения. Источники событий инструментированной среды выполнения в первую очередь предназначены для разработчиков среды выполнения OCaml и сборщика мусора и в настоящее время включают только события-счётчики размера выделения памяти в большой куче.

Полный список событий, формируемых источниками событий, и их описание доступны в модуле Runtime_events.

2.2 Транспорт событий

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

Кольцевые буферы

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

Реализация кольцевого буфера, используемая для событий среды выполнения, допускает запись не более чем одним производителем одновременно, но несколько потребителей могут читать данные одновременно, без координации с производителем. Для каждого работающего домена существует отдельный кольцевой буфер; после завершения домена буферы могут повторно использоваться для новых доменов. Кольцевые буферы выделяются только при включённых событиях среды выполнения (в том числе для основного домена). Сами кольцевые буферы хранятся в файле, отображённом в память, с идентификатором процесса в качестве имени и расширением .events, что позволяет читать их извне основного процесса OCaml. Подробнее см. в Runtime_events.

API для получения событий

Система трассировки событий среды выполнения предоставляет API для OCaml и C, основанные на курсорах и опросе. Процесс получения событий на высоком уровне выглядит следующим образом:

  1. Курсор создаётся с помощью Runtime_events.create_cursor для текущего процесса или внешнего процесса (задаваемого путём и PID).
  2. С помощью Runtime_events.Callbacks.create регистрируется функция обратного вызова для получения событий.
  3. Опрос курсора выполняется с помощью 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, которые позволяют просто извлекать и обобщать данные из событий среды выполнения. В частности, утилита trace формирует данные, аналогичные тем, которые предоставляла прежняя система инструментирования ’eventlog’, доступная в OCaml с 4.12 по 4.14.

Сначала установите runtime_events_tools в переключатель opam с OCaml 5.0 или более поздней версии:

opam install runtime_events_tools

Это должно добавить инструмент olly в PATH. Теперь можно создавать трассировки времени выполнения для программ, скомпилированных с OCaml 5.0 или более поздней версии, с помощью подкоманды trace:

olly trace trace.json 'your_program.exe .. args ..'

Данные трассировки времени выполнения будут записаны в формате json Trace Event Format в файл trace.json. Затем его можно загрузить в средство просмотра трассировок Chrome или в Perfetto, чтобы визуализировать собранную трассировку.

Измерение задержек сборщика мусора

Утилита 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.

Итак, чтобы формировать и получать пользовательские события с пользовательским типом, необходимо:

  1. зарегистрировать пользовательский тип
  2. добавить новый конструктор в расширяемый вариант Runtime_events.User.tag
  3. зарегистрировать новое пользовательское событие, связав имя пользовательского события, новый тег и новый пользовательский тип
  4. формировать экземпляры события с помощью Runtime_events.User.write
  5. указать обратный вызов, который должен получать все события пользовательского типа, используя Runtime_events.Callbacks.add_user_event для его регистрации в объекте Runtime_events.Callbacks.t, который будет использоваться при опросе (см. раздел 26.3.1 выше).
  6. (Необязательно, если зарегистрировано несколько разных событий с одним пользовательским типом:) В этом обратном вызове сопоставьте событие с образцом по тегу, чтобы различать разные теги событий одного пользовательского типа.

Обратите внимание: если источник и потребитель — разные программы, обе программы должны выполнить шаги 1–3, чтобы зарегистрировать пользовательские события и пользовательские типы событий (если они есть). Значения тегов в обеих программах не обязаны совпадать; совпадать должны только имена.

Незарегистрированные события пользовательских типов в настоящее время игнорируются. В качестве запасного варианта доступны незарегистрированные пользовательские события встроенных типов, но всем им присваивается тег UNK (unknown).

Таким образом, при получении события возможны три случая:

  • Событие зарегистрировано: доступны полезная нагрузка и тег.
  • Событие не зарегистрировано и имеет встроенный тип события (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
[...]
« Фаззинг с помощью afl-fuzzПреобразование программы «хвост по модулю конструктора» »
Copyright © 2026 Национальный институт исследований в области информатики и автоматики

© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/runtime-tracing.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API