Устройство PyChronicle · Python 3.12

Как PyChronicle исполняет код и сохраняет историю

Этот разбор рассчитан на человека, который пишет на Python, но не изучал устройство интерпретатора. Каждый внутренний термин сначала объясняется, а затем связывается с конкретным компонентом PyChronicle.

Начать разбор

Что именно нужно сохранить?

Возьмём четыре строки. Здесь создаётся изменяемый список, второе имя связывается с тем же списком, затем список изменяется и используется в вычислении.

example.pyPython 3.12
1 numbers = [2, 4]
2 alias = numbers  # тот же список
3 numbers.append(8)
4 total = sum(alias)

После строки 3 недостаточно сохранить только:

numbers == [2, 4, 8]

Нужно знать ещё четыре факта:

  • alias указывает на тот же объект;
  • раньше этот объект содержал [2, 4];
  • следующей должна выполняться конкретная команда;
  • сохранённый момент принадлежит конкретной ветке.
Что такое alias?

В Python присваивание alias = numbers не копирует список. Имена numbers и alias ссылаются на один объект. Вторую ссылку на тот же объект называют alias, а ситуацию — aliasing. Поэтому numbers.append(8) меняет и то, что видно через alias.

Что сохраняет PyChronicle

Положение выполнения, временные значения, связи имён с объектами, версии изменяемых объектов и принадлежность к ветке.

Что выполняется вместо текста программы

Перед запуском CPython преобразует исходный текст в байткод — последовательность команд вроде «загрузить значение», «вызвать функцию» и «сохранить имя».

Виртуальная машина — исполнитель этих команд. Для каждой команды она явно меняет временный стек, таблицы имён, объекты, кадры вызовов или сведения об исключении. PyChronicle содержит собственную виртуальную машину для поддерживаемой части байткода Python 3.12, поэтому контролирует каждое такое изменение.

ИСХОДНЫЙ КОД / СТРОКА 3numbers.append(8)одно действие превращается в пять команд
смещениекомандачто она делает
14LOAD_NAMEположить numbers на стек
16LOAD_ATTRполучить метод append
36LOAD_CONSTположить число 8 на стек
38CALLвызвать append(8)
46POP_TOPубрать None со стека

Фактический фрагмент dis.get_instructions() для показанной программы в CPython 3.12.13. Смещение — позиция команды внутри байткода.

1

Прочитать команду

Кадр выполнения указывает позицию следующей команды.

2

Выполнить

Обработчик команды меняет строго определённые данные.

3

Зафиксировать шаг

Снимок кадра и новые версии изменённых объектов получают общий номер.

Какие данные описывают один момент

Кадр выполнения (Frame) относится к одному активному запуску модуля или функции. Вложенный вызов создаёт отдельный кадр со своими локальными именами, временным стеком и позицией в байткоде.

Снимок кадра (VmSnapshot) — неизменяемая запись этих данных на конкретном физическом шаге. Снимки индексируются как по шагу, так и по кадру.

FRAME #0<module>step 7 · branch main
ipпозиция следующей команды байткода
data_stackвременные значения вычислений
locals / globalsсвязи имён Python со значениями
current_exceptionобрабатываемое исключение
return_value / yield_valueрезультат функции или генератора
step / branch_idфизический шаг и ветка истории

ПОЛЯ СНИМКА

Не только переменные

Снимок также хранит идентификаторы текущего и родительского кадров, глубину вызова, именованные аргументы, предыдущее исключение и признаки завершения или приостановки.

стек и пространства имённеизменяемые записи
объекты Pythonверсии отдельно
Что такое стек значений?

Это временная рабочая область. Команды кладут туда значения и забирают их обратно. Например, для a + b сначала загружаются a и b, затем команда сложения заменяет их результатом. Без стека нельзя точно продолжить выполнение посреди выражения.

Если стек, локальные или глобальные имена не изменились, следующий снимок переиспользует уже сохранённую запись. Поиск ближайшего снимка по номеру шага выполняется двоичным поиском за O(log n).

Обычные списки остаются обычными списками

Хранилище объектов (ObjectStore) регистрирует изменяемые объекты запуска и связывает каждый из них с отдельной историей. Пользовательский list, dict или set не заменяется специальной обёрткой: рядом создаётся служебная история NativeContainerHistory.

НАИВНЫЙ ПОДХОДКопия после каждой команды

Даже шаги, которые не трогали список, создавали бы ещё одну копию.

PYCHRONICLEНовая версия только при изменении
T+1[2, 4]T+3[2, 4, 8]T+4без изменения · переиспользовать

История сравнивает неглубокое содержимое контейнера и добавляет версию только тогда, когда набор ссылок действительно изменился.

Пространство имён

numbers→ object #7
alias→ object #7

Хранилище объектов / object #7

Обычный Python list[2, 4, 8]история рядом: NativeContainerHistory
Что значит «неглубокая версия»?

Копируется структура самого контейнера, но вложенные объекты не дублируются: сохраняются ссылки на них. Вложенный изменяемый объект получает собственную историю. Поэтому сохраняются alias, общие вложенные объекты и циклические ссылки.

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

Контейнерами дело не ограничивается

Для возврата функций, классов, замыканий и генераторов недостаточно версионировать списки. Хранилище выбирает подходящий тип истории для каждого изменяемого компонента.

NativeContainerHistory

Списки, словари, множества

Хранит неглубокие версии обычных list, dict и set, не меняя тип объекта пользователя.

NameWithHistory

Имена

Запоминает создание, новое присваивание и удаление имени, чтобы восстановить его существование на выбранном шаге.

AttributeHistory

Атрибуты экземпляров

Версионирует __dict__ и объявленные через __slots__ атрибуты пользовательских объектов.

CellHistory

Переменные замыканий

Хранит содержимое ячеек, через которые вложенная функция обращается к переменной внешней функции.

FunctionStateHistory

Состояние функций

Сохраняет значения аргументов по умолчанию, именованных значений по умолчанию и связи с замыканием.

GeneratorStateHistory

Генераторы

Сохраняет точку приостановки, локальные данные и служебное состояние для yield, send, throw и close.

Что происходит при нажатии «Назад»

Возврат — согласованная смена выбранного момента истории. Виртуальная машина получает снимки всех кадров, которые были активны к нужному шагу, а хранилище объектов выбирает версии с тем же номером.

  1. 01

    Выбирается шаг

    Например, момент до numbers.append(8).

  2. 02

    Находятся снимки кадров

    Для каждого активного кадра выбирается последняя запись не позднее заданного шага.

  3. 03

    Восстанавливается выполнение

    Возвращаются позиция команды, стек, имена, исключения, результаты и признаки приостановки.

  4. 04

    Выбираются версии объектов

    Контейнеры, атрибуты, замыкания, функции и генераторы переводятся к версиям этого шага.

  5. 05

    Инспектор получает согласованный момент

    numbers и alias снова указывают на один список [2, 4].

Почему у одного запуска два счётчика

Физический шаг — одна команда байткода. Семантический шаг — завершённое действие, которое можно сопоставить с исходным Python-кодом. Несколько физических шагов могут образовать один семантический.

5 КОМАНД БАЙТКОДАФизическая история

31LOAD_NAME
32LOAD_ATTR
33LOAD_CONST
34CALL
35POP_TOP
1 ДЕЙСТВИЕ PYTHONnumbers.append(8)

Список изменён с [2, 4] на [2, 4, 8]

Зачем оба режима?

Семантический режим удобен для обычной отладки. Продвинутый режим показывает отдельные команды, временный стек и промежуточные состояния. Вмешательство и ветвление разрешаются только в безопасных контрольных точках.

Новая ветка строится повторным исполнением

PyChronicle не продолжает старый процесс с вручную скопированной памятью. Он создаёт новое изолированное исполнение и детерминированно повторяет программу до выбранной безопасной точки.

main · #0
branch-1 · #1
  1. 1 Создаётся новый экземпляр виртуальной машины и новая ветка.
  2. 2 Код повторно исполняется до выбранного шага с записанными ранее вводами и безопасными изменениями пользователя.
  3. 3 Вывод и ввод сверяются с журналом внешних эффектов. Если результат расходится, ветка отклоняется.
  4. 4 После совпавшей контрольной точки новое продолжение записывается независимо от родительской ветки.
Что такое журнал эффектов?

EffectJournal — упорядоченные записи вызовов input() и print() с номером шага и ветки. При повторном исполнении PyChronicle проверяет, что наблюдаемое действие совпало с исходной историей. Другие внешние эффекты в текущую область поддержки не входят.

Почему нужна безопасная точка? Посреди команды на стеке находятся временные значения. Изменение переменной в этот момент может нарушить выполняемую операцию. Поэтому ветвление доступно только там, где трассировщик явно разрешил создать ветку.

Основной сервер не исполняет пользовательский код

Браузер, основной Django-сервер и исполнитель разделены. Для каждой живой сессии запускается отдельный ограниченный Docker-контейнер. Код ядра отладчика находится только внутри исполнителя.

01 / БРАУЗЕР

Интерфейс

Отправляет код и команды, показывает историю, переменные, объекты, кадры, ветки и ошибки.

02 / ОСНОВНОЙ СЕРВЕР

Main

Проверяет вход, владельца UUID-сессии и квоты, управляет очередью, контейнерами и сохранённой историей. Ядро виртуальной машины сюда не импортируется.

03 / ИСПОЛНИТЕЛЬ

Runner

В отдельном контейнере принимает команды протокола и обращается к отладочной сессии и виртуальной машине.

Изоляция контейнераНет сети, корневая файловая система только для чтения, непривилегированный пользователь, сброшенные Linux-возможности и отдельные пространства имён.

ЛимитыПо умолчанию контейнер ограничен 512 МиБ памяти, одним ядром, временем процессора, общим временем команды, 64 процессами или потоками и небольшими временными файловыми системами.

Контроль жизниОсновной сервер проверяет heartbeat исполнителя и различает лимиты процессора, памяти, диска, процессов, тайм-аут и обычную ошибку программы.

HistoryStore

История переживает Runner

Основной сервер сохраняет в PostgreSQL ограниченный JSON-чекпойнт, SHA-256 digest и журнал действий. После остановки контейнера история остаётся доступна для чтения.

Reports

Отчёт строит сервер

Текст пользователя и исходный код попадают в письмо, а полный структурированный расклад сессии прикладывается как JSON. Доставка идёт через повторяемую очередь.

Ownership

Сессии принадлежат пользователю

Каждая команда повторно проверяет аутентификацию, CSRF, UUID и владельца сессии; доступ к чужой истории не определяется только знанием адреса.

Поддерживается уже не только базовый Python

Собственная виртуальная машина должна явно реализовать каждую команду байткода и корректно связать её со снимками, историей объектов, исключениями и ветвлением. Поэтому область поддержки фиксируется точнее, чем просто «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