English · Русский
Начало работы
Эта страница — про порядок работы: какие команды есть, в каком порядке их зовут и что проверить в конце. Поставить инструмент — Установка.
Если вы пришли с конкретной задачей, начните с неё:
- Прологировать чужой код — понять, что происходит в проекте, которого вы не писали.
- Чтобы ИИ понимал, как код исполняется — дать модели то, что было на самом деле, вместо рассуждений по исходнику.
Два способа работать
Прямо в своём дереве. Дописать запись о вызовах в файл, запустить как обычно, прочитать записи. Три команды, ничего лишнего:
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 <путь> | заводит <путь>/draft/ с историей изменений |
write <путь> <файл> | дописывает записи до сохранения, содержимое со стандартного ввода |
execute <путь> -- <команда> | запускает в черновике, сам подставляет путь к debug.info |
finish <путь> | переносит черновик в соседний <путь>/clean/ |
ouroboros create /srv/tmp/разбор{"ok": true, "base": "/srv/tmp/разбор", "draft": "/srv/tmp/разбор/draft", "clean": "/srv/tmp/разбор/clean"}Проект, заведённый до переименования каталогов
Раньше эти два каталога назывались черновик и чистовик. У проекта, сделанного прежним выпуском, они так и лежат на диске вместе с историей изменений — поэтому create открывает такой проект как есть, а не заводит рядом пустой draft/ и не докладывает об успехе:
{"ok": true, "draft": "…/черновик", "clean": "…/чистовик", "legacy_layout": true,
"legacy_note": "This project still uses the previous directory names …"}Переименовать можно когда удобно: mv черновик draft, и mv чистовик clean, если второй каталог есть. История git лежит внутри каталога и переезжает вместе с ним. Если рядом оказались и draft/, и черновик/, работа идёт с draft/, а второй каталог назван в legacy_note, а не обойдён молчанием.
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Остальное — C и C++ через clangd
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": "…/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не ноль. Ноль значит, что обмазанный код просто не исполнялся.- Прогон затронул те ветви, о которых вы собираетесь делать выводы. Трасса молчит о том, что не исполнялось, и не предупреждает об этом.
- Никто не считает, что запись без исключений означает правильную программу.
Последние два пункта — не формальность. Почему именно так: Границы.