Skip to content

feat(data,ingestor): capture per-interval timeline snapshots#541

Merged
ilyanfraimbault merged 5 commits into
developfrom
feat/525-timeline-interval-snapshots
Jun 17, 2026
Merged

feat(data,ingestor): capture per-interval timeline snapshots#541
ilyanfraimbault merged 5 commits into
developfrom
feat/525-timeline-interval-snapshots

Conversation

@ilyanfraimbault

Copy link
Copy Markdown
Owner

What

Capture per-interval timeline snapshots (part of #525), building on the timeline foundation from #538.

New table match_participant_timeline_snapshots

One row per participant per minute mark (5/10/15/20/30), FK-cascaded to matches, unique on (MatchId, ParticipantId, IntervalMinute). Columns: gold, CS, jungle CS, level, xp, damage to champions, cumulative kills, wards placed/killed.

  • Entity + configuration + DbSet + repository (add / delete-by-match / get-by-match), migration, regenerated compiled model.
  • Raw values only — the lead vs lane opponent is intentionally a read-time delta (self-join on the opposing teamPosition), consistent with the matchups design. Not stored.
  • No "end" row: end-of-game gold/CS/kills/damage/vision already live on MatchParticipant.

Ingestion

Design notes

  • Only 5 marks (not every frame) to keep the table bounded — mindful of the storage lessons on match_participants.
  • Vision is a ward-count proxy per interval (no per-frame vision score exists); end-of-game vision score stays on MatchParticipant.

Testing

  • dotnet build --configuration Release clean (Data, Ingestor, IntegrationTests) — 0 warnings.
  • 366 unit tests pass, incl. new TimelineSnapshotBuilderTests (frame selection + cumulative tallies).
  • Integration test DB reset builds its TRUNCATE list from the model, so the new table is handled automatically.

Remaining for #525 (follow-up)

  • Read side: lead-vs-lane-opponent query + API/UI surfacing. Issue stays open.

Part of #525

ilyanfraimbault added 2 commits June 18, 2026 00:26
New child table for per-interval timeline snapshots (issue #525): one row per
participant per minute mark, FK-cascaded to matches, unique on
(MatchId, ParticipantId, IntervalMinute). Adds the entity, configuration, DbSet,
repository (add / delete-by-match / get-by-match), migration and regenerated
compiled model.

Stores raw values only — the lead vs lane opponent is a read-time delta.
Build snapshots at the 5/10/15/20/30-minute marks from the timeline frames:
gold, CS, jungle CS, level, xp, damage to champions, plus cumulative kills and
wards placed/killed up to each mark. Capture ward CreatorId (WARD_PLACED) for the
vision signal. Persisted via delete-then-insert so re-ingestion is idempotent.

Part of #525.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(data,ingestor): capture per-interval timeline snapshots

Implémentation propre et bien délimitée. Le modèle de données, la migration, le compiled model et le repository sont cohérents entre eux. La logique d'idempotence (delete-then-insert) est correctement justifiée : ExecuteDeleteAsync s'exécute dans la transaction ambiante EF, évitant la race condition sur l'index unique. Les tests couvrent les cas clés (sélection de frame, cumul kills/wards, jeu court).

Aucun point bloquant identifié.


Suggestions (non bloquantes) :

  1. SelectFrame — court-circuit possible (ligne 84) : les frames Riot étant en ordre croissant, on peut break dès que delta augmente et éviter de scanner les frames restantes.
  2. CountUpTo — LINQ .Count() O(n) (ligne 110) : les timestamps sont insérés en ordre chronologique, donc la liste est implicitement triée. Un BinarySearch donnerait O(log n). En pratique le volume est trop faible pour que ça compte, mais c'est à noter si la liste grossit.
  3. ToUpperInvariant() — allocations (ligne 33) : les types Riot arrivent déjà en majuscules ; une comparaison OrdinalIgnoreCase directe éviterait une allocation de string par événement.

- SelectFrame breaks early once the distance to the target grows (frames ascending).
- Match event types with OrdinalIgnoreCase instead of allocating via ToUpperInvariant.

Part of #525.
Comment thread backend/tests/TrueMain.UnitTests/TimelineSnapshotBuilderTests.cs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Résumé

Implémentation propre et bien structurée des snapshots par intervalle (issue #525). Le design est cohérent avec le reste du projet : entité, configuration EF, migration, modèle compilé, repository, builder, et tests unitaires sont tous en place et correctement câblés.

Aucun point bloquant. Trois suggestions non bloquantes ont été laissées en commentaires inline.


Suggestions (non bloquantes)

  • SelectFrame — invariant de tri implicite (TimelineSnapshotBuilder.cs:91) : le break early-exit suppose que les frames sont strictement ascendantes. C'est garanti par l'API Riot, mais ce n'est nulle part affirmé dans le code. Un Debug.Assert ou un commentaire liant l'hypothèse à la spec Riot renforcerait la lisibilité à long terme.

  • Atomicité delete-then-insert (TimelineIngestionService.cs:98) : ExecuteDeleteAsync s'exécute immédiatement hors du change-tracker EF. Si SaveChanges échoue sans transaction ambiante, les snapshots existants sont supprimés mais les nouveaux ne sont jamais persistés. En pratique le flag TimelineIngested est rollbacké et la job retentera, donc l'impact est nul — mais ça mérite une note dans le commentaire ou une vérification que le contexte d'appel est bien transactionnel.

  • Couverture des champs Xp et JungleMinionsKilled dans les tests (TimelineSnapshotBuilderTests.cs:68) : le helper Participant ne paramètre pas ces deux champs, donc leurs mappings dans TimelineSnapshotBuilder ne sont jamais exercés avec une valeur non nulle. Ajouter xp et jungleCs au helper permettrait de détecter un éventuel swap de champ.

- Document the ascending-frames precondition SelectFrame's early-exit relies on.
- Document that the delete-then-insert empty window is transient and self-heals
  (TimelineIngested rolls back with a failed SaveChanges, so the match is retried).
- Cover Xp and JungleMinionsKilled in the snapshot builder test to catch field swaps.

Part of #525.
Comment thread backend/tests/TrueMain.UnitTests/TimelineSnapshotBuilderTests.cs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Implémentation propre et bien cadrée de la capture de snapshots par intervalle (issue #525). L'architecture est cohérente avec le reste du projet (delete-then-insert idempotent, FK cascade depuis matches, index unique sur la clé naturelle). Le TimelineSnapshotBuilder est correctement isolé, commenté et testé.

Aucun point bloquant.


Suggestions (non bloquantes)

  • WARD_KILL / plantes de vision (l. 43) — Les destructions de Scryer's Bloom par un joueur émettent un WARD_KILL avec killerId valide, ce qui peut gonfler légèrement WardsKilled. L'approximation est acceptée dans le PR description, mais une note sur la propriété WardsKilled de l'entité clarifierait l'intention.
  • CountUpTo O(E) (l. 117) — Les listes de timestamps sont de fait triées (les events Riot arrivent en ordre chronologique). Un BinarySearch serait possible, mais la différence est négligeable pour les volumes actuels.
  • Test boundary tolérance (l. 46 des tests) — Ajouter un cas à exactement ±30 000 ms (inclus) et 30 001 ms (exclu) sécuriserait la constante FrameMatchToleranceMs contre une future modification.

… kills

- Boundary theory: a frame exactly +30s from a mark is captured, +1ms past is not.
- Note on WardsKilled that vision-plant (Scryer's Bloom) destructions are counted.

Part of #525.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(data,ingestor): capture per-interval timeline snapshots

L'implémentation est solide : entité bien délimitée, index unique comme garde-fou de ré-ingestion, logique de builder propre avec break anticipé dans SelectFrame, et couverture de tests pertinente (sélection de frame + comptage cumulatif + boundary de tolérance).

Aucun point bloquant identifié.


Suggestions (non bloquantes)

  • CountUpTo — scan linéaire (TimelineSnapshotBuilder.cs L114-116) : les listes de timestamps sont implicitement triées (événements Riot chronologiques → Record les ajoute dans l'ordre). Un BinarySearch donnerait O(log N) ; sans impact à la taille actuelle des données.

  • Invariant de transaction non documenté au call site (TimelineIngestionService.cs L100) : ExecuteDeleteAsync participe à la transaction DB ambiante si elle existe. Si un appelant futur enveloppe ApplyTimelineAsync dans BeginTransactionAsync, le comportement « delete commit avant SaveChanges » ne tient plus. Vaut la peine de l'annoter au call site (// Must not be called inside an explicit db transaction).

  • Pas de FK vers match_participants (MatchParticipantTimelineSnapshotConfiguration.cs L29) : aucune contrainte DB n'empêche un ParticipantId invalide pour le match. Risque nul via le pipeline actuel ; à garder en tête si une suppression sélective de participants existait un jour.

@ilyanfraimbault ilyanfraimbault merged commit 4bf34f3 into develop Jun 17, 2026
9 checks passed
@ilyanfraimbault ilyanfraimbault deleted the feat/525-timeline-interval-snapshots branch June 17, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant