Уроборос: запись вызовов работающей программы Границы: чего Уроборос не делает и что ломается на практике
0%

Границы: чего Уроборос не делает и что ломается на практике

Границы: чего Уроборос не делает и что ломается на практике

Эта глава важнее остальных. Инструмент, у которого границы не названы, опаснее отсутствующего: им начинают пользоваться там, где он врёт.

Две части: сначала ограничения по устройству, потом шероховатости, которые видно только руками.

Часть первая: ограничения по устройству

Главное

Трасса записывает, как код себя вёл, а не как он должен себя вести.

Из этого следует всё остальное. Запись «на входе 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 пуст, а если нет — известно, на каком вызове встало.
  • Нагрузка прогона покрывает те ветви, про которые делается вывод.
  • Выводы о длительности не выдаются за цену необмазанного кода.
  • Наблюдения из трассы не записаны как требования.
  • Обмазанный файл не уехал в общую ветку.

Последние два пункта — не формальность. Это ровно те две ошибки, ради которых написана вся глава.

Начало курса — первая глава; короткое описание инструмента — на странице Уробороса.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов