blob: 20757b2974c8ec30c6660e8449c61f80dd695d99 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package simulation
import (
"container/heap"
)
// Aggregator merges multiple event timelines into a single chronologically ordered timeline.
// It implements the Timeline interface.
//
// Architectural Roles:
// 1. Structural Composer: By implementing Timeline while also consuming multiple Timelines,
// Aggregator allows for arbitrary nesting of simulated elements. A composite UI
// component can use an Aggregator to present events from all its children as
// a single unified timeline to its parent.
// 2. Data Agnosticism: The Aggregator only interacts with the Event.Offset() method
// (the time duration since the start of the simulation). It does not know
// or care about the payload of the events (e.g., BuildEvents vs.
// LogEvents), allowing diverse sources to be interleaved seamlessly.
// 3. Virtualization: Because it relies on Peek(), it supports "future-looking" UI
// elements (like event queue previews) without requiring the simulation state
// to actually advance.
//
// Implementation Note: Aggregator uses an N-way merge strategy backed by a Min-Heap,
// providing O(n log k) performance where n is the number of events peeked and k is
// the number of active timelines.
type Aggregator struct {
timelines []Timeline
}
// NewAggregator creates a new Aggregator that merges the provided child timelines.
func NewAggregator(timelines ...Timeline) *Aggregator {
return &Aggregator{
timelines: timelines,
}
}
// Peek returns up to n events across all child timelines, sorted by Offset().
func (a *Aggregator) Peek(n int) []Event {
if n <= 0 || len(a.timelines) == 0 {
return nil
}
// 1. Initialize a Min-Heap of cursors, one for each child timeline.
h := &cursorHeap{}
heap.Init(h)
for _, t := range a.timelines {
if events := t.Peek(n); len(events) > 0 {
heap.Push(h, &eventCursor{events: events, index: 0})
}
}
// 2. Perform N-way merge.
if h.Len() == 0 {
return nil
}
result := make([]Event, 0, n)
for h.Len() > 0 && len(result) < n {
c := heap.Pop(h).(*eventCursor)
result = append(result, c.events[c.index])
// Advance the cursor for the timeline we just pulled from.
c.index++
if c.index < len(c.events) {
heap.Push(h, c)
}
}
return result
}
// eventCursor tracks the current position within a specific child timeline's Peek result.
type eventCursor struct {
events []Event
index int
}
// cursorHeap implements heap.Interface to maintain the cursors in Offset() time order.
type cursorHeap []*eventCursor
func (h cursorHeap) Len() int { return len(h) }
func (h cursorHeap) Less(i, j int) bool {
// Compare events by their chronological offset from simulation start.
return h[i].events[h[i].index].Offset() < h[j].events[h[j].index].Offset()
}
func (h cursorHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *cursorHeap) Push(x any) { *h = append(*h, x.(*eventCursor)) }
func (h *cursorHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}