Границы: чего Уроборос не делает и что ломается на практике
Эта глава важнее остальных. Инструмент, у которого границы не названы, опаснее отсутствующего: им начинают пользоваться там, где он врёт.
Две части: сначала ограничения по устройству, потом шероховатости, которые видно только руками.
Часть первая: ограничения по устройству
Главное
Трасса записывает, как код себя вёл, а не как он должен себя вести.
Из этого следует всё остальное. Запись «на входе 10000 вышло 9000.0» — это
наблюдение. Она не утверждает, что 9000.0 правильный ответ; она утверждает, что
такой ответ получился.
Различие кажется мелким, пока не начинают выводить из трассы требования. Ошибка на границе — в коде стоит «больше», а правило предметной области говорит «не меньше» — в трассу попадает как обычное наблюдение и ничем от правильного поведения не отличается. Программа, которая учится по этой трассе, выучит ошибку и дальше будет её защищать.
Отсюда разделение, которое ослаблять нельзя:
| что | откуда берётся | почему так |
|---|---|---|
| наблюдение — «на таком входе вышло вот это» | из трассы | она наблюдения и даёт |
| требование — как должно быть | от человека или предметной области | закон ищут, а не подсматривают |
| утверждение обо всех прогонах | только от человека | трасса видела несколько прогонов, а не все |
Трасса не видит того, что не исполнялось
Ветвь, в которую не зашли, не оставит ни строки. И — что хуже — не оставит предупреждения об этом. Молчание про ветвь означает «здесь не были», а не «здесь всё в порядке», и отличить одно от другого по трассе нельзя.
Практический вывод: прогон надо ставить на той нагрузке, про которую вы собираетесь что-то утверждать. Три удобных значения дадут записи про три удобных значения.
У молчания есть и вторая причина, про которую стоит знать заранее. В Go функция,
записанная в переменную или переданная доводом (func(x int) int { … }),
не обмазывается вовсе — обмазываются только объявленные функции и методы.
Её вызовы в трассу не попадут, и выглядеть это будет так же, как если бы её не
звали.
Трасса не знает намерения
Функция вернула -1. Это код ошибки или настоящий ответ? Записи всё равно:
"r": "-1". Ответ лежит в предметной области.
Трасса не сохраняет смысл порядка
Записано, что вызов был и когда он начался. Что он дал такой ответ потому что случился после другого — не записано. Порядок восстановить можно, причинность — нет.
Часть вторая: что ломается руками
Значения обрезаются
У Python предел на записанное значение — 200 знаков, и отдельно ограничено число элементов, показываемых у списка, словаря и кортежа: по 10.
Обрезанное значение выглядит как значение: в записи стоит строка, и по ней не видно, что оригинал был длиннее. При разборе на длинных данных это стоит держать в голове.
Объект записывается как объект, а не как значение
У метода первым доводом идёт сам объект, и в записи он выглядит так:
{"p":"in","t":"2026-09-10T19:59:41.961","id":"bee29e39-7c33-4052-b276-c9b63f8f13c5","ci":-1,"th":"89.123507339907584","fn":"Cart.checkout","a":"<__main__.Cart object at 0x70544a878c20>, 10","k":""}
<__main__.Cart object at 0x70544a878c20> — это отметка «вот этот вот», а не
содержимое объекта. Полей внутри по ней не узнать, и от прогона к прогону число в
конце меняется. Если нужно видеть содержимое — либо у класса должно быть своё
представление, либо интересные поля надо передавать отдельными доводами.
Длительность — это длительность обмазанного прогона
Записи стоят работы: каждая — это запись в файл. Поле d честно измеряет вызов,
но вызов вместе с обмазкой, а значит цену того же кода без неё оно не
называет.
Мерить d можно, чтобы сравнивать вызовы между собой в одном прогоне. Нельзя —
чтобы говорить «эта функция стоит столько-то». Для второго есть профилировщик, и
это другой инструмент.
Чистовик остаётся обмазанным — и забирает не всё
Название clean вводит в заблуждение дважды.
Первое: вставки в коде остаются. finish переносит черновик в clean без
истории git и без debug.info, но обмазанный файл остаётся обмазанным, и
помощник ouroboros_runtime.py едет вместе с ним — без него перенесённое не
запустится. Ответ говорит это прямо полем instrumentation_removed:
{"ok": true, "clean": "/home/user/shop/demo/clean", "synced": [".gitignore", "discount.py", "ouroboros_runtime.py"], "skipped": [], "instrumentation_removed": false, "note": "The copy is instrumented, exactly like the draft: this step publishes the draft, it does not un-instrument it. Left behind: .git, debug.info, tool caches, and anything that looks built (compiled binaries, object files, crash dumps) — each one listed in `skipped` with the reason. If something you wanted is in that list, copy it across yourself."}
Снять обмазку инструмент не умеет. Возврат к необмазанному коду делается системой контроля версий — поэтому и сказано в первой главе про прогон: работайте на копии или под git.
Второе: часть файлов не поедет. Кроме .git и debug.info, finish
оставляет в черновике служебные кеши (__pycache__ и его собратья) и всё, что
похоже на собранное. Про последнее он не молчит — каждый такой файл назван в
skipped с причиной:
"skipped": [{"path": "hello", "reason": "looks built (compiled-format signature, or a NUL byte in the first 8 KiB) and has no source extension"}]
Отбор идёт по виду содержимого, а вид у собранной программы и у файла, который
программа должна была сделать, один и тот же: картинку, нарисованную вашим же
кодом, отбросит так же, как объектник. Признака, который бы их разделил, у
finish нет — поэтому список skipped надо читать, а не пролистывать.
Номер ядра почти всегда неизвестен
Поле ci задумано как номер ядра процессора. У всех восьми языков в обычной
программе там стоит -1 — «узнать не удалось», — и при разборе оно
превращается в пустое значение. Переносимого способа спросить номер ядра ни в
одной из этих сред нет, и выдумывать его не стали: раньше Elixir писал сюда
номер планировщика, и это выглядело номером ядра, не будучи им.
Единственное место, где ci бывает настоящим номером ядра, — сборка C внутри
ядра операционной системы.
Если в разборе нужна привязка к ядрам, на ci рассчитывать нельзя. Поле th
(поток) при этом заполнено везде и работает.
Когда инструмент вообще не подходит
- Нужно узнать, что код должен делать. Спросите человека или предметную область: трасса знает только, что он делал.
- Нужно доказать, что код правильный. По устройству не выйдет: наблюдения не складываются в требование, сколько бы их ни было.
- Нужно измерить производительность. Обмазка меняет время; берите профилировщик.
- Программа недетерминирована, а нужна повторяемость. Записи получите, повторить их не сможете.
- Нельзя менять исходник, и копию сделать негде. Обмазка переписывает файлы.
Проверочный список
Перед тем как считать разбор законченным:
-
malformedравен нулю, а если нет — понятно, откуда посторонние строки. -
in_flightпуст, а если нет — известно, на каком вызове встало. - Нагрузка прогона покрывает те ветви, про которые делается вывод.
- Выводы о длительности не выдаются за цену необмазанного кода.
- Наблюдения из трассы не записаны как требования.
- Обмазанный файл не уехал в общую ветку.
Последние два пункта — не формальность. Это ровно те две ошибки, ради которых написана вся глава.
Начало курса — первая глава; короткое описание инструмента — на странице Уробороса.