Записывает, как код на самом деле исполнялся: вызовы, доводы, результаты, исключения, длительности
Эта страница — про порядок работы: какие команды есть, в каком порядке их зовут и что проверить в конце. Поставить инструмент — Установка.
Если вы пришли с конкретной задачей, начните с неё:
Прямо в своём дереве. Дописать запись о вызовах в файл, запустить как обычно, прочитать записи. Три команды, ничего лишнего:
wrap-file → запуск как обычно → trace
В отдельном черновике, если портить рабочее дерево не хочется. Инструмент заводит каталог с историей изменений и переносит готовое наружу:
create → write → execute → trace → finish
Оба способа делают одно и то же с исходником; разница в том, кто хранит файлы.
Всего их семнадцать. Первые семь нужны почти всегда, остальные — по случаю.
| команда | что делает |
|---|---|
wrap-file <путь> |
весь файл на месте |
wrap-file <путь> --stdout |
то же, но напечатать, а файл не трогать |
wrap-functions <путь> <имя>… |
только названные функции, остальное не тронуто |
wrap-snippet -l <язык> |
код со стандартного ввода → на стандартный вывод |
ouroboros wrap-functions stats.py average
{"ok": true, "path": "stats.py", "language": "python", "functions_requested": ["average"], "functions_wrapped": 1, "runtime_header": "ouroboros_runtime.py"}
functions_requested против functions_wrapped — здесь и видно, нашлось ли то,
что вы назвали. Если попросили три имени, а обмазалась одна, значит два имени в
файле не нашлись, и молча это не пройдёт.
Так выглядит результат — тронута ровно одна функция:
"""Средняя длительность запросов из журнала."""
from ouroboros_runtime import log as _ouro_log
def parse_line(line):
name, _, ms = line.partition(" ")
return name, int(ms)
@_ouro_log
def average(values):
return sum(values) / len(values)
def report(lines):
pairs = [parse_line(l) for l in lines]
return average([ms for _, ms in pairs])
Проверить, что получится, не трогая файл, — --stdout:
echo 'int add(int a, int b) { return a + b; }' | ouroboros wrap-snippet -l c
#include "ouroboros_runtime.h"
int add(int a, int b) {
struct _ouro_call __ouro __attribute__((cleanup(_ouro_emit)));
int __ouro_result;
_ouro_enter(&__ouro, "add", "%d, %d", a, b);
return (__ouro_result = (a + b), _ouro_set_result(&__ouro, "%d", __ouro_result), __ouro_result); }
Способ обмазки у каждого языка свой: у Python — надстройка над функцией, у C —
__attribute__((cleanup)), у C++ — сторож области видимости, у JavaScript —
try/finally, у Elixir — переопределение def, у Go — defer и именованные
возвраты, у Java и C# — try/catch/finally. Записи при этом одинаковые.
Языки, design/example.md.
| команда | что делает |
|---|---|
trace <файл> |
сами записи, с отбором |
trace-stats <файл> |
сводка: сколько раз звали, сколько бросили, сколько шло |
Отбор у обеих одинаковый:
| довод | что отбирает |
|---|---|
--function, -f |
по имени функции |
--contains, -c |
по доводам или по тому, что вернули |
--outcome result\|raised\|unknown |
вернули / бросили / непонятно |
--min-duration <секунды> |
только медленные вызовы |
--thread <поток> |
один поток из общей записи |
--regex |
считать -f и -c образцами поиска |
--tail N, -n N |
только последние N |
--limit, --cursor |
читать частями (next_cursor из прошлой страницы) |
Готовые ответы на два самых частых вопроса:
ouroboros trace debug.info --outcome raised # что бросило и с чем
ouroboros trace debug.info --min-duration 0.1 # что шло дольше 0,1 с
А «где висит» смотрят не отбором: в ответе всегда есть поле in_flight —
вызовы, у которых есть строка входа и нет строки выхода.
| команда | что делает |
|---|---|
create <путь> |
заводит <путь>/черновик/ с историей изменений |
write <путь> <файл> |
дописывает записи до сохранения, содержимое со стандартного ввода |
execute <путь> -- <команда> |
запускает в черновике, сам подставляет путь к debug.info |
finish <путь> |
переносит черновик в соседний <путь>/чистовик/ |
ouroboros create /srv/tmp/разбор
{"ok": true, "base": "/srv/tmp/разбор", "draft": "/srv/tmp/разбор/черновик", "clean": "/srv/tmp/разбор/чистовик"}
ouroboros execute /srv/tmp/разбор -- python3 stats.py
Кроме записей о вызовах, execute дописывает в тот же файл одну строку о самой
команде — что запускалось, с каким кодом возврата и каким выводом:
{"p":"exec","cmd":["python3","stats.py"],"rc":0,"out":"20.0\n","err":""}
Читалка эту строку пропускает: p у неё не in и не out, значит это не
вызов. В счёт испорченных строк она тоже не идёт.
Каждая операция write оставляет отдельную запись в истории:
ouroboros: write stats.py (+3 wrapped)
ouroboros: init draft
lint, symbols, doc-symbols, refs, callers, describe. Они нужны, когда
надо выбрать, что именно обмазывать в большом дереве на C: найти функцию по
имени, посмотреть, кто её зовёт, и уже потом назвать её в wrap-functions.
Требуют clang-tidy и clangd на машине.
Он не сохраняется. Не «сохраняется как есть», не «сохраняется наполовину» — не сохраняется:
printf 'def broken(:\n return 1\n' | ouroboros write /srv/tmp/разбор bad.py
[python] corrupted source in bad.py: invalid syntax (<unknown>, line 1)
Код возврата 1, файла в черновике нет, записи в истории нет. Разбор делает
родной для языка разборщик — ast у Python, libclang у C и C++, @babel/parser
у JavaScript, Code.string_to_quoted у Elixir, go/parser у Go, компилятор из
JDK у Java, Roslyn у C#, — так что отдельного проверяльщика
кода не нужно: не разобралось — значит испорчено
(ouroboros/languages/base.py:18).
finish переносит, а что оставляетПроверено прогоном, а не выведено из описания. finish копирует черновик в
чистовик, оставляя позади то, что заново делает машина: .git, debug.info,
кэши инструментов, собранные двоичные файлы и слепки памяти после аварии
(ouroboros/sandbox/sync.py:29-82).
Дословный ответ на том же проекте, что и выше:
{
"ok": true,
"clean": "…/чистовик",
"synced": [".gitignore", "ouroboros_runtime.py", "stats.py"],
"skipped": [],
"instrumentation_removed": false,
"note": "The copy is instrumented, exactly like the draft: …"
}
В черновике на этот момент лежал ещё и __pycache__/ouroboros_runtime.cpython-313.pyc,
оставленный запуском, — в чистовик он не поехал.
Отсюда три следствия, о которых стоит знать заранее:
instrumentation_removed: false говорит об этом прямо. В чистовике лежит тот
же обмазанный файл, что и в черновике. Обратной операции у инструмента нет и
быть не может: write_file обмазывает до сохранения, поэтому исходного
текста автора нет ни в черновике, ни в истории изменений. Снимают обмазку из
своей системы контроля версий или руками.ouroboros_runtime.py едет вместе с кодом — и правильно, без
него обмазанный файл не запустится.skipped. Если собрать программу прямо в
черновике, двоичный файл останется там, а не уедет в чистовик. Правило не
умеет отличить итог работы компилятора от файла, который программу просили
сделать: и то и другое произведено машиной. Поэтому оно отбрасывает и
называет отброшенное — посмотрите список и заберите руками то, что вам
было нужно.Тот же проект, но программа собрана в черновике, и рядом положена картинка:
{
"synced": [".gitignore", "notes.csv", "ouroboros_runtime.h", "prog.c", "run"],
"skipped": [
{"path": "prog", "reason": "looks built (compiled-format signature, or a NUL byte in the first 8 KiB) and has no source extension"},
{"path": "prog.o", "reason": ".o: build output, remade by rebuilding"},
{"path": "real.png", "reason": "looks built (compiled-format signature, or a NUL byte in the first 8 KiB) and has no source extension"}
]
}
Обратите внимание: run — сценарий оболочки без расширения и с правом на
запуск — уехал. Право на запуск признаком не считается, потому что оно есть и у
обычного сценария; смотрят на содержимое.
Горячий файл. wrap-file на файле с миллионом вызовов в секунду топит
нужные записи в шуме и заметно замедляет программу. Для таких файлов есть
wrap-functions — обмазываются только названные функции. Для C есть ещё
--minimal: облегчённая запись без хранения кадра вызова, для горячих и
рекурсивных функций.
Повторный запуск ничего не портит. Обмазка идемпотентна: wrap-file на уже
обмазанном файле не добавит вторую надстройку. А вот debug.info только
дописывается — если хотите чистую запись, удалите файл перед прогоном.
trace поле malformed — 0. Иначе часть строк не разобралась.in_flight пусто — или вы знаете, почему эти вызовы не вернулись.calls_parsed не ноль. Ноль значит, что обмазанный код просто не исполнялся.Последние два пункта — не формальность. Почему именно так: Границы.