From f70087c4389d9a34c87e94d485a376e6c09b4d26 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 1 Aug 2026 15:54:28 +0300 Subject: [PATCH 1/6] =?UTF-8?q?docs(research):=20=D1=81=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B1=D0=B0=D1=82=D1=87=D0=B5=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B2=20Python=20=E2=80=94=20=D0=BF=D0=BE=D1=80=D1=8F?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=20=D0=B2=D0=B5=D0=BB=D0=B8=D1=87=D0=B8=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: развилке производительности генератора (#30) нужны числа вместо догадок — тикет #31 просил ответ по первоисточникам. Что: заметка docs/research/2026-08-01-python-batch-generation-speed.md — бенчмарки авторов orjson (5–14x к stdlib json), таблица производительности генераторов numpy (~3 нс на значение), доки multiprocessing (обход GIL), бенчмарк mimesis против Faker (~24x), кросс-проверка msgspec; плюс локальная проверка порядков через uv. Проверка: ссылки на первоисточники в тексте; локальный микробенчмарк воспроизводится `uv run --with numpy --with orjson`. Co-Authored-By: Claude Opus 5 --- ...026-08-01-python-batch-generation-speed.md | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 docs/research/2026-08-01-python-batch-generation-speed.md diff --git a/docs/research/2026-08-01-python-batch-generation-speed.md b/docs/research/2026-08-01-python-batch-generation-speed.md new file mode 100644 index 0000000..f771856 --- /dev/null +++ b/docs/research/2026-08-01-python-batch-generation-speed.md @@ -0,0 +1,159 @@ +# Скорость батчевой генерации событийных данных в Python: порядки величин + +Дата: 2026-08-01. Тикет: #31 (часть карты #26). Потребитель: развилка +производительности генератора (#30, спека v2, разделы 9 и 11). + +## Вопрос + +Какой скорости (строк или событий в секунду) реально ждать от батчевой +генерации событий в Python и что для неё берут: numpy/векторная генерация +против посточной, orjson против stdlib json, multiprocessing по дням? +Ответ — по первоисточникам (документация и бенчмарки авторов библиотек). + +## Краткий вывод + +Порядки величин на одно ядро CPU: + +| Подход | Порядок скорости | Опора | +|---|---|---| +| Случайные значения numpy (векторно) | ~10^8 значений/с | таблица numpy | +| Векторная сборка колонок события | ~10^6–10^7 строк/с | локальная проверка | +| Посточная генерация (random + dict) | ~10^4–10^5 строк/с | локальная проверка | +| Посточная генерация через Faker | ~10^3–10^4 строк/с | бенчмарк mimesis | +| Сериализация orjson (запись ~50 КБ) | ~10^5 док/с | README orjson | +| Сериализация stdlib json (та же запись) | ~10^4 док/с | README orjson | + +Ключевые множители: векторная генерация быстрее посточной на 1–2 порядка; +orjson быстрее stdlib json в 5–14 раз на сериализации; multiprocessing +умножает всё на число ядер, потому что обходит GIL подпроцессами. + +Узкое место конвейера «сгенерировать → JSON в Kafka» — не случайные числа +(они почти бесплатны), а сборка посточных словарей и их сериализация: +это ~2–5×10^5 событий/с на ядро, дальше масштабирование только процессами. + +## Находки по первоисточникам + +### 1. orjson против stdlib json — бенчмарки автора + +Источник: README orjson, раздел Performance — +. Среда автора: Python 3.11.10, +Fedora 42, x86-64-v4; скрипт `pybench` в репозитории. + +Сериализация (медиана, операций в секунду): + +| Файл | orjson | json | Ускорение | +|---|---|---|---| +| twitter.json (~600 КБ) | 8 453 | 765 | 11,1x | +| github.json (~55 КБ) | 103 693 | 7 648 | 13,6x | +| citm_catalog.json | 3 975 | 338 | 11,8x | +| canada.json | 399 | 33 | 11,9x | + +Десериализация ускоряется скромнее: 2,2–6x на тех же файлах. + +Отдельно важное для генератора: orjson сериализует `numpy.ndarray` нативно +(опция `OPT_SERIALIZE_NUMPY`). Числа автора: массив float64 на 92 МиБ — +105 мс против 1 481 мс у json (14,2x); int32 на 100 МиБ — 68 мс против +684 мс (10,1x). `datetime` тоже сериализуется нативно в RFC 3339, без +обёрток и `default=`. + +Документация stdlib json заявлений о скорости не делает вовсе +(). Факт «чистый Python +с C-ускорителем» виден только в исходниках CPython: `Lib/json/encoder.py` +импортирует `c_make_encoder` из `_json` с откатом на Python-реализацию +(). + +### 2. Векторная генерация случайных значений — документация numpy + +Источник: официальная страница производительности генераторов — +. +Среда: Linux, AMD Ryzen 9 3900X. Единицы — наносекунды на одно значение. + +| Распределение | PCG64 | SFC64 | MT19937 | +|---|---|---|---| +| uint32 | 1,9 | 1,8 | 3,3 | +| uint64 | 3,2 | 2,5 | 5,6 | +| равномерное | 3,1 | 2,6 | 5,9 | +| нормальное | 10,8 | 8,3 | 13,9 | +| пуассоновское | 103,4 | 90,7 | 111,7 | + +То есть 3 нс на равномерное значение — это ~3×10^8 значений/с на ядро. +Даже для широкого события в ~47 колонок чистая генерация значений даёт +миллионы строк в секунду: не она ограничивает конвейер. Рекомендация +numpy — PCG64 (или PCG64DXSM для сильно параллельных сценариев). + +### 3. multiprocessing — документация Python + +Источник: . +Дословно: пакет «effectively side-stepping the Global Interpreter Lock by +using subprocesses instead of threads» — то есть генерация по независимым +модельным дням масштабируется числом ядер, а не упирается в GIL. + +Практические указания из тех же доков: `Pool.map` режет итерируемое на +куски (`chunksize`), а для длинных последовательностей «using a large value +for chunksize can make the job complete much faster than using the default +value of 1» (про `imap`). Для нашей схемы «один процесс — один день» это +неактуально: единица работы и так крупная. + +### 4. Faker и цена посточной генерации + +Сам Faker чисел не публикует, но признаёт цену взвешенного выбора: +конструктор принимает `use_weighting`, и при `False` «the selection process +is much faster» (, +раздел Optimizations). + +Числа даёт авторский бенчмарк mimesis против Faker — + (mimesis 19.0.0, MacBook Pro +M1 Pro). На 47 сопоставимых операциях mimesis выиграл все сравнения, +суммарное ускорение ~24x; по категориям — от 5x (Datetime) до 69x (Finance). +Порядок абсолютных цифр: сотни микросекунд на вызов Faker — то есть +~10^3–10^4 значений/с. Оговорка: единицы в таблицах страницы внутренне +противоречивы (µs против ms не сходятся с заявленным 24x), надёжны именно +коэффициенты ускорения, абсолютные значения — с осторожностью. + +Вывод для нас: Faker в горячем цикле на десятки миллионов событий — это +узкое место на 2–3 порядка хуже numpy. Его место — генерация небольших +справочников (каталог, имена), не потока событий. + +### 5. Кросс-проверка: msgspec + +Бенчмарки автора msgspec (, CPython 3.11): +msgspec со схемами быстрее всех, без схем — «on-par with orjson (the next +fastest JSON library)». Это подтверждает, что orjson — верхняя планка +скорости JSON в Python без введения схем; выигрыш msgspec — скорее память +(на декоде 77 МиБ: 67,6 МиБ пика против 406,3 МиБ у orjson). + +## Локальная проверка порядков (не первоисточник) + +Микробенчмарк на машине стенда (8 логических ядер, Python 3.14, `uv run +--with numpy --with orjson`), 200 тыс. строк по 15 колонок: + +- numpy векторно: ~6,4 млн строк/с; +- посточно (stdlib random + dict): ~71 тыс. строк/с — разрыв ~90x; +- orjson.dumps по строке: ~560 тыс. строк/с; json.dumps: ~120 тыс. (4,7x); +- транспонирование «колонки → посточные dict» (нужно для JSON в Kafka): + ~275 тыс. строк/с — именно оно, а не RNG, становится узким местом + векторного пути; +- multiprocessing, 4 процесса на независимых «днях»: x1,9 на маленькой + нагрузке (старт пула съедает долю; на крупных днях доля падает). + +Локальные числа сходятся с первоисточниками по порядку величины: разрыв +orjson/json на маленьких записях меньше README-шного (4–5x против 11–14x — +у автора записи крупнее), остальное совпадает. + +## Что это значит для генератора стенда + +Без принятия решений — решает тикет #30; здесь только опора для него. + +- Реалистичная планка конвейера «генерация + orjson посточно» — + ~2–5×10^5 событий/с на ядро, с multiprocessing по дням — умножить на + число ядер. Например, 10 млн событий — это десятки секунд на одном ядре + и секунды на восьми. +- Случайные числа брать векторно из numpy (PCG64) — их стоимость можно не + учитывать в бюджете. Посточный цикл оставить только там, где логика + действительно посточная (цепочки сессий), и не звать в нём Faker. +- Живой поток ×60: даже 100 тыс. событий модельного дня при ×60 — это + ~70 событий/с реального времени; на 3–4 порядка ниже планки, скоростью + не ограничен. +- Довод спеки «компилируемый язык только если замеры покажут, что Python + не тянет» получает численную опору: до ~10^6 событий/с суммарно Python + с батчевой архитектурой укладывается, дальше — территория Rust/Go. From a9fade84f53f6731748f0ec10c8b6aad4f4e3423 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 1 Aug 2026 18:30:41 +0300 Subject: [PATCH 2/6] =?UTF-8?q?docs(agents):=20=D1=80=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D0=BA=D0=B8=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=E2=80=94=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=81=D0=BA=D0=B8=D0=BB=D0=BB=20brainstorm-w?= =?UTF-8?q?ith-docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: практика веера жила только в заметках карты #26 и памяти агента — хрупко и локально для одной машины. Строку в AGENTS.md читает каждая сессия, включая чартинг будущих wayfinder-карт. Что: подраздел «Развилки решений» в Agent skills — существенные развилки вести через скилл brainstorm-with-docs: веер вариантов, потом конвергенция. Состав веера и запись отклонённых — в самом скилле, без дублирования. Проверка: чтение. Сам скилл версионируется в dotfiles владельца. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c820aef..bc4bf61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,12 @@ Airflow) и названия из кода. Если для понятия ес ## Agent skills +### Развилки решений + +Существенные развилки — в грилинге, на тикетах wayfinder-карт и вне их — +вести через скилл `brainstorm-with-docs`: сначала веер вариантов, потом +конвергенция. Состав веера и запись отклонённых вариантов — в самом скилле. + ### Issue tracker Задачи — в Gitea на `git.dementev.space`, все операции через CLI `tea`. From f85626508f514d004a8b0232ee4fcfb3232d577a Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 1 Aug 2026 19:47:29 +0300 Subject: [PATCH 3/6] =?UTF-8?q?docs(specs):=20=D1=81=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0=20=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B8=D0=BB=D0=BE=D0=BA=20=D0=BA=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D1=8B=20#26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - этап 2 нельзя нарезать на тикеты без единой картины генератора, а решения четырёх развилок карты #26 жили только в тикетах трекера. - Что: - новая спека docs/specs/2026-08-01-generator.md: функциональный мир, детерминизм до байта (SeedSequence сверен через Context7), контракт схемы, канонический сериализатор, числа и порог производительности; отклонённые варианты записаны с доводами. - мастер-спека согласована тем же коммитом: 1.4 — data contract вместо автогенерации DDL, 8 — в git только манифест, 11 — числа вместо «зафиксировать требования»; мелкие согласования в 7 и 9. - CONTEXT.md пополнен терминами модели мира и вывода генератора. - Проверка: - вычитка; относительные ссылки спек указывают на существующие файлы в docs/specs/ и docs/research/. Co-Authored-By: Claude Opus 5 --- CONTEXT.md | 48 ++++ docs/specs/2026-07-30-stand-v2-realism.md | 53 ++-- docs/specs/2026-08-01-generator.md | 322 ++++++++++++++++++++++ 3 files changed, 403 insertions(+), 20 deletions(-) create mode 100644 CONTEXT.md create mode 100644 docs/specs/2026-08-01-generator.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..b351b7b --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,48 @@ +# Кликстрим-платформа (стенд v2) + +Словарь понятий проекта: одни и те же слова для одних и тех же вещей — +у владельца, кода, документов и агентов. Только язык, никаких решений. + +## Язык + +**Хранилище**: +Аналитическая база стенда — кластер ClickHouse со слоями STG/ODS/DDS/DM. +Принимающая сторона границы «трекер | хранилище»: нормализует имена и стили +источников, строится по их документации. +_Избегать_: склад, склад данных + +**Состав мира**: +Постоянная часть мира генератора — популяция посетителей, их привычки, +календарь двухкуковых пар. Чистая функция зерна: вычисляется при старте +любого процесса, между прогонами не хранится. +_Избегать_: состояние мира + +**Ось модельного времени**: +Собственный календарь мира генератора. Дни считаются от фиксированного +первого дня D0; реальный календарь в модели не участвует. Между прогонами +живут только зерно и позиция на оси. + +**Пошаговый режим**: +Базовый способ движения по оси модельного времени: «прожить следующий +день» — явное действие. + +**Живой день**: +Проигрывание текущего модельного дня в реальном времени с ускорением; +включается по требованию, не постоянный фон. + +**Граница суток**: +Единственный структурный шов модели: сессии режутся по ней, слепок заказов +снимается на ней, день проживается только целиком. + +**Контракт схемы**: +Python-модуль с описателями колонок события — собственность генератора. +Из него выводятся генератор, валидация и документация формата; хранилище +строится по документации, не по модулю. + +**Канонический сериализатор**: +Единственное место, где событие превращается в байты. Фиксированный порядок +ключей и строк — основа побайтовой воспроизводимости. + +**Проигрыватель**: +Компонент доставки готового потока дня в приёмник. Два режима: пакетный +(пачкой, без темпа) и живой день. diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index 48e96cc..e1ce3f7 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -163,12 +163,18 @@ Ecommerce (заполнены только у торговых событий): ### 1.4 Схема как контракт -Одно машинное описание схемы события (python-модуль или YAML) — источник -истины: из него выводятся DDL и валидация генератора, а не наоборот. 47 -колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью, -трансформации, витрины, манифест, доки) — без контракта они расходятся -молча. Заодно это учебный артефакт: менти видит на живом примере, что такое -«схема как контракт». +Машинное описание схемы события — python-модуль с чистыми данными, +собственность генератора (data contract; решение развилки «Архитектура» +карты #26, подробности — [спека генератора](2026-08-01-generator.md), +раздел 3). Из контракта выводятся сам генератор, его валидация и +рендеренное «описание выгрузки» в доках — аналог документации Метрики. +Сторона хранилища (DDL, SELECT матвью, трансформации, витрины) пишется по +этой документации на своих этапах, как в бою хранилище адаптируется к +источнику; границу сторожат строгий приём (раздел 6) и contract-тест в +smoke — сравнение `system.columns` поднятого стенда со схемой генератора. +Без контракта 47 колонок, повторяясь примерно в семи местах, расходятся +молча. Заодно это учебный артефакт: менти видит на живом примере, что +такое data contract. ## 2. Заказы бэкенда @@ -375,7 +381,8 @@ ReplacingMergeTree. Таблицы `stg.*_raw` хранят виртуальны (`_topic`, `_partition`, `_offset`, `_timestamp`) — без них урок «какая нода читала топик» ненаблюдаем. `stg.hits_raw` дополнительно хранит извлечённый `event_date` — им кормится переобработка дня X (при исчерпании retention -Kafka переобработка возможна только из эталонного артефакта). +Kafka день переигрывается генератором заново: снимок — кэш чистой функции, +см. [спеку генератора](2026-08-01-generator.md)). `dds.v_event` — первый на стенде пример правила «слой — это контракт, а не обязательно копия данных». @@ -425,9 +432,15 @@ Kafka переобработка возможна только из эталон ## 8. Эталонный мир и манифест -Пересборка артефакта `data/startup_history/` неизбежна и оплачена решением -#18 один раз — все изменения генератора съезжаются в одну пересборку. -Манифест расширяется контрольными числами: +В git хранится только манифест эталонного мира; сам снимок (14 модельных +дней) генерируется на месте — при `make up` и при проверках (решение +развилки «Производительность» карты #26, подробности — +[спека генератора](2026-08-01-generator.md), раздел 5). Манифест несёт +паспорт мира (каноническое зерно, версия генератора), контрольные счётчики +и хеши по дням; проверки «пустой git diff» и «пересгенерируй день N — +сравни хеш» живут на нём. Политика версионирования артефакта (бывший туман +карты #10) закрыта этим же ходом: версионируется манифест. +Контрольные числа манифеста: - заказная сторона: заказы и выручка по дням; манифест хранит точные счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) — @@ -435,9 +448,8 @@ Kafka переобработка возможна только из эталон - идентичность: uniq кук, uniq известных пользователей, число двухкуковых покупателей — лаба склейки получает самопроверку. -Артефакт вырастет (ecommerce-массивы, заказы) — размер проверить при -пересборке. Политика версионирования артефакта здесь не решается (туман -карты #10). +Снимок вырастет против v1 (ecommerce-массивы, заказы) — размер проверить +при пересборке. ## 9. Оценка объёма исполнения @@ -451,7 +463,7 @@ v2 стартует пустым, поэтому объём ниже — это | SQL | 5 DDL-файлов (ON CLUSTER, Replicated*, Distributed) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность | | Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~5–6 файлов | | Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла | -| Эталонный мир | сборка артефакта v2, манифест-счётчики, чек-скрипты | M–L | +| Эталонный мир | пересборка снимка на месте, манифест-счётчики, чек-скрипты | M–L | | Мониторинг | дашборды Grafana «данные», «кластер», «запросы»; ClickHouse источником данных, панели на SQL; Prometheus тонким полом (ADR 0002) | M — конфиги и дашборды | | Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR | @@ -496,7 +508,8 @@ v2 стартует пустым, поэтому объём ниже — это 5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets). 6. Расхождения B+D и опоздания; счётчики манифеста. -7. Эталонный мир: пересборка артефакта, чек-скрипты. +7. Эталонный мир: манифест и пересборка снимка, чек-скрипты; CI-генерация + на amd64 и arm64. 8. Superset-дашборд v2. 9. Мониторинг и runbook «keeper упал / DDL повис в очереди». Состав дашбордов и границы — ADR 0002. @@ -510,7 +523,6 @@ smoke-проверки, а не «дашборд зелёный». Это мин - «Грязь» в данных: боты, дубли на транспорте, опоздавшие мобильные батчи, расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом. -- Политика версионирования эталонного артефакта — туман карты. - Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не переносится. Спека даёт будущим лабам только опорные точки — контрольные @@ -550,10 +562,11 @@ smoke-проверки, а не «дашборд зелёный». Это мин (батчевая генерация вместо посточной, быстрая JSON-сериализация, распараллеливание по модельным дням). Читаемость генератора для менти — не довод при выборе языка: он в любом случае сложнее уровня DE-джуна. - До этапа 3 зафиксировать требования производительности (пересборка - эталонного мира, живой поток ×60); переход на компилируемый язык - (Rust/Go) — только если замеры покажут, что Python приемлемой скорости - не даёт. + Числовые требования производительности и способ замера зафиксированы + [спекой генератора](2026-08-01-generator.md), раздел 5 (порядки величин — + [исследование](../research/2026-08-01-python-batch-generation-speed.md)); + переход на компилируемый язык (Rust/Go) — только если живые замеры + этапа 2 выйдут за её порог. ## 12. Влияние на документацию diff --git a/docs/specs/2026-08-01-generator.md b/docs/specs/2026-08-01-generator.md new file mode 100644 index 0000000..68ca0c9 --- /dev/null +++ b/docs/specs/2026-08-01-generator.md @@ -0,0 +1,322 @@ +# Генератор (этап 2): функциональный мир, детерминизм до байта, контракт схемы, числа скорости + +Статус: Proposed — ждёт приёмки владельцем (тикет #33). +Дата: 2026-08-01. Мандат — тикет #14 (этап 2, родитель #4), карта #26. +Развилки: модель мира (#27), детерминизм от зерна (#28), архитектура вывода +(#29), производительность (#30); исследование скорости (#31). +Источники: мастер-спека +[«Боевой реализм стенда (v2)»](2026-07-30-stand-v2-realism.md) — разделы 1.4, +5, 9–11; заметка +[«Скорость батчевой генерации в Python»](../research/2026-08-01-python-batch-generation-speed.md). + +## Зачем + +Мастер-спека решила, **что** генерирует стенд: широкое событие в 47 колонок, +таксономию, анонимность, двухкуковых покупателей, заказы слепками. Четыре +развилки о том, **как** генератор устроен, она отложила: модель мира, +границы детерминизма, источник истины схемы с разделением потока и пакета, +числовые требования скорости. Карта #26 эти развилки прошла; спека собирает +решения в одну картину. По ней этап 2 режется на тикеты (#34). + +Здесь не переоткрывается решённое мастер-спекой: модель данных события, +таксономия, анонимность, механика заказов (границы мандата #14). Генератор +слепков заказов — этап 3: эта спека лишь не должна ему мешать. + +## Целевая картина одним взглядом + +- **Мир — функция, не состояние.** Состав мира — чистая функция зерна; + модельный день D — функция (зерно, D). Между прогонами живут только зерно + и позиция на оси времени. +- **Детерминизм до байта.** Одно зерно — побайтово тот же снимок; сверка — + хешами манифеста. Транспорт (офсеты Kafka, темп) — вне обещания. +- **Схема — контракт генератора.** Python-модуль с чистыми данными; + хранилище строится по рендеренной документации, границу сторожит + contract-тест. +- **Один сериализатор, глупые приёмники.** День-функция выдаёт канонические + байты; приёмники — файл, Kafka пачкой, Kafka с темпом. +- **Числа.** Средний день ~50 тыс. событий; эталонный снимок — 14 дней; + в git — только манифест; автоматический порог один — день ≤ 30 с. + +## 1. Модель мира + +Резолюция развилки [«Модель мира»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/27). + +- **Мир функциональный, ничего не мутирует.** Постоянный состав мира — + популяция посетителей, их привычки, календарь двухкуковых пар — чистая + функция зерна, вычисляется при старте любого процесса. День D — функция + (зерно, D); межднёвные связи (окно заказов K, опоздания) выводятся из + плана состава, а не копятся в состоянии. Глобальные инварианты («каждый + двухкуковый покупатель заказал с обеих кук») гарантируются планом — + счётчики манифеста известны до генерации событий. +- **Своя ось модельного времени.** Мир рождается в фиксированный день D0 + (понедельник — см. раздел 5); реальный календарь в модели не участвует. + Между прогонами живут только зерно и позиция на оси: выключенный ноутбук — + мир замер, потом продолжил. +- **Два режима движения по одной оси.** Пошаговый — базовый для лаб: старт + с эталонного снимка, дальше «прожить следующий день» — явное действие. + Живой день — текущий день проигрывается с ускорением, дашборд и мониторинг + «дышат»; включается по требованию, не постоянный фон. +- **Граница суток — единственный структурный шов.** Сессии режутся по ней, + дневная партиция самодостаточна; в конце модельного дня — слепок заказов. + День проживается целиком, полдня не бывает: недожитый из-за обрыва день + переигрывается (раздел 4). +- **Поток и пакет совместимы по построению.** День-функция выдаёт один + упорядоченный поток событий; режимы отличаются только способом + проигрывания — пачкой или с темпом. + +Отклонено с доводами: + +- *Мутирующее состояние мира* («мир стареет»): ломает параллельность по + дням, требует чекпоинтов, счётчики манифеста узнаваемы только постфактум; + ни один урок стенда на старении не стоит. +- *Чистая функция без слоя состава*: глобальные инварианты пришлось бы + выводить в каждом дне заново — тот же план мира, но неявный и размазанный. +- *Привязка модельного времени к реальному календарю* (T-1 с догоном): + конфликтует с ускорением ×60 — за вечер мир уезжает в будущее — и делает + даты эталонного мира зависимыми от даты запуска, манифест теряет + воспроизводимость. + +## 2. Детерминизм от зерна + +Резолюция развилки [«Детерминизм от зерна»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/28). + +- **Обещание — содержимое до байта.** Два прогона с одним зерном дают тот же + набор событий: те же `WatchID`/`VisitID`, поля, метки модельного времени. + Артефакт снимка при пересборке побайтово совпадает: канонический порядок + ключей и строк, пустой diff означает «ничего не изменилось». Вне + обещания — транспорт: офсеты и партиции Kafka, какая нода прочитала, + `_ingested_at`, темп живого дня. +- **Условия обещания.** Детерминизм держится при зафиксированном `uv.lock` + и внутри канонического контейнера (везде Linux; переменная — только + архитектура CPU). Истина — CI на Linux; сходимость любой машины проверяет + скрипт «пересгенерируй день N — сравни хеш с манифестом». Расхождение на + любой платформе — баг генератора, а не допуск. +- **Раздача зерна — иерархией подпотоков.** Корневое зерно → состав мира; + (зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик, + торговые события, расхождения, опоздания — в фиксированном порядке. + По построению: параллельный прогон равен последовательному; продление + истории днём N+1 не трогает дни 1…N; правка одного компонента меняет + только его часть снимка — diff читаем. +- **Механизм подпотоков — `numpy.random.SeedSequence`.** Сверено через + Context7 по документации numpy (2026-08-01): `spawn(n)` порождает детей + расширением `spawn_key`, потомок полностью определяется парой + (entropy, spawn_key) — позицией в дереве, а не порядком вычислений. Это + ровно то свойство, на котором держатся три гарантии предыдущего пункта. + Каждый подпоток кормит `PCG64` — генератор, рекомендованный numpy. +- **Дисциплина целочисленной случайности.** Случайность тянется целыми + числами: диапазоны, выбор из таблиц. Плавающие распределения из системной + математики не используются — это снимает межархитектурные расхождения + amd64/arm64. Деньги считаются в целых копейках; Float64 — только + представление в клиентском `purchase` (урок мастер-спеки о расхождениях). +- **Канонический seed и паспорт мира.** Эталонный мир собирается одним + каноническим зерном — константой репозитория; свои зёрна менти крутит без + гарантий манифеста. Манифест хранит паспорт мира — зерно и версию + генератора; чек-скрипты сверяют паспорт раньше счётчиков. +- **Суточный профиль интенсивности задаёт день-функция.** Форма — волны: + ночной провал, обеденный и вечерний пики, различие будней и выходных; + пики — до ~2× среднего. С детерминизмом профиль совместим: это часть + функции дня, а не внешний шум. + +Отклонено с доводами: + +- *Воспроизводимы только состав мира и счётчики*: ломает доигрывание дня + через дедуп (другие `WatchID` — дубли вместо склейки) и воспроизводимую + отладку. +- *События те же, байты не обещаем*: экономия копеечная, а честный diff + снимка и тесты «хеш совпал» теряются. +- *Общий RNG-поток на все дни*: порядок исполнения менял бы результат — + «параллельно равно последовательно» недостижимо. +- *Обещание детерминизма поверх обновления зависимостей*: numpy сознательно + улучшает алгоритмы распределений между версиями (NEP 19), Faker меняет + словари. Фиксация — `uv.lock`; обновление зависимостей — осознанная + пересборка манифеста одним PR. + +## 3. Контракт схемы + +Резолюция развилки [«Архитектура»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/29), часть первая. + +- **Граница вывода — по шву «трекер | хранилище», как data contract.** + Контракт схемы — собственность генератора, как формат выгрузки — + собственность Метрики. Из контракта выводятся: сам генератор, его + валидация и публичное «описание выгрузки» в доках — рендеренная таблица + колонок, аналог документации Метрики. +- **Форма контракта — импортируемый python-модуль с чистыми данными**: + описатели колонок (имя Метрики, тип ClickHouse, тип numpy, snake_case-имя + для DDS, группа полей, порядок), никакой логики. Читаемость для менти + несёт рендеренная таблица в доках, не модуль. +- **Сторона хранилища пишется по документации, не генерируется.** DDL + `ods.event`, SELECT матвью, `dds.v_event`, трансформации — работа + следующих этапов по «описанию выгрузки», как в бою хранилище адаптируется + к источнику. Границу сторожат два боевых механизма: строгий приём + (`input_format_skip_unknown_fields = 0`, таблицы `*_errors` — раздел 6 + мастер-спеки) и contract-тест в smoke — сравнение `system.columns` + поднятого стенда со схемой генератора. + +Отклонено с доводами: + +- *Автогенерация DDL хранилища из контракта* (буква раздела 1.4 + мастер-спеки до правки): пересекает границу ответственности компонент — + в бою хранилище адаптируется к источнику руками, менти пришлось бы + объяснять приём, которого в жизни нет. Data contract даёт тот же щит от + дрейфа без этой условности. +- *YAML как форма контракта*: красота ценой загрузчика и «схемы для схемы»; + потребителя вне Python нет — хранилище читает рендеренную документацию, + не машинный файл. + +## 4. Поток и пакет: сериализатор, проигрыватель, приёмники + +Резолюция развилки [«Архитектура»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/29), часть вторая. + +- **Один канонический сериализатор, глупые приёмники.** День-функция выдаёт + упорядоченный поток канонических байтов — единственное место, где событие + превращается в JSON. Приёмники не знают о содержимом: файл (локальный кэш + для пересборки и проверок манифеста), Kafka пачкой — пакетный режим, + Kafka с темпом ×60 — живой день. Новых топиков нет. +- **Рабочий выбор сериализатора — orjson**: быстрее stdlib json в 5–14 раз, + numpy-массивы и datetime сериализует нативно (заметка исследования #31). + Смена библиотеки меняет канонические байты, поэтому проходит как + обновление зависимости: осознанная пересборка манифеста одним PR. +- **Промежуточные файлы не хранятся.** Файл дня — кэш чистой функции: + потерял — пересчитал. В git снимок не попадает (раздел 5). +- **Обрыв любого режима — переигровка дня целиком**; дедуп склеивает + повторы: `WatchID` детерминированы, повтор — та же строка для + ReplacingMergeTree. +- **Эталонный снимок при старте стенда — через Kafka, пакетным режимом + проигрывателя.** Отдельный механизм заливки не строится: каждый `make up` + бесплатно прогоняет весь конвейер и contract-тест на настоящих данных. + Оговорка «если заливка уйдёт в десятки минут — вернуться к прямой + загрузке» проверена при фиксации чисел: 14 × 50 тыс. ≈ 700 тыс. событий — + расчётно минута-две, запас есть. + +Отклонено с доводами: + +- *Отдельный топик / Kafka как хранилище дней*: офсеты и партиции вне + обещания детерминизма, retention конечен, в git топик не положишь, хеш с + манифестом не сверишь; Kafka на стенде — труба, не хранилище (раздел 7 + мастер-спеки). +- *Файл как обязательная станция доставки*: доигрывание обрыва уже решено + через дедуп, канон держит единственный сериализатор, а не диск; файл + остаётся только там, где нужен кэш. +- *Прямая загрузка снимка в ClickHouse* (и гибрид с ручным заполнением + сырого слоя): второй путь приёма, пустой либо поддельный `stg.hits_raw` — + теряются переобработка дня X по `event_date` и урок виртуальных колонок + «какая нода читала топик». + +## 5. Числа: объёмы, режимы, бюджеты + +Резолюция развилки [«Производительность»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/30); +порядки величин — [исследование #31](../research/2026-08-01-python-batch-generation-speed.md). + +- **Средний модельный день — ~50 тыс. событий**; суточные волны с пиками до + ~2× среднего, будни/выходные; ≈8–12 тыс. сессий, 6–8 тыс. посетителей — + правдоподобный средний магазин. +- **Эталонный снимок — 14 дней**: две полные календарные недели, D0 — + понедельник. Самая короткая длина, при которой есть замороженная зона за + окном K = 7, дышащая зона и две волны недельной сезонности. Удлинение до + месяца — дешёвый ход (пересборка манифеста), если понадобится. +- **В git — только манифест, снимок не хранится.** Снимок генерируется при + `make up` и при проверках: артефакт — кэш чистой функции, кэш в git не + хранят. Манифест несёт паспорт мира, счётчики и хеши по дням; проверки + «пустой git diff» и «пересгенерируй день N — сравни хеш» живут на нём. + Каждый `make up` — живая демонстрация детерминизма. Честная потеря — + страховка от платформенного бага; смягчение — CI гоняет генерацию на + amd64 и arm64. +- **Живой день — ×60 по умолчанию**: модельные сутки за 24 реальные минуты, + суточная волна разворачивается на глазах, темп ~35–100 событий/с. Число — + значение по умолчанию, переопределяется флагом проигрывателя: ускорение — + свойство транспорта, вне обещания воспроизводимости, константой мира не + делается. + +### Бюджеты и способ замера + +Схема двухъярусная — урок ADR 0004: пороги впритык к расчёту на разном +железе кончаются ритуальным удалением проверки. + +Ориентиры на референсной машине — в спеке, без автоматики: + +| Операция | Ориентир | +|---|---| +| Генерация одного дня | секунды | +| Пересборка эталонного мира (14 дней + манифест, без транспорта) | до минуты | +| Заливка снимка при `make up` (Kafka → матвью → ODS) | минуты | +| Лаг живого дня | секунды | + +**Автоматический порог один: полный день (50 тыс. событий) генерируется +≤ 30 с.** Расчёт по планке исследования (~2–5×10⁵ событий/с на ядро) — около +секунды; порог держит машинный разброс ×2–5 и ловит деградацию на 1–2 +порядка: Faker в горячем цикле, случайная квадратичность. Реализация — +pytest-тест с маркером `perf` и таймаутом-обрубанием: обязателен в CI, +исключён из быстрой локальной петли, зовётся отдельной целью при правках +горячего цикла. + +Остальное — наблюдаемость без порогов: `make up` и smoke печатают тайминги +(генерация и доставка отдельно), проигрыватель логирует лаг. Прототип-замер +до этапа 2 не нужен: числа назначены с запасом порядок и больше от планки +исследования, планка подтверждена локальной проверкой на машине стенда; +первый замер настоящего кода — порог этапа 2. + +Отклонено с доводами: + +- *Четыре жёстких CI-ворот на все бюджеты*: машинный разброс против порогов + впритык — повторение истории с памятью (ADR 0004); порог оставлен один, + грубый, между «×5 шума» и «×100 беды». +- *Снимок в git* (статус-кво раздела 8 мастер-спеки): основание из v1 — + медленный генератор — съедено детерминизмом и скоростью; остаётся только + раздутый репозиторий. *Снимок вложением релиза Gitea*: страховка без + раздутия, но лишняя машинерия и вторая правда. +- *Снимок 7 дней*: ни одного замороженного дня, сезонность без сравнения. + *30 дней*: нового урока не даёт — отложено как дешёвое удлинение. +- *×120 / ×1440*: волна смазывается в перемотку либо превращается в пакет с + анимацией — живой режим теряет смысл. + +## 6. Правила кода этапа 2 + +Хвосты резолюций, обязательные для реализации: + +- **Целочисленная случайность** — правило кода, а не пожелание: диапазоны и + выбор из таблиц целыми, плавающие распределения системной математики не + звать; деньги — в целых копейках. +- **Случайные значения — векторно из numpy (PCG64)**; посточный цикл — лишь + там, где логика действительно посточная (цепочки сессий). +- **Faker — только для справочников** (каталог, имена), не в горячем цикле: + он медленнее numpy на 2–3 порядка. +- **Сериализация — через единственный канонический сериализатор** (orjson); + прямых `json.dumps` по коду нет. +- **Распараллеливание — multiprocessing по модельным дням**; внутри дня — + однопоточно, единица работы и так крупная. + +## 7. Расхождения с мастер-спекой + +Внесены в мастер-спеку тем же коммитом, что и эта спека: + +- **Раздел 1.4**: «из контракта выводятся DDL и валидация» заменено на data + contract — хранилище пишется по документации, границу сторожит + contract-тест (раздел 3 здесь). +- **Раздел 8**: артефакт `data/startup_history/` в git заменён манифестом; + снимок генерируется на месте (раздел 5 здесь). Туман «политика + версионирования артефакта» закрыт этим же ходом: версионируется манифест. +- **Раздел 11**: пункт «до этапа 3 зафиксировать требования + производительности» закрыт числами раздела 5. +- Мелкие согласования там, где текст опирался на артефакт в git: источник + переобработки при исчерпании retention Kafka (раздел 7), формулировка + этапа 7 (раздел 9). + +## 8. Хвосты следующим этапам + +- **Этап 5 (Airflow)**: живой день со стороны хранилища — обычный ETL-даг + по расписанию (~раз в 24 минуты); генератор не дорабатывается. +- **Этап 7 (эталонный мир)**: пересборка — это манифест, не артефакт; + CI-генерация на amd64 и arm64. +- **Будущие лабы**: перезаливка дня X пакетным режимом проигрывателя — + готовая демонстрация идемпотентности конвейера. + +## 9. Решается при нарезке этапа 2 (#34) + +Осталось из тумана карты — вопросы уровня тикетов, не развилок: + +- интерфейс запуска генератора (CLI / цели make) и как он делит режимы + проигрывателя; +- формат описания мира и конфигурации (что константа кода, что параметр); +- как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки): + с манифестным решением напрашивается мини-манифест зернового мира — форму + выбрать при нарезке. From 8c69397ccfca112980f6748356dc734f41ca11e4 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 1 Aug 2026 20:49:49 +0300 Subject: [PATCH 4/6] =?UTF-8?q?docs(specs):=20=D0=BF=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D0=BE=D0=BA=20=D0=BF=D0=BE=D1=81=D0=B5=D1=82=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=D0=B8=20=D1=84=D0=B5=D0=B9=D0=BA=D0=B5?= =?UTF-8?q?=D1=80=D1=8B=20=D0=B1=D0=B5=D0=B7=20=D0=B3=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D1=8F=20=E2=80=94=20=D0=B2=D1=8B=D1=87=D0=B8=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=BB=D0=B0=D0=B4=D0=B5=D0=BB=D1=8C=D1=86=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - вычитка спеки на приёмке нашла дыру: состав мира читался как замкнутая труппа без новых посетителей — неправдоподобный магазин. - Что: - раздел 1: состав не замкнут — план задаёт календарь появления кук, доля одноразовых высока; приток — часть плана, не мутация. - раздел 6: «Faker» ослаблен до «посточные фейкеры (Faker, mimesis)», выбор библиотеки — этапу 2; там же оговорка про таблицы-литералы. - раздел 9: к интерфейсу запуска добавлены вопросы «кто зовёт генератор (даги world_init/next_day)» и «в каком контейнере живёт». - CONTEXT.md: в «Состав мира» добавлен календарь появления. - Проверка: - вычитка; правки точечные, решения развилок не пересматриваются. Co-Authored-By: Claude Opus 5 --- CONTEXT.md | 6 +++--- docs/specs/2026-08-01-generator.md | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index b351b7b..de3c1d0 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -12,9 +12,9 @@ _Избегать_: склад, склад данных **Состав мира**: -Постоянная часть мира генератора — популяция посетителей, их привычки, -календарь двухкуковых пар. Чистая функция зерна: вычисляется при старте -любого процесса, между прогонами не хранится. +Постоянная часть мира генератора — популяция посетителей с календарём их +появления, привычки, календарь двухкуковых пар. Чистая функция зерна: +вычисляется при старте любого процесса, между прогонами не хранится. _Избегать_: состояние мира **Ось модельного времени**: diff --git a/docs/specs/2026-08-01-generator.md b/docs/specs/2026-08-01-generator.md index 68ca0c9..537c4b4 100644 --- a/docs/specs/2026-08-01-generator.md +++ b/docs/specs/2026-08-01-generator.md @@ -48,6 +48,13 @@ плана состава, а не копятся в состоянии. Глобальные инварианты («каждый двухкуковый покупатель заказал с обеих кук») гарантируются планом — счётчики манифеста известны до генерации событий. +- **Состав не замкнут: посетители появляются и уходят.** План состава задаёт + календарь появления — у каждого посетителя есть дата первого визита и + профиль возвратов, заметная доля кук одноразовая, как в живом трафике. + Новые посетители появляются на всём протяжении оси: uniq(ClientID) растёт + с горизонтом, DAU и MAU не сходятся в одно число. Приток — часть плана, а + не мутация: счётчики манифеста по-прежнему известны до генерации + (уточнение по вычитке владельца, 2026-08-01). - **Своя ось модельного времени.** Мир рождается в фиксированный день D0 (понедельник — см. раздел 5); реальный календарь в модели не участвует. Между прогонами живут только зерно и позиция на оси: выключенный ноутбук — @@ -278,8 +285,11 @@ pytest-тест с маркером `perf` и таймаутом-обрубан звать; деньги — в целых копейках. - **Случайные значения — векторно из numpy (PCG64)**; посточный цикл — лишь там, где логика действительно посточная (цепочки сессий). -- **Faker — только для справочников** (каталог, имена), не в горячем цикле: - он медленнее numpy на 2–3 порядка. +- **Посточные фейкеры (Faker, mimesis) — только для справочников** (каталог + и прочие справочные строки), не в горячем цикле: они медленнее numpy на + 2–3 порядка. Выбор библиотеки — этапу 2: справочники генерируются один + раз, скорость безразлична; возможно, хватит таблиц-литералов в коде и + фейкер не понадобится вовсе. - **Сериализация — через единственный канонический сериализатор** (orjson); прямых `json.dumps` по коду нет. - **Распараллеливание — multiprocessing по модельным дням**; внутри дня — @@ -315,7 +325,8 @@ pytest-тест с маркером `perf` и таймаутом-обрубан Осталось из тумана карты — вопросы уровня тикетов, не развилок: - интерфейс запуска генератора (CLI / цели make) и как он делит режимы - проигрывателя; + проигрывателя; кто его зовёт в стенде — даги `world_init`/`next_day` из + оценки мастер-спеки (раздел 9) — и в каком контейнере он живёт; - формат описания мира и конфигурации (что константа кода, что параметр); - как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки): с манифестным решением напрашивается мини-манифест зернового мира — форму From 7df5e482b9b4cb9a932fc58e9877aed15f794d6a Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 1 Aug 2026 21:00:18 +0300 Subject: [PATCH 5/6] =?UTF-8?q?docs(specs):=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=85=D0=BE=D0=BB=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20?= =?UTF-8?q?=E2=80=94=20=D0=B4=D0=BE=D0=BB=D0=B3=20=D1=81=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=80=D1=8F=20=D0=B8=20=D0=BF=D0=BE=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D1=8F=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=BE=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - холодное ревью (Fable, свежая сессия) нашло невыполненный хвост тикета #32 и места, где доводы резолюций сжались до непонятности. - Что: - мастер-спека 1.1: «склад» заменён на «хранилище» (хвост #32); CONTEXT.md: DWH в избегаемых, отдельная статья «Пакетный режим». - спека: восстановлены доводы «на маке и в WSL тоже» и «менти упрётся в красный чек манифеста»; обещания про diff привязаны к манифесту; темп ×60 и расчёт порога согласованы с числами разделов; заголовок притока честен про затухание; выход за мандат оговорён в «Зачем». - раздел 9: добавлены числа притока и календарная дата-константа D0; заметка исследования: у Faker единицы «значений/с», не «строк/с». - Проверка: - вычитка; решения развилок не пересматриваются, правки текстовые. Co-Authored-By: Claude Opus 5 --- CONTEXT.md | 6 ++- ...026-08-01-python-batch-generation-speed.md | 2 +- docs/specs/2026-07-30-stand-v2-realism.md | 2 +- docs/specs/2026-08-01-generator.md | 48 ++++++++++++------- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index de3c1d0..31f440d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -9,7 +9,7 @@ Аналитическая база стенда — кластер ClickHouse со слоями STG/ODS/DDS/DM. Принимающая сторона границы «трекер | хранилище»: нормализует имена и стили источников, строится по их документации. -_Избегать_: склад, склад данных +_Избегать_: склад, склад данных, DWH **Состав мира**: Постоянная часть мира генератора — популяция посетителей с календарём их @@ -30,6 +30,10 @@ _Избегать_: состояние мира Проигрывание текущего модельного дня в реальном времени с ускорением; включается по требованию, не постоянный фон. +**Пакетный режим**: +Проигрывание готового дня пачкой, без темпа: заливка снимка при старте +стенда, пересборки и проверки. + **Граница суток**: Единственный структурный шов модели: сессии режутся по ней, слепок заказов снимается на ней, день проживается только целиком. diff --git a/docs/research/2026-08-01-python-batch-generation-speed.md b/docs/research/2026-08-01-python-batch-generation-speed.md index f771856..db9ac88 100644 --- a/docs/research/2026-08-01-python-batch-generation-speed.md +++ b/docs/research/2026-08-01-python-batch-generation-speed.md @@ -19,7 +19,7 @@ | Случайные значения numpy (векторно) | ~10^8 значений/с | таблица numpy | | Векторная сборка колонок события | ~10^6–10^7 строк/с | локальная проверка | | Посточная генерация (random + dict) | ~10^4–10^5 строк/с | локальная проверка | -| Посточная генерация через Faker | ~10^3–10^4 строк/с | бенчмарк mimesis | +| Посточная генерация через Faker | ~10^3–10^4 значений/с | бенчмарк mimesis | | Сериализация orjson (запись ~50 КБ) | ~10^5 док/с | README orjson | | Сериализация stdlib json (та же запись) | ~10^4 док/с | README orjson | diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index e1ce3f7..32c2d87 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -56,7 +56,7 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент - **Имена колонок — как в облачной выгрузке Метрики** (`ClientID`, `UTCEventTime`, `purchaseID`…). Сырой слой хранит имена источника; свои snake_case-имена появляются в DDS/DM. Это учебный пункт: у каждого - источника — свой стиль, нормализует его склад, а не трекер. + источника — свой стиль, нормализует его хранилище, а не трекер. - **Идентификаторы — числовые UInt64** (`WatchID`, `VisitID`, `ClientID`), UUID уходят. Исследование советовало UUID не трогать, но тот совет исходил из цены переделки текущего генератора; v2 пишет генератор заново, цена diff --git a/docs/specs/2026-08-01-generator.md b/docs/specs/2026-08-01-generator.md index 537c4b4..870442d 100644 --- a/docs/specs/2026-08-01-generator.md +++ b/docs/specs/2026-08-01-generator.md @@ -20,7 +20,10 @@ Здесь не переоткрывается решённое мастер-спекой: модель данных события, таксономия, анонимность, механика заказов (границы мандата #14). Генератор -слепков заказов — этап 3: эта спека лишь не должна ему мешать. +слепков заказов — этап 3: эта спека лишь не должна ему мешать. Один +осознанный выход за границы: решение о хранении снимка (раздел 5) формально +касается этапа 7 — расширение подтверждено владельцем в резолюции +«Производительности». ## Целевая картина одним взглядом @@ -48,15 +51,18 @@ плана состава, а не копятся в состоянии. Глобальные инварианты («каждый двухкуковый покупатель заказал с обеих кук») гарантируются планом — счётчики манифеста известны до генерации событий. -- **Состав не замкнут: посетители появляются и уходят.** План состава задаёт - календарь появления — у каждого посетителя есть дата первого визита и - профиль возвратов, заметная доля кук одноразовая, как в живом трафике. - Новые посетители появляются на всём протяжении оси: uniq(ClientID) растёт - с горизонтом, DAU и MAU не сходятся в одно число. Приток — часть плана, а - не мутация: счётчики манифеста по-прежнему известны до генерации - (уточнение по вычитке владельца, 2026-08-01). +- **Состав не замкнут: посетители появляются и затухают.** План состава + задаёт календарь появления — у каждого посетителя есть дата первого + визита и профиль возвратов, включая затухание: заметная доля кук + одноразовая, как в живом трафике. Новые посетители появляются на всём + протяжении оси: uniq(ClientID) растёт с горизонтом, дневная и накопленная + аудитории не сходятся в одно число. Приток — часть плана, а не мутация: + счётчики манифеста по-прежнему известны до генерации (уточнение по + вычитке владельца, 2026-08-01); его числа — раздел 9. - **Своя ось модельного времени.** Мир рождается в фиксированный день D0 (понедельник — см. раздел 5); реальный календарь в модели не участвует. + В `EventDate`/`UTCEventTime` дни оси ложатся конкретными датами, но это + константа мира, от даты запуска не зависящая (значение — раздел 9). Между прогонами живут только зерно и позиция на оси: выключенный ноутбук — мир замер, потом продолжил. - **Два режима движения по одной оси.** Пошаговый — базовый для лаб: старт @@ -89,13 +95,14 @@ - **Обещание — содержимое до байта.** Два прогона с одним зерном дают тот же набор событий: те же `WatchID`/`VisitID`, поля, метки модельного времени. - Артефакт снимка при пересборке побайтово совпадает: канонический порядок - ключей и строк, пустой diff означает «ничего не изменилось». Вне + Снимок при пересборке побайтово совпадает: канонический порядок ключей и + строк; сверка — по хешам манифеста, а два локально пересобранных снимка + сравнимы обычным diff — пустой означает «ничего не изменилось». Вне обещания — транспорт: офсеты и партиции Kafka, какая нода прочитала, `_ingested_at`, темп живого дня. - **Условия обещания.** Детерминизм держится при зафиксированном `uv.lock` - и внутри канонического контейнера (везде Linux; переменная — только - архитектура CPU). Истина — CI на Linux; сходимость любой машины проверяет + и внутри канонического контейнера — то есть везде Linux, на маке и в WSL + тоже; единственная переменная — архитектура CPU. Истина — CI на Linux; сходимость любой машины проверяет скрипт «пересгенерируй день N — сравни хеш с манифестом». Расхождение на любой платформе — баг генератора, а не допуск. - **Раздача зерна — иерархией подпотоков.** Корневое зерно → состав мира; @@ -103,7 +110,8 @@ торговые события, расхождения, опоздания — в фиксированном порядке. По построению: параллельный прогон равен последовательному; продление истории днём N+1 не трогает дни 1…N; правка одного компонента меняет - только его часть снимка — diff читаем. + только его часть снимка — в манифесте меняются хеши только затронутых + дней, дифф двух локальных пересборок читаем. - **Механизм подпотоков — `numpy.random.SeedSequence`.** Сверено через Context7 по документации numpy (2026-08-01): `spawn(n)` порождает детей расширением `spawn_key`, потомок полностью определяется парой @@ -226,10 +234,12 @@ хранят. Манифест несёт паспорт мира, счётчики и хеши по дням; проверки «пустой git diff» и «пересгенерируй день N — сравни хеш» живут на нём. Каждый `make up` — живая демонстрация детерминизма. Честная потеря — - страховка от платформенного бага; смягчение — CI гоняет генерацию на - amd64 и arm64. + страховка на случай платформенного бага: раньше менти с расходящимися + байтами мог взять готовый снимок из git, теперь он упрётся в красный чек + манифеста; смягчение — CI гоняет генерацию на amd64 и arm64. - **Живой день — ×60 по умолчанию**: модельные сутки за 24 реальные минуты, - суточная волна разворачивается на глазах, темп ~35–100 событий/с. Число — + суточная волна разворачивается на глазах; темп в среднем ~35 событий/с, + в пиковые часы сильных дней — до ~100. Число — значение по умолчанию, переопределяется флагом проигрывателя: ускорение — свойство транспорта, вне обещания воспроизводимости, константой мира не делается. @@ -249,7 +259,7 @@ | Лаг живого дня | секунды | **Автоматический порог один: полный день (50 тыс. событий) генерируется -≤ 30 с.** Расчёт по планке исследования (~2–5×10⁵ событий/с на ядро) — около +≤ 30 с.** Расчёт по планке исследования (~2–5×10⁵ событий/с на ядро) — доли секунды; порог держит машинный разброс ×2–5 и ловит деградацию на 1–2 порядка: Faker в горячем цикле, случайная квадратичность. Реализация — pytest-тест с маркером `perf` и таймаутом-обрубанием: обязателен в CI, @@ -327,6 +337,10 @@ pytest-тест с маркером `perf` и таймаутом-обрубан - интерфейс запуска генератора (CLI / цели make) и как он делит режимы проигрывателя; кто его зовёт в стенде — даги `world_init`/`next_day` из оценки мастер-спеки (раздел 9) — и в каком контейнере он живёт; +- числа притока посетителей: доля одноразовых кук и темп появления новых + (раздел 1); +- календарная дата-константа D0: каким числом дни оси ложатся в + `EventDate`/`UTCEventTime`; - формат описания мира и конфигурации (что константа кода, что параметр); - как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки): с манифестным решением напрашивается мини-манифест зернового мира — форму From 41d4dee945359df730d0cc977a9cc7809e647b95 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 1 Aug 2026 21:01:58 +0300 Subject: [PATCH 6/6] =?UTF-8?q?docs(context):=20DWH=20=E2=80=94=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D0=BE=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=BC=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=D1=89=D0=B0,=20=D0=B8=D0=B7=20=D0=B8=D0=B7=D0=B1=D0=B5?= =?UTF-8?q?=D0=B3=D0=B0=D0=B5=D0=BC=D1=8B=D1=85=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - владелец сознательно вывел DWH из списка избегаемых (синоним хранилища), а правка по холодному ревью вернула его по старому комментарию тикета #32 — откат к воле владельца. - Что: - в статье «Хранилище» список «_Избегать_» снова: склад, склад данных. - Проверка: - вычитка CONTEXT.md. Co-Authored-By: Claude Opus 5 --- CONTEXT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTEXT.md b/CONTEXT.md index 31f440d..56eea55 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -9,7 +9,7 @@ Аналитическая база стенда — кластер ClickHouse со слоями STG/ODS/DDS/DM. Принимающая сторона границы «трекер | хранилище»: нормализует имена и стили источников, строится по их документации. -_Избегать_: склад, склад данных, DWH +_Избегать_: склад, склад данных **Состав мира**: Постоянная часть мира генератора — популяция посетителей с календарём их