feat(04-01): add AggregatePcap with timestamp-based windowing and gap-filling
- Add AggregatePcap to aggregate/window.go: reads ClassifiedPackets from channel, assigns to time windows using Timestamp field (D-01), fills gaps with empty snapshots (D-02: silence), fires onSnapshot callback per window (D-07) - Add 7 tests covering basic, multiple windows, gaps, empty input, WindowIndex, class counts, and onSnapshot callback - All tests pass
This commit is contained in:
@@ -6,6 +6,70 @@ import (
|
||||
"github.com/netsynth/netsynth/classify"
|
||||
)
|
||||
|
||||
// AggregatePcap reads all ClassifiedPackets (with Timestamp set), assigns to time windows
|
||||
// using packet timestamps per D-01, fills gap windows with empty snapshots per D-02,
|
||||
// and returns all WindowSnapshots. Calls onSnapshot for each if non-nil (D-07: --verbose).
|
||||
// Returns synchronously after channel closes (pcap processing is finite).
|
||||
func AggregatePcap(events <-chan classify.ClassifiedPacket, windowMs int,
|
||||
onSnapshot func(classify.WindowSnapshot)) []classify.WindowSnapshot {
|
||||
|
||||
// Drain the events channel into a slice.
|
||||
var all []classify.ClassifiedPacket
|
||||
for ev := range events {
|
||||
all = append(all, ev)
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find min and max timestamps (handle non-monotonic pcaps per Pitfall 5).
|
||||
minTS := all[0].Timestamp
|
||||
maxTS := all[0].Timestamp
|
||||
for _, ev := range all[1:] {
|
||||
if ev.Timestamp.Before(minTS) {
|
||||
minTS = ev.Timestamp
|
||||
}
|
||||
if ev.Timestamp.After(maxTS) {
|
||||
maxTS = ev.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the maximum window index.
|
||||
maxWindowIdx := int(maxTS.Sub(minTS).Milliseconds()) / windowMs
|
||||
|
||||
// Create all snapshots including gaps (D-02: gaps are silent).
|
||||
snapshots := make([]classify.WindowSnapshot, maxWindowIdx+1)
|
||||
for i := range snapshots {
|
||||
snapshots[i] = classify.WindowSnapshot{
|
||||
Counts: make(map[classify.TrafficClass]int64),
|
||||
WindowIndex: i,
|
||||
}
|
||||
}
|
||||
|
||||
// Assign each packet to its window.
|
||||
for _, ev := range all {
|
||||
idx := int(ev.Timestamp.Sub(minTS).Milliseconds()) / windowMs
|
||||
// Clamp to [0, maxWindowIdx] for safety.
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx > maxWindowIdx {
|
||||
idx = maxWindowIdx
|
||||
}
|
||||
snapshots[idx].Counts[ev.Class]++
|
||||
snapshots[idx].TotalPackets++
|
||||
}
|
||||
|
||||
// Fire onSnapshot callback for each snapshot in order (D-07: --verbose).
|
||||
if onSnapshot != nil {
|
||||
for _, s := range snapshots {
|
||||
onSnapshot(s)
|
||||
}
|
||||
}
|
||||
|
||||
return snapshots
|
||||
}
|
||||
|
||||
// DefaultWindowMs is the default time window duration in milliseconds.
|
||||
const DefaultWindowMs = 500
|
||||
|
||||
|
||||
Reference in New Issue
Block a user