Implemented qt port + e2e
This commit is contained in:
+177
-10
@@ -44,6 +44,9 @@ var (
|
||||
durSec = flag.Float64("dur", 4.0, "live recording duration (s)")
|
||||
timeout = flag.Duration("timeout", 90*time.Second, "overall timeout")
|
||||
verbose = flag.Bool("v", false, "log every event")
|
||||
mode = flag.String("mode", "checks", "checks | stress")
|
||||
reqrate = flag.Float64("reqrate", 0, "stress: sustained zoom requests/sec (0 = liveness only)")
|
||||
clientID = flag.Int("clientid", 0, "stress: parallel client index (output suffix)")
|
||||
)
|
||||
|
||||
// ── wire types ────────────────────────────────────────────────────────────
|
||||
@@ -173,13 +176,14 @@ type client struct {
|
||||
ws *websocket.Conn
|
||||
deadline time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
sources []sourceInfo
|
||||
configs map[string][]signalInfo
|
||||
pushes []*pushFrame
|
||||
zooms map[uint32]map[string]points
|
||||
trigSt []string
|
||||
captures []*captureFrame
|
||||
mu sync.Mutex
|
||||
sources []sourceInfo
|
||||
configs map[string][]signalInfo
|
||||
pushes []*pushFrame
|
||||
zooms map[uint32]map[string]points
|
||||
zoomArrival map[uint32]time.Time
|
||||
trigSt []string
|
||||
captures []*captureFrame
|
||||
|
||||
readErr error
|
||||
done chan struct{}
|
||||
@@ -266,6 +270,7 @@ func (c *client) handle(mt int, data []byte) {
|
||||
if json.Unmarshal(data, &body) == nil {
|
||||
c.mu.Lock()
|
||||
c.zooms[ev.ReqID] = body.Signals
|
||||
c.zoomArrival[ev.ReqID] = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case "triggerState":
|
||||
@@ -491,9 +496,10 @@ func main() {
|
||||
|
||||
c := &client{
|
||||
ws: ws, deadline: time.Now().Add(*timeout),
|
||||
configs: map[string][]signalInfo{},
|
||||
zooms: map[uint32]map[string]points{},
|
||||
done: make(chan struct{}),
|
||||
configs: map[string][]signalInfo{},
|
||||
zooms: map[uint32]map[string]points{},
|
||||
zoomArrival: map[uint32]time.Time{},
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go c.reader()
|
||||
out := checksOut{Scenario: *scenario}
|
||||
@@ -522,6 +528,13 @@ func main() {
|
||||
return true
|
||||
})
|
||||
|
||||
// stress mode: record liveness + sustained zoom latency, then exit. The
|
||||
// correctness checks below are skipped (a separate gate framework).
|
||||
if *mode == "stress" {
|
||||
c.runStress(*scenario, *clientID, *durSec, *reqrate, *outDir)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. live recording — the reader goroutine accumulates pushes in the
|
||||
// background, so we simply wait out the recording window (or an early
|
||||
// reader death).
|
||||
@@ -647,6 +660,160 @@ func main() {
|
||||
fmt.Printf("OK chain-client %s: %s + %s\n", *scenario, filepath.Base(recvPath), filepath.Base(cj))
|
||||
}
|
||||
|
||||
// ── stress mode ──────────────────────────────────────────────────────────────
|
||||
|
||||
type stressOut struct {
|
||||
Scenario string `json:"scenario"`
|
||||
ClientID int `json:"clientId"`
|
||||
Frames int `json:"frames"`
|
||||
Signals int `json:"signals"`
|
||||
Monotonic bool `json:"monotonic"`
|
||||
WallClock bool `json:"wallclock"`
|
||||
DurationS float64 `json:"duration"`
|
||||
ReqRate float64 `json:"reqRate"`
|
||||
ZoomCount int `json:"zoomCount"`
|
||||
ZoomFail int `json:"zoomFail"`
|
||||
ZoomP50ms float64 `json:"zoomP50ms"`
|
||||
ZoomP95ms float64 `json:"zoomP95ms"`
|
||||
ZoomMaxms float64 `json:"zoomMaxms"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// busiestKey returns the full "src:sig" key carrying the most samples so far.
|
||||
func (c *client) busiestKey() string {
|
||||
m := c.merged()
|
||||
var busy string
|
||||
bn := 0
|
||||
for k, p := range m {
|
||||
if len(p.T) > bn {
|
||||
bn, busy = len(p.T), k
|
||||
}
|
||||
}
|
||||
return busy
|
||||
}
|
||||
|
||||
// maxTimeFull returns the latest timestamp seen for a full "src:sig" key.
|
||||
func (c *client) maxTimeFull(full string) (float64, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var mx float64
|
||||
found := false
|
||||
for _, f := range c.pushes {
|
||||
if !strings.HasPrefix(full, f.sourceID+":") {
|
||||
continue
|
||||
}
|
||||
name := full[len(f.sourceID)+1:]
|
||||
p, ok := f.signals[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, t := range p.T {
|
||||
if !found || t > mx {
|
||||
mx, found = t, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return mx, found
|
||||
}
|
||||
|
||||
// runStress records liveness for the duration while (optionally) issuing zoom
|
||||
// queries at a sustained rate, measuring round-trip latency. One request is in
|
||||
// flight at a time, so latency reflects the hub's serialised zoom service time
|
||||
// under whatever concurrent live/zoom load the matrix imposes.
|
||||
func (c *client) runStress(scenario string, clientID int, dur, reqrate float64, outDir string) {
|
||||
if !c.waitFor(20*time.Second, func() bool { return len(c.pushes) > 0 }) {
|
||||
fatal("stress: no live push within timeout")
|
||||
}
|
||||
// brief warmup so the busiest signal and a usable time window exist.
|
||||
c.waitFor(500*time.Millisecond, func() bool { return false })
|
||||
key := c.busiestKey()
|
||||
|
||||
start := time.Now()
|
||||
end := start.Add(time.Duration(dur * float64(time.Second)))
|
||||
var lat []float64
|
||||
zoomFail := 0
|
||||
var reqID uint32 = 5000
|
||||
var interval time.Duration
|
||||
if reqrate > 0 {
|
||||
interval = time.Duration(float64(time.Second) / reqrate)
|
||||
}
|
||||
|
||||
for reqrate > 0 && key != "" && time.Now().Before(end) {
|
||||
tick := time.Now()
|
||||
t1, ok := c.maxTimeFull(key)
|
||||
if !ok {
|
||||
c.waitFor(50*time.Millisecond, func() bool { return false })
|
||||
continue
|
||||
}
|
||||
reqID++
|
||||
c.send(map[string]interface{}{
|
||||
"type": "zoom", "reqId": reqID, "t0": t1 - 0.5, "t1": t1,
|
||||
"n": 300, "signals": key,
|
||||
})
|
||||
sendT := time.Now()
|
||||
got := c.waitFor(3*time.Second, func() bool {
|
||||
_, ok := c.zoomArrival[reqID]
|
||||
return ok
|
||||
})
|
||||
if got {
|
||||
c.mu.Lock()
|
||||
at := c.zoomArrival[reqID]
|
||||
c.mu.Unlock()
|
||||
lat = append(lat, at.Sub(sendT).Seconds()*1000.0)
|
||||
} else {
|
||||
zoomFail++
|
||||
}
|
||||
if rem := interval - time.Since(tick); rem > 0 {
|
||||
c.waitFor(rem, func() bool { return false })
|
||||
}
|
||||
}
|
||||
// if no reqrate, simply wait out the remaining liveness window.
|
||||
if reqrate <= 0 {
|
||||
c.waitFor(time.Until(end), func() bool { return false })
|
||||
}
|
||||
|
||||
m := c.merged()
|
||||
now := float64(time.Now().UnixNano()) / 1e9
|
||||
mono, wall := true, true
|
||||
for _, p := range m {
|
||||
for i, t := range p.T {
|
||||
if math.Abs(t-now) > 60.0 {
|
||||
wall = false
|
||||
}
|
||||
if i > 0 && t < p.T[i-1]-1e-9 {
|
||||
mono = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Float64s(lat)
|
||||
pct := func(q float64) float64 {
|
||||
if len(lat) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(q * float64(len(lat)-1))
|
||||
return lat[idx]
|
||||
}
|
||||
so := stressOut{
|
||||
Scenario: scenario, ClientID: clientID,
|
||||
Frames: c.nPushes(), Signals: len(m), Monotonic: mono,
|
||||
WallClock: wall, DurationS: dur, ReqRate: reqrate, Key: key,
|
||||
ZoomCount: len(lat), ZoomFail: zoomFail,
|
||||
ZoomP50ms: pct(0.50), ZoomP95ms: pct(0.95),
|
||||
}
|
||||
if len(lat) > 0 {
|
||||
so.ZoomMaxms = lat[len(lat)-1]
|
||||
}
|
||||
path := filepath.Join(outDir,
|
||||
fmt.Sprintf("stress_%s_c%d.json", scenario, clientID))
|
||||
b, _ := json.MarshalIndent(so, "", " ")
|
||||
if err := os.WriteFile(path, b, 0o644); err != nil {
|
||||
fatal("write stress: %v", err)
|
||||
}
|
||||
log.Printf("stress: frames=%d signals=%d zoom n=%d fail=%d p50=%.1fms p95=%.1fms max=%.1fms",
|
||||
so.Frames, so.Signals, so.ZoomCount, so.ZoomFail, so.ZoomP50ms, so.ZoomP95ms, so.ZoomMaxms)
|
||||
fmt.Printf("OK stress %s c%d: %s\n", scenario, clientID, filepath.Base(path))
|
||||
}
|
||||
|
||||
// runTrigger configures one edge/mode trigger, arms it, and records the result.
|
||||
func (c *client) runTrigger(key, edge, mode string, thr float64) trigCheck {
|
||||
tc := trigCheck{Edge: edge, Mode: mode, Key: key}
|
||||
|
||||
Reference in New Issue
Block a user