perf(forecast): per-request memoization cache — §22 cold build (#1129) #1160

Merged
bot-backend merged 1 commit from perf/forecast-parallel-horizons-1129 into main 2026-06-08 05:26:28 +00:00
Collaborator

Что

#1129: холодный §22-прогноз ~215-233с (~4 мин) на проде → per-request memoization-кэш. Это «Прогноз рассчитывается…» 4 минуты на демо.

Профиль (корень)

§9.x-слои массово пере-вызывают ОДНИ И ТЕ ЖЕ горизонт/сегмент-инвариантные БД-загрузки с идентичными аргументами: get_competitors ×69, market_metrics ×124, get_monthly_macro ×290 на ОДИН отчёт. Это case (c): общая дорогая загрузка пере-выполняется N раз.

Решение (безопасное — без конкурентности)

  • forecast_request_cache.pyforecast_cache() (ContextVar, открывается ОДИН раз в orchestrator на сборку отчёта) + @cached(key_builder) на дорогих §9.x-loader'ах. Уникальная загрузка считается ОДИН раз, переиспользуется тот же frozen-инстанс.
  • Вывод побайтово идентичен: все мемоизируемые продьюсеры — frozen-dataclass / read-only Pydantic, callers только читают; кэш per-report, сбрасывается на выходе; вне сборки отчёта — no-op (standalone API/тесты не затронуты). Детерминизм §16/§26 сохранён.

Качество

  • code-reviewer: APPROVE — оба gate: ключи корректны (каждый key-builder включает все result-affecting args; horizon_months/rate_future/window_months keyed где нужно) + mutation-safe (8 продьюсеров frozen, все consumers read-only). Нет вектора «тихо неверного прогноза».
  • 58 тестов кэша: key-discrimination call-counting (regression-guard: убери arg из ключа → тест падает), no-op-outside, per-report isolation, reentrancy, frozen-canary, amplification proof (реальный get_monthly_macro: 5 идентичных вызовов → 1 roundtrip, ×N→1). 1265 forecast/cache-тестов зелёные, ruff+mypy чисто, без новых deps. NON-colliding (forecasting/ — моя полоса; параллельный трек в parcels.py/scrapers).

Замер

Механизм доказан тестом (×N→1). Wall-clock замерю на проде post-deploy (baseline 215-233с → цель ≤90с).

Refs #1129.

## Что #1129: холодный §22-прогноз **~215-233с (~4 мин)** на проде → per-request memoization-кэш. Это «Прогноз рассчитывается…» 4 минуты на демо. ## Профиль (корень) §9.x-слои массово пере-вызывают ОДНИ И ТЕ ЖЕ горизонт/сегмент-инвариантные БД-загрузки с идентичными аргументами: **get_competitors ×69, market_metrics ×124, get_monthly_macro ×290** на ОДИН отчёт. Это case (c): общая дорогая загрузка пере-выполняется N раз. ## Решение (безопасное — без конкурентности) - `forecast_request_cache.py` — `forecast_cache()` (ContextVar, открывается ОДИН раз в orchestrator на сборку отчёта) + `@cached(key_builder)` на дорогих §9.x-loader'ах. Уникальная загрузка считается ОДИН раз, переиспользуется тот же frozen-инстанс. - **Вывод побайтово идентичен:** все мемоизируемые продьюсеры — frozen-dataclass / read-only Pydantic, callers только читают; кэш per-report, сбрасывается на выходе; вне сборки отчёта — no-op (standalone API/тесты не затронуты). Детерминизм §16/§26 сохранён. ## Качество - `code-reviewer`: ✅ APPROVE — оба gate: **ключи корректны** (каждый key-builder включает все result-affecting args; horizon_months/rate_future/window_months keyed где нужно) + **mutation-safe** (8 продьюсеров frozen, все consumers read-only). Нет вектора «тихо неверного прогноза». - **58 тестов** кэша: key-discrimination call-counting (regression-guard: убери arg из ключа → тест падает), no-op-outside, per-report isolation, reentrancy, frozen-canary, **amplification proof** (реальный get_monthly_macro: 5 идентичных вызовов → 1 roundtrip, ×N→1). 1265 forecast/cache-тестов зелёные, ruff+mypy чисто, без новых deps. NON-colliding (forecasting/ — моя полоса; параллельный трек в parcels.py/scrapers). ## Замер Механизм доказан тестом (×N→1). Wall-clock замерю на проде post-deploy (baseline 215-233с → цель ≤90с). Refs #1129.
bot-backend added the
performance
scope/backend
GG-форсайт
site-finder
labels 2026-06-08 05:26:25 +00:00
bot-backend added 1 commit 2026-06-08 05:26:26 +00:00
perf(forecast): per-request memoization cache for §22 cold build (#1129)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m32s
ac04b156a7
Cold §22 forecast measured ~215-233s on prod: §9.x layers re-execute the same
horizon/segment-invariant DB loads with identical args hundreds of times per
report (profiled: get_competitors x69, market_metrics x124, get_monthly_macro
x290). Add a per-report ContextVar cache (forecast_cache(), opened once in the
orchestrator) + @cached(key_builder) on the expensive §9.x loaders so each
unique load runs ONCE and reuses the same frozen, read-only instance.

Output is byte-identical (memoized producers are frozen dataclasses / read-only
Pydantic, callers never mutate; cache is per-report, discarded on exit; no-op
outside the report build). No concurrency, no signature changes.

- forecast_request_cache.py: ContextVar cache + cached() decorator (no-op
  outside context, reentrant, _MISS sentinel for cached None)
- @cached on competitors/future_supply/market_metrics/macro_series/
  sales_series/macro_coefficient/demand_normalization/regression loaders
- orchestrator: wrap build_site_finder_report in forecast_cache()
- 58 tests: key discrimination (call-counting regression guard), no-op-outside,
  per-report isolation, reentrancy, frozen-producer canary, amplification proof
  (real get_monthly_macro xN->1)

code-reviewer APPROVE (keys correct, mutation-safe, output identical). 1265
forecast/cache tests green. No new deps. Refs #1129.
bot-backend merged commit 8206a0b067 into main 2026-06-08 05:26:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1160
No description provided.