2025 05 17.RuntimeMetricsDeepDive - SergeiGolos/wod-wiki GitHub Wiki

Wod.Wiki - Runtime Metrics Deep Dive

1. Introduction

1.1. Purpose

This document provides a detailed explanation of the runtime metrics system within Wod.Wiki. It covers how metrics are defined, generated, aggregated, tracked internally, and displayed in the user interface. Understanding this system is crucial for developers working on the runtime engine, UI components related to results display, or extending workout tracking capabilities.

1.2. Importance of Metrics

Runtime metrics are the quantitative record of a user's performance during a workout. They provide valuable feedback, enable progress tracking, and form the basis for analytics. Accurate and comprehensive metrics are key to the user experience.

2. RuntimeMetric Data Structure (timer.types.ts)

The core data structure for representing metrics is RuntimeMetric.

  • RuntimeMetric:

    • effort: string: The name or description of the exercise or effort.
    • repetitions: number: The count of repetitions performed.
    • resistance?: MetricValue: Optional. The weight or resistance used.
    • distance?: MetricValue: Optional. The distance covered.
  • MetricValue:

    • value: number: The numerical value of the metric.
    • unit: string: The unit of measurement (e.g., "kg", "lb", "m", "km", "ft").

This structure ensures consistency in how different types of performance data are handled throughout the application.

3. Metrics Generation and MetricsContext

  • Initialization: RuntimeMetric objects are typically initialized from PrecompiledNode sources (via Fragments like EffortFragment, RepFragment, etc.) using reader functions (e.g., getMetrics).

  • RuntimeBlock Role: The RuntimeBlock base class plays a key role. During its initialization (often in the constructor or onEnter), it creates a MetricsContext for the block instance. This might involve a MetricsFactory for consistent creation.

  • MetricsContext: Each block instance possesses a MetricsContext (this.context.metrics) which holds:

    • The RuntimeMetric data for the block.
    • Is governed by a MetricsRelationshipType which defines how its metrics relate to its parent and child blocks.
  • MetricsRelationshipType:

    • ADD: Metrics from child blocks are summed up and added to this block's metrics (e.g., RootBlock, TimedGroupBlock).
    • MULTIPLY: Metrics from this block are multiplied by a factor (e.g., number of rounds in a RepeatingBlock). The RepeatingBlock itself might also generate "Round Complete" type metrics.
    • INHERIT: The block directly uses or passes through the metrics defined at its level, typically from its source PrecompiledNode.

4. ResultSpan and Metric Emission

  • Creation & Management: A ResultSpan is created for each execution instance of a block. This is typically managed by the BlockContext under the direction of RuntimeBlock's lifecycle methods (onEnter, onNext, onLeave).

  • Enhancement: Concrete block types override a method within RuntimeBlock (e.g., enhanceResultSpan or similar, or directly in onLeave) to populate their ResultSpan with specific metrics derived during their execution, or by aggregating metrics from their children (obtained by calling child.getMetrics()).

  • Emission: Upon block completion (usually in onLeave), the finalized ResultSpan (containing duration, effort, reps, resistance, distance, etc.) is emitted, typically via a WriteResultAction. This action makes the metrics available to the UI and for aggregation by parent blocks.

5. Aggregation Logic in Parent Blocks

  • RepeatingBlock (Handles Rounds): The RepeatingBlock's MetricsContext relationship is typically MULTIPLY if it's simply repeating a single child or a composed set of children. However, its behavior becomes more nuanced based on the groupType implied by its structure or child LapFragments (+, - operators).

    • Compose (groupType: 'compose', from + operator):

      • Conceptual Aggregation: All child blocks within the compose group are executed as a single unit for each round of the RepeatingBlock.
      • Metrics: For one round of the RepeatingBlock, the metrics from all composed children are typically ADDED together. This sum (representing one full composed iteration) is then MULTIPLIED by the total number of rounds in the RepeatingBlock.
      • Example: (3) rounds of (A + B). Metrics(A) and Metrics(B) are ADDED. (Metrics(A) + Metrics(B)) is then MULTIPLIED by 3.
    • Round-Robin (groupType: 'round', from - operator):

      • Conceptual Aggregation: Each round of the RepeatingBlock cycles through one child from the round-robin set.
      • Metrics: The RepeatingBlock's total metrics would be the SUM of metrics from each child executed across all rounds. If RepeatingBlock itself has a MetricsRelationshipType of ADD, it would sum the ResultSpan metrics from each child activation.
      • Example: (3) rounds of (A - B - C). This means A runs, then B, then C (that's 1 parent round). Then A, B, C again (2nd parent round), etc. The RepeatingBlock's total metrics = Metrics(A1)+Metrics(B1)+Metrics(C1) + Metrics(A2)+Metrics(B2)+Metrics(C2) + Metrics(A3)+Metrics(B3)+Metrics(C3).
    • Repeat (Default, groupType: 'repeat'):

      • Conceptual Aggregation: Each child element individually completes all rounds of the parent RepeatingBlock before the next child begins.
      • Metrics: This is the most complex. If child A (itself potentially a RepeatingBlock or a simple EffortBlock) completes all its parent-defined rounds, its total metrics are calculated. Then child B does the same. The parent RepeatingBlock would then SUM the total metrics from child A's full execution and child B's full execution.
      • Example: (Parent: 2 rounds) of (ChildA: (3 rounds of X) then ChildB: 5 Y).
        • ChildA runs (3 rounds of X) for ParentRound1, then ChildA runs (3 rounds of X) for ParentRound2. Total for A = Metrics(3X) * 2 (if parent relationship is MULTIPLY, or just Metrics(3X) + Metrics(3X) if ADD).
        • Then ChildB runs 5 Y for ParentRound1, then ChildB runs 5 Y for ParentRound2. Total for B = Metrics(5Y) * 2 (or summed).
        • The overall RepeatingBlock metrics would be Total(A) + Total(B).
  • TimedGroupBlock (e.g., AMRAPs/EMOMs):

    • Typically has a MetricsRelationshipType of ADD.
    • It sums the metrics from all ResultSpans generated by its child blocks during its timed duration.
    • If it contains children that are part of a compose (+) or round-robin (-) structure (though less common for a top-level AMRAP container), those sub-groups would aggregate their metrics as described above, and their total would then be added to the TimedGroupBlock's sum.

6. Internal State Tracking for Metrics

  • RuntimeStack: As blocks are pushed onto and popped from the RuntimeStack, their BlockContext (containing MetricsContext and ResultSpan data) is managed.

  • Data Flow:

    1. Raw fragments are parsed into PrecompiledNodes.
    2. RuntimeJit strategies compile PrecompiledNodes into IRuntimeBlocks.
    3. RuntimeBlock initializes MetricsContext for each block, using initial metrics from the node.
    4. As a block executes (enter, next, leave phases):
      • It may update its own MetricsContext based on its logic.
      • It may trigger child blocks.
      • Upon completion of a child, the parent block may aggregate the child's ResultSpan metrics into its own MetricsContext according to its MetricsRelationshipType.
    5. When a block itself completes, its enhanceResultSpan() method finalizes its ResultSpan, which is then stored/emitted.
  • BlockContext: Holds the MetricsContext and the list of ResultSpans generated by a block (if it can run multiple times or has sub-elements that generate spans).

7. UI Display and Expectations

The UI presents metrics to the user in a clear and understandable manner.

  • Key UI Components:

    • WodResults: Displays a summary or detailed breakdown of completed workout metrics.
    • WodTable: May show metrics in a tabular format, possibly per round or per exercise.
    • EventsView: Could show a log of ResultSpan emissions, providing a granular view of metrics generation.
    • WodResultsSectionHead: Shows summary metrics with even spacing.
  • Visual Cues:

    • Icons are used to differentiate metric types (e.g., 💪 for resistance, 📏 for distance).
    • Units are always displayed alongside values.
  • Real-time Updates: Some UI elements might update metrics in real-time as blocks complete and emit ResultSpans, while others might update only at the end of the workout.

  • Clarity: The UI should clearly distinguish between metrics for individual efforts, rounds, and the total workout.

8. Metrics in Group Operations

The group operators affect how metrics are aggregated:

  • Round-Robin Operator (-): Metrics for each child item are typically logged individually as they are completed in their respective turns within the parent round. Aggregation at the parent level would sum these individual ResultSpans.

  • Compose Operator (+): All children are executed as a single unit per parent round. The MetricsContext of the parent (or a TimedGroupBlock if applicable) would use ADD relationship to sum metrics from all children for that composed unit.

  • Repeat (No Operator): Each child individually goes through all parent rounds. The RepeatingBlock (parent) would likely use a MULTIPLY relationship for each child's metrics over the rounds, or sum the ResultSpans from each child's full set of repetitions.

The specific IRuntimeBlock and IRuntimeBlockStrategy implementations for these group operations will define the precise metric aggregation logic.

9. Example Scenario: 3 Rounds of (Push Press 5 reps @60kg, 10 Cal Row)

  1. Parsing:

    • Parent PrecompiledNode (Repeating, 3 rounds)
      • Child PrecompiledNode ("Push Press", 5 reps, 60kg) -> EffortFragment, RepFragment, ResistanceFragment
      • Child PrecompiledNode ("10 Cal Row") -> EffortFragment, RepFragment (value 10, unit "Cal")
  2. Runtime - Round 1:

    • RepeatingBlock (3 rounds) enters. MetricsContext (MULTIPLY).
    • EffortBlock ("Push Press") enters. MetricsContext (INHERIT).
      • Base metrics: { effort: "Push Press", reps: 5, resistance: {value: 60, unit: "kg"} }.
      • EffortBlock leaves. ResultSpan_PP1 created with these metrics.
    • EffortBlock ("10 Cal Row") enters. MetricsContext (INHERIT).
      • Base metrics: { effort: "Row", reps: 10, unit: "Cal" }.
      • EffortBlock leaves. ResultSpan_Row1 created.
    • RepeatingBlock's MetricsContext might aggregate ResultSpan_PP1 and ResultSpan_Row1 for Round 1 summary if needed, or just store them.
  3. Runtime - Rounds 2 & 3: Similar flow, creating ResultSpan_PP2, ResultSpan_Row2, ResultSpan_PP3, ResultSpan_Row3.

  4. RepeatingBlock Completion:

    • When RepeatingBlock leaves, its enhanceResultSpan() is called.
    • It calculates its total metrics. This could be:
      • Sum of all child ResultSpans: (PP1+Row1) + (PP2+Row2) + (PP3+Row3).
      • Or, if its base metric was "1 round", it might be { effort: "Rounds", reps: 3 } and the child spans are associated.
    • A final ResultSpan_RepeatingBlock is generated.
  5. UI Display:

    • Could show:
      • Total workout summary from RootBlock (which aggregated ResultSpan_RepeatingBlock).
      • Per-round breakdown (PP1+Row1, PP2+Row2, PP3+Row3).
      • Individual exercise contributions (total Push Press reps/weight, total Row cals).

10. Extensibility

  • Adding New Metric Types:
    1. Extend the RuntimeMetric interface in timer.types.ts.
    2. Update MetricValue if necessary.
    3. Modify parser/lexer and fragment types to capture the new data.
    4. Update reader functions (like getMetrics) to populate the new metric fields.
    5. Adjust MetricsContext and relevant block enhanceResultSpan() methods to handle/aggregate the new metric.
    6. Update UI components to display the new metric type.
⚠️ **GitHub.com Fallback** ⚠️