feat(generator): откалиброван поток steady-stream генератора
- Зачем: - поток генератора должен соответствовать модели интенсивности и профилю сида перед реализацией состояния версии 2. - Что: - событийный бюджет тика переведён в рождения визитов через ожидаемую среднюю длину визита. - дефолты интенсивности и обычный docker-compose запуск синхронизированы с целевыми 30 событиями в минуту. - добавлены статистические проверки длины визита, воронки, новых пользователей, межсессионных пауз и долгого окна потока. - обновлены README, OPERATIONS и карточка задачи 05. - Проверка: - uv run --with-requirements generator/requirements.txt pytest generator/tests -q. - git diff --check.
This commit is contained in:
@@ -16,13 +16,13 @@ class Config:
|
||||
default_factory=lambda: int(os.getenv("GEN_TICK_SECONDS", "5"))
|
||||
)
|
||||
lambda_base_per_min: int = field(
|
||||
default_factory=lambda: int(os.getenv("GEN_LAMBDA_BASE_PER_MIN", "200"))
|
||||
default_factory=lambda: int(os.getenv("GEN_LAMBDA_BASE_PER_MIN", "30"))
|
||||
)
|
||||
jitter_pct: int = field(
|
||||
default_factory=lambda: int(os.getenv("GEN_JITTER_PCT", "20"))
|
||||
)
|
||||
min_events_per_tick: int = field(
|
||||
default_factory=lambda: int(os.getenv("GEN_MIN_EVENTS_PER_TICK", "5"))
|
||||
default_factory=lambda: int(os.getenv("GEN_MIN_EVENTS_PER_TICK", "1"))
|
||||
)
|
||||
max_events_per_tick: int = field(
|
||||
default_factory=lambda: int(os.getenv("GEN_MAX_EVENTS_PER_TICK", "50"))
|
||||
|
||||
@@ -22,51 +22,57 @@ PAGE_START_DISTRIBUTION = [
|
||||
|
||||
PAGE_TRANSITIONS = {
|
||||
"/home": [
|
||||
("/home", 0.40),
|
||||
("/home", 0.41),
|
||||
("/product_a", 0.28),
|
||||
("/product_b", 0.18),
|
||||
("/cart", 0.04),
|
||||
(None, 0.10),
|
||||
],
|
||||
"/product_a": [
|
||||
("/home", 0.30),
|
||||
("/product_a", 0.16),
|
||||
("/product_b", 0.18),
|
||||
("/cart", 0.27),
|
||||
(None, 0.09),
|
||||
],
|
||||
"/product_b": [
|
||||
("/home", 0.32),
|
||||
("/product_a", 0.15),
|
||||
"/product_a": [
|
||||
("/home", 0.34),
|
||||
("/product_a", 0.13),
|
||||
("/product_b", 0.18),
|
||||
("/cart", 0.27),
|
||||
(None, 0.08),
|
||||
],
|
||||
"/product_b": [
|
||||
("/home", 0.36),
|
||||
("/product_a", 0.15),
|
||||
("/product_b", 0.15),
|
||||
("/cart", 0.27),
|
||||
(None, 0.07),
|
||||
],
|
||||
"/cart": [
|
||||
("/home", 0.20),
|
||||
("/product_a", 0.12),
|
||||
("/product_b", 0.10),
|
||||
("/cart", 0.10),
|
||||
("/cart", 0.11),
|
||||
("/payment", 0.42),
|
||||
(None, 0.06),
|
||||
(None, 0.05),
|
||||
],
|
||||
"/payment": [
|
||||
("/home", 0.12),
|
||||
("/cart", 0.24),
|
||||
("/payment", 0.12),
|
||||
("/payment", 0.14),
|
||||
("/confirmation", 0.38),
|
||||
(None, 0.14),
|
||||
(None, 0.12),
|
||||
],
|
||||
"/confirmation": [
|
||||
("/home", 0.35),
|
||||
("/home", 0.37),
|
||||
("/product_a", 0.15),
|
||||
("/product_b", 0.10),
|
||||
("/confirmation", 0.05),
|
||||
(None, 0.35),
|
||||
(None, 0.33),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# Калибровочный ориентир из профиля сида: средний визит около 10 событий.
|
||||
# Тиковый слой использует это число, чтобы переводить событийный бюджет в
|
||||
# рождения визитов; тесты ниже защищают фактическую длину визита от дрейфа.
|
||||
EXPECTED_VISIT_EVENTS = 10.0
|
||||
|
||||
|
||||
class EventGenerator:
|
||||
"""Генератор одного связанного визита."""
|
||||
|
||||
@@ -118,7 +124,7 @@ class EventGenerator:
|
||||
|
||||
def _visit_pause_seconds(self) -> float:
|
||||
"""Разыгрывает паузу между событиями визита."""
|
||||
pause = self.rng.lognormvariate(math.log(45.0), 1.0)
|
||||
pause = self.rng.lognormvariate(math.log(20.0), 0.9)
|
||||
return max(1.0, min(pause, 29 * 60.0))
|
||||
|
||||
def _hour_factor(self) -> float:
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from clickstream_generator.generation import EventGenerator
|
||||
from clickstream_generator.generation import EXPECTED_VISIT_EVENTS, EventGenerator
|
||||
|
||||
|
||||
TOPICS = ("browser_events", "location_events", "device_events", "geo_events")
|
||||
@@ -136,7 +136,7 @@ class TickStreamGenerator:
|
||||
self.generator = generator
|
||||
self.active_visits: list[ActiveVisit] = []
|
||||
self.population = UserPopulation(generator)
|
||||
self._pending_event_budget = 0
|
||||
self._pending_visit_births = 0.0
|
||||
|
||||
@property
|
||||
def population_size(self) -> int:
|
||||
@@ -165,18 +165,18 @@ class TickStreamGenerator:
|
||||
|
||||
def _birth_visits(self, event_budget: int, tick_time: datetime) -> None:
|
||||
if len(self.active_visits) >= self.generator.config.max_active_sessions:
|
||||
self._pending_event_budget = 0
|
||||
self._pending_visit_births = 0.0
|
||||
return
|
||||
|
||||
self._pending_event_budget += max(0, event_budget)
|
||||
self._pending_visit_births += max(0, event_budget) / EXPECTED_VISIT_EVENTS
|
||||
|
||||
while (
|
||||
self._pending_event_budget > 0
|
||||
self._pending_visit_births >= 1.0
|
||||
and len(self.active_visits) < self.generator.config.max_active_sessions
|
||||
):
|
||||
user = self.population.choose_for_visit(tick_time)
|
||||
if user is None:
|
||||
self._pending_event_budget = 0
|
||||
self._pending_visit_births = 0.0
|
||||
return
|
||||
|
||||
visit_batch = self.generator.generate_batch(
|
||||
@@ -196,10 +196,10 @@ class TickStreamGenerator:
|
||||
self.active_visits.append(
|
||||
ActiveVisit(batch=visit_batch, timestamps=timestamps, user=user)
|
||||
)
|
||||
self._pending_event_budget -= len(timestamps)
|
||||
self._pending_visit_births -= 1.0
|
||||
|
||||
if len(self.active_visits) >= self.generator.config.max_active_sessions:
|
||||
self._pending_event_budget = 0
|
||||
self._pending_visit_births = 0.0
|
||||
|
||||
def _release_due_events(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user