| // 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 |
| } |