Прочитать команду
Кадр выполнения указывает позицию следующей команды.
Устройство PyChronicle · Python 3.12
Этот разбор рассчитан на человека, который пишет на Python, но не изучал устройство интерпретатора. Каждый внутренний термин сначала объясняется, а затем связывается с конкретным компонентом PyChronicle.
Маршрут
01 Программа и alias02 Байткод и виртуальная машина03 Кадр и снимок04 Хранилище объектов05 Что имеет историю06 Возврат к шагу07 Два уровня истории08 Ветки исполнения09 Web-архитектура10 Возможности и границы01 / ПРОГРАММА И СВЯЗИ МЕЖДУ ОБЪЕКТАМИ
Возьмём четыре строки. Здесь создаётся изменяемый список, второе имя связывается с тем же списком, затем список изменяется и используется в вычислении.
1 numbers = [2, 4]
2 alias = numbers # тот же список
3 numbers.append(8)
4 total = sum(alias)После строки 3 недостаточно сохранить только:
numbers == [2, 4, 8]Нужно знать ещё четыре факта:
alias указывает на тот же объект;[2, 4];В Python присваивание alias = numbers не копирует список. Имена numbers и alias ссылаются на один объект. Вторую ссылку на тот же объект называют alias, а ситуацию — aliasing. Поэтому numbers.append(8) меняет и то, что видно через alias.
Положение выполнения, временные значения, связи имён с объектами, версии изменяемых объектов и принадлежность к ветке.
02 / БАЙТКОД И ВИРТУАЛЬНАЯ МАШИНА
Перед запуском CPython преобразует исходный текст в байткод — последовательность команд вроде «загрузить значение», «вызвать функцию» и «сохранить имя».
Виртуальная машина — исполнитель этих команд. Для каждой команды она явно меняет временный стек, таблицы имён, объекты, кадры вызовов или сведения об исключении. PyChronicle содержит собственную виртуальную машину для поддерживаемой части байткода Python 3.12, поэтому контролирует каждое такое изменение.
numbers.append(8)одно действие превращается в пять командLOAD_NAMEположить numbers на стекLOAD_ATTRполучить метод appendLOAD_CONSTположить число 8 на стекCALLвызвать append(8)POP_TOPубрать None со стекаФактический фрагмент dis.get_instructions() для показанной программы в CPython 3.12.13. Смещение — позиция команды внутри байткода.
Кадр выполнения указывает позицию следующей команды.
Обработчик команды меняет строго определённые данные.
Снимок кадра и новые версии изменённых объектов получают общий номер.
03 / КАДР ВЫПОЛНЕНИЯ И СНИМОК
Кадр выполнения (Frame) относится к одному активному запуску модуля или функции. Вложенный вызов создаёт отдельный кадр со своими локальными именами, временным стеком и позицией в байткоде.
Снимок кадра (VmSnapshot) — неизменяемая запись этих данных на конкретном физическом шаге. Снимки индексируются как по шагу, так и по кадру.
ipпозиция следующей команды байткодаdata_stackвременные значения вычисленийlocals / globalsсвязи имён Python со значениямиcurrent_exceptionобрабатываемое исключениеreturn_value / yield_valueрезультат функции или генератораstep / branch_idфизический шаг и ветка историиПОЛЯ СНИМКА
Снимок также хранит идентификаторы текущего и родительского кадров, глубину вызова, именованные аргументы, предыдущее исключение и признаки завершения или приостановки.
Это временная рабочая область. Команды кладут туда значения и забирают их обратно. Например, для a + b сначала загружаются a и b, затем команда сложения заменяет их результатом. Без стека нельзя точно продолжить выполнение посреди выражения.
Если стек, локальные или глобальные имена не изменились, следующий снимок переиспользует уже сохранённую запись. Поиск ближайшего снимка по номеру шага выполняется двоичным поиском за O(log n).
04 / ХРАНИЛИЩЕ И ВЕРСИИ ОБЪЕКТОВ
Хранилище объектов (ObjectStore) регистрирует изменяемые объекты запуска и связывает каждый из них с отдельной историей. Пользовательский list, dict или set не заменяется специальной обёрткой: рядом создаётся служебная история NativeContainerHistory.
Даже шаги, которые не трогали список, создавали бы ещё одну копию.
[2, 4]T+3[2, 4, 8]T+4без изменения · переиспользоватьИстория сравнивает неглубокое содержимое контейнера и добавляет версию только тогда, когда набор ссылок действительно изменился.
Пространство имён
numbers→ object #7alias→ object #7Хранилище объектов / object #7
Обычный Python list[2, 4, 8]история рядом: NativeContainerHistoryКопируется структура самого контейнера, но вложенные объекты не дублируются: сохраняются ссылки на них. Вложенный изменяемый объект получает собственную историю. Поэтому сохраняются alias, общие вложенные объекты и циклические ссылки.
Почему память не заполняется копиями слишком быстро?Подход похож на персистентные структуры данных: неизменившиеся записи переиспользуются, а новая версия появляется только у изменившейся части. Снимки кадра отдельно переиспользуют неизменившиеся стек, локальные и глобальные имена. История всё равно имеет цену: новый вариант большого контейнера хранит его неглубокую копию, а длинный запуск с большим числом изменений занимает больше памяти.
05 / КАКИЕ ДАННЫЕ ИМЕЮТ ИСТОРИЮ
Для возврата функций, классов, замыканий и генераторов недостаточно версионировать списки. Хранилище выбирает подходящий тип истории для каждого изменяемого компонента.
Хранит неглубокие версии обычных list, dict и set, не меняя тип объекта пользователя.
Запоминает создание, новое присваивание и удаление имени, чтобы восстановить его существование на выбранном шаге.
Версионирует __dict__ и объявленные через __slots__ атрибуты пользовательских объектов.
Хранит содержимое ячеек, через которые вложенная функция обращается к переменной внешней функции.
Сохраняет значения аргументов по умолчанию, именованных значений по умолчанию и связи с замыканием.
Сохраняет точку приостановки, локальные данные и служебное состояние для yield, send, throw и close.
06 / ВОЗВРАТ К ВЫБРАННОМУ ШАГУ
Возврат — согласованная смена выбранного момента истории. Виртуальная машина получает снимки всех кадров, которые были активны к нужному шагу, а хранилище объектов выбирает версии с тем же номером.
Например, момент до numbers.append(8).
Для каждого активного кадра выбирается последняя запись не позднее заданного шага.
Возвращаются позиция команды, стек, имена, исключения, результаты и признаки приостановки.
Контейнеры, атрибуты, замыкания, функции и генераторы переводятся к версиям этого шага.
numbers и alias снова указывают на один список [2, 4].
07 / ФИЗИЧЕСКИЕ И СЕМАНТИЧЕСКИЕ ШАГИ
Физический шаг — одна команда байткода. Семантический шаг — завершённое действие, которое можно сопоставить с исходным Python-кодом. Несколько физических шагов могут образовать один семантический.
5 КОМАНД БАЙТКОДАФизическая история
LOAD_NAMELOAD_ATTRLOAD_CONSTCALLPOP_TOPСписок изменён с [2, 4] на [2, 4, 8]
Семантический режим удобен для обычной отладки. Продвинутый режим показывает отдельные команды, временный стек и промежуточные состояния. Вмешательство и ветвление разрешаются только в безопасных контрольных точках.
08 / СОЗДАНИЕ НОВОЙ ВЕТКИ
PyChronicle не продолжает старый процесс с вручную скопированной памятью. Он создаёт новое изолированное исполнение и детерминированно повторяет программу до выбранной безопасной точки.
EffectJournal — упорядоченные записи вызовов input() и print() с номером шага и ветки. При повторном исполнении PyChronicle проверяет, что наблюдаемое действие совпало с исходной историей. Другие внешние эффекты в текущую область поддержки не входят.
Почему нужна безопасная точка? Посреди команды на стеке находятся временные значения. Изменение переменной в этот момент может нарушить выполняемую операцию. Поэтому ветвление доступно только там, где трассировщик явно разрешил создать ветку.
09 / АРХИТЕКТУРА WEB-ВЕРСИИ
Браузер, основной Django-сервер и исполнитель разделены. Для каждой живой сессии запускается отдельный ограниченный Docker-контейнер. Код ядра отладчика находится только внутри исполнителя.
Отправляет код и команды, показывает историю, переменные, объекты, кадры, ветки и ошибки.
Проверяет вход, владельца UUID-сессии и квоты, управляет очередью, контейнерами и сохранённой историей. Ядро виртуальной машины сюда не импортируется.
В отдельном контейнере принимает команды протокола и обращается к отладочной сессии и виртуальной машине.
Изоляция контейнераНет сети, корневая файловая система только для чтения, непривилегированный пользователь, сброшенные Linux-возможности и отдельные пространства имён.
ЛимитыПо умолчанию контейнер ограничен 512 МиБ памяти, одним ядром, временем процессора, общим временем команды, 64 процессами или потоками и небольшими временными файловыми системами.
Контроль жизниОсновной сервер проверяет heartbeat исполнителя и различает лимиты процессора, памяти, диска, процессов, тайм-аут и обычную ошибку программы.
Основной сервер сохраняет в PostgreSQL ограниченный JSON-чекпойнт, SHA-256 digest и журнал действий. После остановки контейнера история остаётся доступна для чтения.
Текст пользователя и исходный код попадают в письмо, а полный структурированный расклад сессии прикладывается как JSON. Доставка идёт через повторяемую очередь.
Каждая команда повторно проверяет аутентификацию, CSRF, UUID и владельца сессии; доступ к чужой истории не определяется только знанием адреса.
10 / ВОЗМОЖНОСТИ И ГРАНИЦЫ ALPHA
Собственная виртуальная машина должна явно реализовать каждую команду байткода и корректно связать её со снимками, историей объектов, исключениями и ветвлением. Поэтому область поддержки фиксируется точнее, чем просто «Python 3.12».
Арифметика, сравнения, условия, циклы, comprehensions, срезы, распаковка; списки, словари и множества, включая операции на месте, dict | и алгебру множеств.
Позиционные, именованные и значения по умолчанию, вложенные вызовы, рекурсия, замыкания, классы, наследование, super, методы, свойства, декораторы, атрибуты и __slots__.
Исключения, контекстные менеджеры и синхронные генераторы: yield, yield from, send, throw, close, GeneratorExit и возвращаемое значение генератора.
input() и print() поддерживаются как записываемые эффекты. Они привязаны к шагу и ветке и проверяются во время повторного исполнения.
Импорты и внешние библиотеки, файлы, сеть, процессы, системные вызовы, динамическое исполнение кода, низкоуровневая интроспекция, потоки и произвольные C-расширения.
async def, await, async for, async with, корутины и асинхронные генераторы. Неподдержанная конструкция должна завершаться явным сообщением, а не создавать неверную историю.
ИСТОРИЯ — ЧАСТЬ ИСПОЛНЕНИЯ
Запустите пример, откройте состояние объектов, сравните физическую и семантическую историю и создайте альтернативную ветку.
Открыть PyChronicle