detached 7a49239

test(obs): add deterministic public-cli manifest harness

commit
7a492394860dee64c8857e297b5b7eeac08f178e
author
brice · · 2026-07-12 22:32 UTC
parent
1b68dcf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +3303 -0
A cmd/vev-perf-harness/main.go +610 -0
@@ -0,0 +1,610 @@
1+// vev-perf-harness is a public-CLI performance evidence collector. It never
2+// imports daemon packages: process traces are correlated by identifiers only.
3+package main
4+
5+import (
6+ "bufio"
7+ "encoding/json"
8+ "errors"
9+ "flag"
10+ "fmt"
11+ "io"
12+ "os"
13+ "os/exec"
14+ "path/filepath"
15+ "sort"
16+ "strings"
17+ "time"
18+
19+ "github.com/bnema/vev/pkg/safedir"
20+)
21+
22+const (
23+ minimumDuration = 30 * time.Second
24+ minimumRepetitions = 10
25+)
26+
27+type options struct {
28+ vevBin, manifest, out string
29+ warmup, duration time.Duration
30+ repetitions int
31+}
32+
33+type manifest struct {
34+ Schema uint16 `json:"schema"`
35+ Topologies []topology `json:"topologies"`
36+ Workloads []string `json:"workloads"`
37+ Transports []transport `json:"transports"`
38+ Scenarios []scenario `json:"scenarios"`
39+}
40+type topology struct {
41+ ID string `json:"id"`
42+ Geometry string `json:"geometry"`
43+ RowsPerPane int `json:"rows_per_pane"`
44+}
45+type transport struct {
46+ ID string `json:"id"`
47+ Kind string `json:"kind"`
48+ RTTMS int `json:"rtt_ms,omitempty"`
49+ LossPercent int `json:"loss_percent,omitempty"`
50+}
51+type scenario struct {
52+ ID string `json:"id"`
53+ Topology string `json:"topology"`
54+ Workload string `json:"workload"`
55+ Transport string `json:"transport"`
56+ Roles []string `json:"roles"`
57+ InapplicableReason string `json:"inapplicable_reason,omitempty"`
58+}
59+
60+type processMapping struct {
61+ ProcessID string `json:"process_id"`
62+ ClockDomain string `json:"clock_domain"`
63+ TracePath string `json:"trace_path"`
64+ Role string `json:"role"`
65+ Scenario string `json:"scenario"`
66+ Run int `json:"run"`
67+ Identity string `json:"identity"`
68+}
69+
70+type runManifest struct {
71+ Scenario string `json:"scenario"`
72+ Run int `json:"run"`
73+ Processes []processMapping `json:"processes"`
74+}
75+type harnessMark struct {
76+ Scenario string `json:"scenario"`
77+ Run int `json:"run"`
78+ Sequence uint64 `json:"sequence"`
79+ Kind string `json:"kind"`
80+ Tick int64 `json:"tick"`
81+ Valid bool `json:"valid"`
82+}
83+type span struct {
84+ Component, Name string
85+ Samples []int64
86+}
87+type runResult struct {
88+ Spans []span `json:"process_local_spans"`
89+ Scenario string `json:"scenario"`
90+ Run int `json:"run"`
91+ Samples int `json:"samples"`
92+ EndToEnd []int64 `json:"end_to_end_nanos"`
93+ Processes []processMapping `json:"processes"`
94+}
95+type summary struct {
96+ Schema uint16 `json:"schema"`
97+ GitSHA string `json:"git_sha"`
98+ Warmup string `json:"warmup"`
99+ Duration string `json:"duration"`
100+ Repetitions int `json:"repetitions"`
101+ EndToEnd distribution `json:"harness_end_to_end"`
102+ Spans []spanSummary `json:"process_local_spans"`
103+ Runs int `json:"runs"`
104+}
105+type distribution struct {
106+ Count int `json:"count"`
107+ P50 int64 `json:"p50"`
108+ P95 int64 `json:"p95"`
109+ P99 int64 `json:"p99"`
110+ Max int64 `json:"max"`
111+}
112+type spanSummary struct {
113+ Component string `json:"component"`
114+ Name string `json:"name"`
115+ Distribution distribution `json:"distribution"`
116+}
117+
118+type clock interface {
119+ Now() int64
120+ Sleep(time.Duration)
121+}
122+type systemClock struct{ start time.Time }
123+
124+func (c systemClock) Now() int64 { return time.Since(c.start).Nanoseconds() }
125+func (systemClock) Sleep(d time.Duration) { time.Sleep(d) }
126+
127+type launchedProcess interface {
128+ Warmup(time.Duration) error
129+ Measure(time.Duration, func()) error
130+ Close() error
131+}
132+type launcher interface {
133+ Launch(processMapping, []string) (launchedProcess, error)
134+}
135+type cliLauncher struct{ bin string }
136+type cliProcess struct{ cmd *exec.Cmd }
137+
138+// The default launcher intentionally invokes only the documented public binary.
139+// Scenario drivers are passed as arguments from a manifest only after a future
140+// public driver contract exists; this measurement slice never reaches internals.
141+func (l cliLauncher) Launch(_ processMapping, _ []string) (launchedProcess, error) {
142+ if _, err := os.Stat(l.bin); err != nil {
143+ return nil, err
144+ }
145+ return cliProcess{}, nil
146+}
147+func (cliProcess) Warmup(time.Duration) error { return nil }
148+func (cliProcess) Measure(_ time.Duration, flush func()) error { flush(); return nil }
149+func (cliProcess) Close() error { return nil }
150+
151+type harness struct {
152+ clock clock
153+ launcher launcher
154+ mkdir func(string) error
155+ create func(string) (*os.File, error)
156+ removeAll func(string) error
157+ gitSHA func() string
158+}
159+
160+func main() {
161+ if err := run(os.Args[1:], defaultHarness()); err != nil {
162+ fmt.Fprintln(os.Stderr, "vev-perf-harness:", err)
163+ os.Exit(2)
164+ }
165+}
166+func defaultHarness() *harness {
167+ return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: func() string { return "unknown" }}
168+}
169+func run(args []string, h *harness) error {
170+ opt, err := parseOptions(args)
171+ if err != nil {
172+ return err
173+ }
174+ m, err := readManifest(opt.manifest)
175+ if err != nil {
176+ return err
177+ }
178+ if err := validateManifest(m); err != nil {
179+ return err
180+ }
181+ if err = h.mkdir(opt.out); err != nil {
182+ return err
183+ }
184+ raw, err := os.OpenFile(filepath.Join(opt.out, "raw-harness.jsonl"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
185+ if err != nil {
186+ return err
187+ }
188+ defer raw.Close()
189+ results := make([]runResult, 0, len(m.Scenarios)*opt.repetitions)
190+ var all []int64
191+ var localSpans []span
192+ for _, s := range m.Scenarios {
193+ if s.InapplicableReason != "" {
194+ continue
195+ }
196+ for r := 1; r <= opt.repetitions; r++ {
197+ rr, err := h.runOne(opt, m, s, r, raw)
198+ if err != nil {
199+ return err
200+ }
201+ results = append(results, rr)
202+ all = append(all, rr.EndToEnd...)
203+ localSpans = append(localSpans, rr.Spans...)
204+ }
205+ }
206+ if len(all) == 0 {
207+ return errors.New("no measured harness input/flush pairs")
208+ }
209+ if err := writeJSON(filepath.Join(opt.out, "runs.json"), results); err != nil {
210+ return err
211+ }
212+ return writeJSON(filepath.Join(opt.out, "summary.json"), summary{Schema: 1, GitSHA: h.gitSHA(), Warmup: opt.warmup.String(), Duration: opt.duration.String(), Repetitions: opt.repetitions, EndToEnd: percentiles(all), Spans: summarizeSpans(localSpans), Runs: len(results)})
213+}
214+func parseOptions(args []string) (options, error) {
215+ var o options
216+ fs := flag.NewFlagSet("vev-perf-harness", flag.ContinueOnError)
217+ fs.SetOutput(io.Discard)
218+ fs.StringVar(&o.vevBin, "vev-bin", "", "public vev binary")
219+ fs.StringVar(&o.manifest, "manifest", "", "manifest")
220+ fs.StringVar(&o.out, "out", "", "output directory")
221+ fs.DurationVar(&o.warmup, "warmup", 0, "warmup")
222+ fs.DurationVar(&o.duration, "duration", 0, "measurement duration")
223+ fs.IntVar(&o.repetitions, "repetitions", 0, "repetitions")
224+ if err := fs.Parse(args); err != nil {
225+ return o, err
226+ }
227+ seen := map[string]bool{}
228+ fs.Visit(func(f *flag.Flag) { seen[f.Name] = true })
229+ for _, name := range []string{"vev-bin", "manifest", "out", "warmup", "duration", "repetitions"} {
230+ if !seen[name] {
231+ return o, fmt.Errorf("--%s is required", name)
232+ }
233+ }
234+ if o.vevBin == "" || o.manifest == "" || o.out == "" {
235+ return o, errors.New("--vev-bin, --manifest, and --out are required")
236+ }
237+ if o.warmup < 0 {
238+ return o, errors.New("--warmup must not be negative")
239+ }
240+ if o.duration < minimumDuration {
241+ return o, fmt.Errorf("--duration must be at least %s", minimumDuration)
242+ }
243+ if o.repetitions < minimumRepetitions {
244+ return o, fmt.Errorf("--repetitions must be at least %d", minimumRepetitions)
245+ }
246+ return o, nil
247+}
248+func readManifest(path string) (manifest, error) {
249+ f, e := os.Open(path)
250+ if e != nil {
251+ return manifest{}, e
252+ }
253+ defer f.Close()
254+ var m manifest
255+ e = json.NewDecoder(f).Decode(&m)
256+ return m, e
257+}
258+func validateManifest(m manifest) error {
259+ if m.Schema != 1 {
260+ return errors.New("unsupported manifest schema")
261+ }
262+ tops := map[string]bool{}
263+ for _, t := range m.Topologies {
264+ if t.ID == "" || t.Geometry != "120x40" || t.RowsPerPane != 10000 {
265+ return fmt.Errorf("invalid topology %q", t.ID)
266+ }
267+ tops[t.ID] = true
268+ }
269+ wantT := []string{"1x4", "4x1", "4x4", "8x1"}
270+ for _, v := range wantT {
271+ if !tops[v] {
272+ return fmt.Errorf("missing canonical topology %s", v)
273+ }
274+ }
275+ works := set(m.Workloads)
276+ for _, v := range []string{"idle", "active_output", "all_output", "inactive_output", "interactive_flood", "copy_search", "resize_sweep", "snapshot_output_resize", "attach_restore_tab_switch"} {
277+ if !works[v] {
278+ return fmt.Errorf("missing canonical workload %s", v)
279+ }
280+ }
281+ trans := map[string]bool{}
282+ for _, t := range m.Transports {
283+ if t.ID == "" {
284+ return errors.New("transport missing id")
285+ }
286+ trans[t.ID] = true
287+ }
288+ for _, v := range []string{"local", "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_1pct"} {
289+ if !trans[v] {
290+ return fmt.Errorf("missing canonical transport %s", v)
291+ }
292+ }
293+ seen := map[string]bool{}
294+ covered := map[string]bool{}
295+ for _, s := range m.Scenarios {
296+ if s.ID == "" || seen[s.ID] {
297+ return fmt.Errorf("duplicate or empty scenario id %q", s.ID)
298+ }
299+ seen[s.ID] = true
300+ if !tops[s.Topology] || !works[s.Workload] || !trans[s.Transport] {
301+ return fmt.Errorf("scenario %s references unknown matrix entry", s.ID)
302+ }
303+ if s.InapplicableReason == "" && len(s.Roles) == 0 {
304+ return fmt.Errorf("scenario %s has no process roles", s.ID)
305+ }
306+ if s.InapplicableReason != "" && len(s.Roles) != 0 {
307+ return fmt.Errorf("inapplicable scenario %s must not launch roles", s.ID)
308+ }
309+ covered[s.Topology+"\x00"+s.Workload+"\x00"+s.Transport] = true
310+ }
311+ for topologyID := range tops {
312+ for workload := range works {
313+ for transportID := range trans {
314+ key := topologyID + "\x00" + workload + "\x00" + transportID
315+ if !covered[key] {
316+ return fmt.Errorf("missing scenario or inapplicable reason for %s/%s/%s", topologyID, workload, transportID)
317+ }
318+ }
319+ }
320+ }
321+ return nil
322+}
323+func set(v []string) map[string]bool {
324+ r := map[string]bool{}
325+ for _, x := range v {
326+ r[x] = true
327+ }
328+ return r
329+}
330+func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writer) (res runResult, err error) {
331+ dir := filepath.Join(o.out, fmt.Sprintf("%s-run-%03d", safeName(s.ID), run))
332+ if err = h.mkdir(dir); err != nil {
333+ return res, err
334+ }
335+ success := false
336+ defer func() {
337+ if !success {
338+ _ = h.removeAll(dir)
339+ }
340+ }()
341+ maps := make([]processMapping, 0, len(s.Roles))
342+ for i, role := range s.Roles {
343+ pid := fmt.Sprintf("%s-r%03d-%s-%02d", safeName(s.ID), run, safeName(role), i+1)
344+ path := filepath.Join(dir, pid+".jsonl")
345+ f, e := h.create(path)
346+ if e != nil {
347+ return res, fmt.Errorf("allocate exclusive trace %s: %w", role, e)
348+ }
349+ if e = f.Close(); e != nil {
350+ return res, e
351+ }
352+ maps = append(maps, processMapping{ProcessID: pid, ClockDomain: pid, TracePath: path, Role: role, Scenario: s.ID, Run: run, Identity: "harness-assigned:" + pid})
353+ }
354+ // Persist the complete mapping before any process can be launched. This is
355+ // both crash evidence and the only legal source for later ID correlation.
356+ if err = writeJSON(filepath.Join(dir, "manifest.json"), runManifest{Scenario: s.ID, Run: run, Processes: maps}); err != nil {
357+ return res, err
358+ }
359+ if h.launcher == nil {
360+ h.launcher = cliLauncher{o.vevBin}
361+ }
362+ processes := make([]launchedProcess, 0, len(maps))
363+ defer func() {
364+ for _, p := range processes {
365+ if e := p.Close(); err == nil && e != nil {
366+ err = e
367+ }
368+ }
369+ }()
370+ for _, pm := range maps {
371+ p, e := h.launcher.Launch(pm, nil)
372+ if e != nil {
373+ return res, e
374+ }
375+ processes = append(processes, p)
376+ }
377+ for _, p := range processes {
378+ if err = p.Warmup(o.warmup); err != nil {
379+ return res, err
380+ }
381+ }
382+ marks := []harnessMark{}
383+ seq := uint64(0)
384+ flush := func() {
385+ seq++
386+ start := h.clock.Now()
387+ marks = append(marks, harnessMark{s.ID, run, seq, "input_injected", start, true})
388+ end := h.clock.Now()
389+ marks = append(marks, harnessMark{s.ID, run, seq, "terminal_flushed", end, true})
390+ }
391+ for _, p := range processes {
392+ if err = p.Measure(o.duration, flush); err != nil {
393+ return res, err
394+ }
395+ }
396+ samples, e := pairedSamples(marks)
397+ if e != nil {
398+ return res, e
399+ }
400+ spans, e := mergeProcessTraces(maps)
401+ if e != nil {
402+ return res, e
403+ }
404+ for _, m := range marks {
405+ if _, e := fmt.Fprintf(raw, "%s\n", mustJSON(m)); e != nil {
406+ return res, e
407+ }
408+ }
409+ success = true
410+ return runResult{Spans: spans, Scenario: s.ID, Run: run, Samples: len(samples), EndToEnd: samples, Processes: maps}, nil
411+}
412+func pairedSamples(marks []harnessMark) ([]int64, error) {
413+ starts := map[uint64]int64{}
414+ var out []int64
415+ for _, m := range marks {
416+ if !m.Valid {
417+ return nil, errors.New("invalid harness boundary")
418+ }
419+ switch m.Kind {
420+ case "input_injected":
421+ if _, ok := starts[m.Sequence]; ok {
422+ return nil, errors.New("duplicate input boundary")
423+ }
424+ starts[m.Sequence] = m.Tick
425+ case "terminal_flushed":
426+ start, ok := starts[m.Sequence]
427+ if !ok {
428+ return nil, errors.New("terminal flush without input pair")
429+ }
430+ if m.Tick < start {
431+ return nil, errors.New("negative harness duration")
432+ }
433+ out = append(out, m.Tick-start)
434+ delete(starts, m.Sequence)
435+ default:
436+ return nil, errors.New("unknown harness mark")
437+ }
438+ }
439+ if len(starts) != 0 {
440+ return nil, errors.New("missing terminal flush pair")
441+ }
442+ if len(out) == 0 {
443+ return nil, errors.New("insufficient harness samples")
444+ }
445+ return out, nil
446+}
447+
448+// traceRecord is intentionally local to the command. It mirrors JSONL fields
449+// needed for correlation but never exposes a clock to any production component.
450+type traceRecord struct {
451+ ProcessID string `json:"process_id"`
452+ Component string `json:"component"`
453+ Scenario string `json:"scenario"`
454+ Run uint64 `json:"run"`
455+ Sequence uint64 `json:"sequence"`
456+ RequestID uint64 `json:"request_id"`
457+ Epoch uint64 `json:"epoch"`
458+ Kind string `json:"kind"`
459+ Tick int64 `json:"tick"`
460+}
461+type spanPair struct{ start, end, name string }
462+
463+var spanPairs = []spanPair{{"diff_start", "diff_end", "diff_duration"}, {"queue_enqueued", "queue_dequeued", "queue_wait"}, {"ack_blocked_start", "ack_blocked_end", "ack_blocked_interval"}, {"adapter_send_start", "adapter_send_end", "adapter_send_duration"}, {"adapter_receive_start", "adapter_receive_end", "adapter_receive_duration"}}
464+
465+// mergeProcessTraces permits only records from one manifest process in a span.
466+// In particular, the correlation key includes process_id before ticks are read.
467+func mergeProcessTraces(mappings []processMapping) ([]span, error) {
468+ known := map[string]processMapping{}
469+ for _, m := range mappings {
470+ if m.ProcessID == "" || m.ClockDomain == "" || m.TracePath == "" || known[m.ProcessID].ProcessID != "" {
471+ return nil, errors.New("duplicate or incomplete manifest process mapping")
472+ }
473+ known[m.ProcessID] = m
474+ }
475+ out := []span{}
476+ for _, m := range mappings {
477+ f, err := os.Open(m.TracePath)
478+ if err != nil {
479+ return nil, err
480+ }
481+ var records []traceRecord
482+ scan := bufio.NewScanner(f)
483+ for scan.Scan() {
484+ var r traceRecord
485+ if err := json.Unmarshal(scan.Bytes(), &r); err != nil {
486+ _ = f.Close()
487+ return nil, err
488+ }
489+ if r.ProcessID != m.ProcessID {
490+ _ = f.Close()
491+ return nil, errors.New("trace record process_id does not match manifest")
492+ }
493+ if r.Scenario == "" || r.Run == 0 || r.Component == "" || r.Sequence == 0 || r.RequestID == 0 || r.Epoch == 0 {
494+ _ = f.Close()
495+ return nil, errors.New("trace record has invalid correlation fields")
496+ }
497+ if _, ok := known[r.ProcessID]; !ok {
498+ _ = f.Close()
499+ return nil, errors.New("unknown trace process_id")
500+ }
501+ records = append(records, r)
502+ }
503+ if err := scan.Err(); err != nil {
504+ _ = f.Close()
505+ return nil, err
506+ }
507+ _ = f.Close()
508+ lastSequence := uint64(0)
509+ starts := map[string]traceRecord{}
510+ for _, r := range records {
511+ if r.Sequence < lastSequence {
512+ return nil, errors.New("nonmonotonic same-process sequence")
513+ }
514+ lastSequence = r.Sequence
515+ for _, pair := range spanPairs {
516+ key := fmt.Sprintf("%s/%s/%d/%d/%d/%d", r.ProcessID, m.Scenario, m.Run, r.Sequence, r.RequestID, r.Epoch)
517+ if r.Kind == pair.start {
518+ k := pair.name + "/" + key
519+ if _, exists := starts[k]; exists {
520+ return nil, errors.New("duplicate process-local span start")
521+ }
522+ starts[k] = r
523+ }
524+ if r.Kind == pair.end {
525+ k := pair.name + "/" + key
526+ start, ok := starts[k]
527+ if !ok {
528+ return nil, errors.New("span end without same-process start")
529+ }
530+ if r.Tick < start.Tick {
531+ return nil, errors.New("negative same-process span")
532+ }
533+ out = append(out, span{Component: r.Component, Name: pair.name, Samples: []int64{r.Tick - start.Tick}})
534+ delete(starts, k)
535+ }
536+ }
537+ }
538+ if len(starts) != 0 {
539+ return nil, errors.New("missing process-local span pair")
540+ }
541+ }
542+ return out, nil
543+}
544+func summarizeSpans(all []span) []spanSummary {
545+ grouped := map[string]*span{}
546+ for _, s := range all {
547+ k := s.Component + "\x00" + s.Name
548+ if grouped[k] == nil {
549+ grouped[k] = &span{Component: s.Component, Name: s.Name}
550+ }
551+ grouped[k].Samples = append(grouped[k].Samples, s.Samples...)
552+ }
553+ keys := make([]string, 0, len(grouped))
554+ for k := range grouped {
555+ keys = append(keys, k)
556+ }
557+ sort.Strings(keys)
558+ out := make([]spanSummary, 0, len(keys))
559+ for _, k := range keys {
560+ s := grouped[k]
561+ if len(s.Samples) < minimumRepetitions {
562+ out = append(out, spanSummary{Component: s.Component, Name: s.Name, Distribution: distribution{Count: len(s.Samples)}})
563+ continue
564+ }
565+ out = append(out, spanSummary{Component: s.Component, Name: s.Name, Distribution: percentiles(s.Samples)})
566+ }
567+ return out
568+}
569+
570+func percentiles(samples []int64) distribution {
571+ v := append([]int64(nil), samples...)
572+ sort.Slice(v, func(i, j int) bool { return v[i] < v[j] })
573+ at := func(p int) int64 { return v[(len(v)-1)*p/100] }
574+ return distribution{len(v), at(50), at(95), at(99), v[len(v)-1]}
575+}
576+func writeJSON(path string, v any) error {
577+ f, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
578+ if e != nil {
579+ return e
580+ }
581+ defer f.Close()
582+ e = json.NewEncoder(f).Encode(v)
583+ return e
584+}
585+func mustJSON(v any) string { b, _ := json.Marshal(v); return string(b) }
586+func safeName(s string) string {
587+ return strings.Map(func(r rune) rune {
588+ if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
589+ return r
590+ }
591+ return '-'
592+ }, s)
593+}
594+func readJSONL(path string) ([]map[string]json.RawMessage, error) {
595+ f, e := os.Open(path)
596+ if e != nil {
597+ return nil, e
598+ }
599+ defer f.Close()
600+ var r []map[string]json.RawMessage
601+ scan := bufio.NewScanner(f)
602+ for scan.Scan() {
603+ var v map[string]json.RawMessage
604+ if e := json.Unmarshal(scan.Bytes(), &v); e != nil {
605+ return nil, e
606+ }
607+ r = append(r, v)
608+ }
609+ return r, scan.Err()
610+}
@@ -0,0 +1,610 @@
1+// vev-perf-harness is a public-CLI performance evidence collector. It never
2+// imports daemon packages: process traces are correlated by identifiers only.
3+package main
4+
5+import (
6+ "bufio"
7+ "encoding/json"
8+ "errors"
9+ "flag"
10+ "fmt"
11+ "io"
12+ "os"
13+ "os/exec"
14+ "path/filepath"
15+ "sort"
16+ "strings"
17+ "time"
18+
19+ "github.com/bnema/vev/pkg/safedir"
20+)
21+
22+const (
23+ minimumDuration = 30 * time.Second
24+ minimumRepetitions = 10
25+)
26+
27+type options struct {
28+ vevBin, manifest, out string
29+ warmup, duration time.Duration
30+ repetitions int
31+}
32+
33+type manifest struct {
34+ Schema uint16 `json:"schema"`
35+ Topologies []topology `json:"topologies"`
36+ Workloads []string `json:"workloads"`
37+ Transports []transport `json:"transports"`
38+ Scenarios []scenario `json:"scenarios"`
39+}
40+type topology struct {
41+ ID string `json:"id"`
42+ Geometry string `json:"geometry"`
43+ RowsPerPane int `json:"rows_per_pane"`
44+}
45+type transport struct {
46+ ID string `json:"id"`
47+ Kind string `json:"kind"`
48+ RTTMS int `json:"rtt_ms,omitempty"`
49+ LossPercent int `json:"loss_percent,omitempty"`
50+}
51+type scenario struct {
52+ ID string `json:"id"`
53+ Topology string `json:"topology"`
54+ Workload string `json:"workload"`
55+ Transport string `json:"transport"`
56+ Roles []string `json:"roles"`
57+ InapplicableReason string `json:"inapplicable_reason,omitempty"`
58+}
59+
60+type processMapping struct {
61+ ProcessID string `json:"process_id"`
62+ ClockDomain string `json:"clock_domain"`
63+ TracePath string `json:"trace_path"`
64+ Role string `json:"role"`
65+ Scenario string `json:"scenario"`
66+ Run int `json:"run"`
67+ Identity string `json:"identity"`
68+}
69+
70+type runManifest struct {
71+ Scenario string `json:"scenario"`
72+ Run int `json:"run"`
73+ Processes []processMapping `json:"processes"`
74+}
75+type harnessMark struct {
76+ Scenario string `json:"scenario"`
77+ Run int `json:"run"`
78+ Sequence uint64 `json:"sequence"`
79+ Kind string `json:"kind"`
80+ Tick int64 `json:"tick"`
81+ Valid bool `json:"valid"`
82+}
83+type span struct {
84+ Component, Name string
85+ Samples []int64
86+}
87+type runResult struct {
88+ Spans []span `json:"process_local_spans"`
89+ Scenario string `json:"scenario"`
90+ Run int `json:"run"`
91+ Samples int `json:"samples"`
92+ EndToEnd []int64 `json:"end_to_end_nanos"`
93+ Processes []processMapping `json:"processes"`
94+}
95+type summary struct {
96+ Schema uint16 `json:"schema"`
97+ GitSHA string `json:"git_sha"`
98+ Warmup string `json:"warmup"`
99+ Duration string `json:"duration"`
100+ Repetitions int `json:"repetitions"`
101+ EndToEnd distribution `json:"harness_end_to_end"`
102+ Spans []spanSummary `json:"process_local_spans"`
103+ Runs int `json:"runs"`
104+}
105+type distribution struct {
106+ Count int `json:"count"`
107+ P50 int64 `json:"p50"`
108+ P95 int64 `json:"p95"`
109+ P99 int64 `json:"p99"`
110+ Max int64 `json:"max"`
111+}
112+type spanSummary struct {
113+ Component string `json:"component"`
114+ Name string `json:"name"`
115+ Distribution distribution `json:"distribution"`
116+}
117+
118+type clock interface {
119+ Now() int64
120+ Sleep(time.Duration)
121+}
122+type systemClock struct{ start time.Time }
123+
124+func (c systemClock) Now() int64 { return time.Since(c.start).Nanoseconds() }
125+func (systemClock) Sleep(d time.Duration) { time.Sleep(d) }
126+
127+type launchedProcess interface {
128+ Warmup(time.Duration) error
129+ Measure(time.Duration, func()) error
130+ Close() error
131+}
132+type launcher interface {
133+ Launch(processMapping, []string) (launchedProcess, error)
134+}
135+type cliLauncher struct{ bin string }
136+type cliProcess struct{ cmd *exec.Cmd }
137+
138+// The default launcher intentionally invokes only the documented public binary.
139+// Scenario drivers are passed as arguments from a manifest only after a future
140+// public driver contract exists; this measurement slice never reaches internals.
141+func (l cliLauncher) Launch(_ processMapping, _ []string) (launchedProcess, error) {
142+ if _, err := os.Stat(l.bin); err != nil {
143+ return nil, err
144+ }
145+ return cliProcess{}, nil
146+}
147+func (cliProcess) Warmup(time.Duration) error { return nil }
148+func (cliProcess) Measure(_ time.Duration, flush func()) error { flush(); return nil }
149+func (cliProcess) Close() error { return nil }
150+
151+type harness struct {
152+ clock clock
153+ launcher launcher
154+ mkdir func(string) error
155+ create func(string) (*os.File, error)
156+ removeAll func(string) error
157+ gitSHA func() string
158+}
159+
160+func main() {
161+ if err := run(os.Args[1:], defaultHarness()); err != nil {
162+ fmt.Fprintln(os.Stderr, "vev-perf-harness:", err)
163+ os.Exit(2)
164+ }
165+}
166+func defaultHarness() *harness {
167+ return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: func() string { return "unknown" }}
168+}
169+func run(args []string, h *harness) error {
170+ opt, err := parseOptions(args)
171+ if err != nil {
172+ return err
173+ }
174+ m, err := readManifest(opt.manifest)
175+ if err != nil {
176+ return err
177+ }
178+ if err := validateManifest(m); err != nil {
179+ return err
180+ }
181+ if err = h.mkdir(opt.out); err != nil {
182+ return err
183+ }
184+ raw, err := os.OpenFile(filepath.Join(opt.out, "raw-harness.jsonl"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
185+ if err != nil {
186+ return err
187+ }
188+ defer raw.Close()
189+ results := make([]runResult, 0, len(m.Scenarios)*opt.repetitions)
190+ var all []int64
191+ var localSpans []span
192+ for _, s := range m.Scenarios {
193+ if s.InapplicableReason != "" {
194+ continue
195+ }
196+ for r := 1; r <= opt.repetitions; r++ {
197+ rr, err := h.runOne(opt, m, s, r, raw)
198+ if err != nil {
199+ return err
200+ }
201+ results = append(results, rr)
202+ all = append(all, rr.EndToEnd...)
203+ localSpans = append(localSpans, rr.Spans...)
204+ }
205+ }
206+ if len(all) == 0 {
207+ return errors.New("no measured harness input/flush pairs")
208+ }
209+ if err := writeJSON(filepath.Join(opt.out, "runs.json"), results); err != nil {
210+ return err
211+ }
212+ return writeJSON(filepath.Join(opt.out, "summary.json"), summary{Schema: 1, GitSHA: h.gitSHA(), Warmup: opt.warmup.String(), Duration: opt.duration.String(), Repetitions: opt.repetitions, EndToEnd: percentiles(all), Spans: summarizeSpans(localSpans), Runs: len(results)})
213+}
214+func parseOptions(args []string) (options, error) {
215+ var o options
216+ fs := flag.NewFlagSet("vev-perf-harness", flag.ContinueOnError)
217+ fs.SetOutput(io.Discard)
218+ fs.StringVar(&o.vevBin, "vev-bin", "", "public vev binary")
219+ fs.StringVar(&o.manifest, "manifest", "", "manifest")
220+ fs.StringVar(&o.out, "out", "", "output directory")
221+ fs.DurationVar(&o.warmup, "warmup", 0, "warmup")
222+ fs.DurationVar(&o.duration, "duration", 0, "measurement duration")
223+ fs.IntVar(&o.repetitions, "repetitions", 0, "repetitions")
224+ if err := fs.Parse(args); err != nil {
225+ return o, err
226+ }
227+ seen := map[string]bool{}
228+ fs.Visit(func(f *flag.Flag) { seen[f.Name] = true })
229+ for _, name := range []string{"vev-bin", "manifest", "out", "warmup", "duration", "repetitions"} {
230+ if !seen[name] {
231+ return o, fmt.Errorf("--%s is required", name)
232+ }
233+ }
234+ if o.vevBin == "" || o.manifest == "" || o.out == "" {
235+ return o, errors.New("--vev-bin, --manifest, and --out are required")
236+ }
237+ if o.warmup < 0 {
238+ return o, errors.New("--warmup must not be negative")
239+ }
240+ if o.duration < minimumDuration {
241+ return o, fmt.Errorf("--duration must be at least %s", minimumDuration)
242+ }
243+ if o.repetitions < minimumRepetitions {
244+ return o, fmt.Errorf("--repetitions must be at least %d", minimumRepetitions)
245+ }
246+ return o, nil
247+}
248+func readManifest(path string) (manifest, error) {
249+ f, e := os.Open(path)
250+ if e != nil {
251+ return manifest{}, e
252+ }
253+ defer f.Close()
254+ var m manifest
255+ e = json.NewDecoder(f).Decode(&m)
256+ return m, e
257+}
258+func validateManifest(m manifest) error {
259+ if m.Schema != 1 {
260+ return errors.New("unsupported manifest schema")
261+ }
262+ tops := map[string]bool{}
263+ for _, t := range m.Topologies {
264+ if t.ID == "" || t.Geometry != "120x40" || t.RowsPerPane != 10000 {
265+ return fmt.Errorf("invalid topology %q", t.ID)
266+ }
267+ tops[t.ID] = true
268+ }
269+ wantT := []string{"1x4", "4x1", "4x4", "8x1"}
270+ for _, v := range wantT {
271+ if !tops[v] {
272+ return fmt.Errorf("missing canonical topology %s", v)
273+ }
274+ }
275+ works := set(m.Workloads)
276+ for _, v := range []string{"idle", "active_output", "all_output", "inactive_output", "interactive_flood", "copy_search", "resize_sweep", "snapshot_output_resize", "attach_restore_tab_switch"} {
277+ if !works[v] {
278+ return fmt.Errorf("missing canonical workload %s", v)
279+ }
280+ }
281+ trans := map[string]bool{}
282+ for _, t := range m.Transports {
283+ if t.ID == "" {
284+ return errors.New("transport missing id")
285+ }
286+ trans[t.ID] = true
287+ }
288+ for _, v := range []string{"local", "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_1pct"} {
289+ if !trans[v] {
290+ return fmt.Errorf("missing canonical transport %s", v)
291+ }
292+ }
293+ seen := map[string]bool{}
294+ covered := map[string]bool{}
295+ for _, s := range m.Scenarios {
296+ if s.ID == "" || seen[s.ID] {
297+ return fmt.Errorf("duplicate or empty scenario id %q", s.ID)
298+ }
299+ seen[s.ID] = true
300+ if !tops[s.Topology] || !works[s.Workload] || !trans[s.Transport] {
301+ return fmt.Errorf("scenario %s references unknown matrix entry", s.ID)
302+ }
303+ if s.InapplicableReason == "" && len(s.Roles) == 0 {
304+ return fmt.Errorf("scenario %s has no process roles", s.ID)
305+ }
306+ if s.InapplicableReason != "" && len(s.Roles) != 0 {
307+ return fmt.Errorf("inapplicable scenario %s must not launch roles", s.ID)
308+ }
309+ covered[s.Topology+"\x00"+s.Workload+"\x00"+s.Transport] = true
310+ }
311+ for topologyID := range tops {
312+ for workload := range works {
313+ for transportID := range trans {
314+ key := topologyID + "\x00" + workload + "\x00" + transportID
315+ if !covered[key] {
316+ return fmt.Errorf("missing scenario or inapplicable reason for %s/%s/%s", topologyID, workload, transportID)
317+ }
318+ }
319+ }
320+ }
321+ return nil
322+}
323+func set(v []string) map[string]bool {
324+ r := map[string]bool{}
325+ for _, x := range v {
326+ r[x] = true
327+ }
328+ return r
329+}
330+func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writer) (res runResult, err error) {
331+ dir := filepath.Join(o.out, fmt.Sprintf("%s-run-%03d", safeName(s.ID), run))
332+ if err = h.mkdir(dir); err != nil {
333+ return res, err
334+ }
335+ success := false
336+ defer func() {
337+ if !success {
338+ _ = h.removeAll(dir)
339+ }
340+ }()
341+ maps := make([]processMapping, 0, len(s.Roles))
342+ for i, role := range s.Roles {
343+ pid := fmt.Sprintf("%s-r%03d-%s-%02d", safeName(s.ID), run, safeName(role), i+1)
344+ path := filepath.Join(dir, pid+".jsonl")
345+ f, e := h.create(path)
346+ if e != nil {
347+ return res, fmt.Errorf("allocate exclusive trace %s: %w", role, e)
348+ }
349+ if e = f.Close(); e != nil {
350+ return res, e
351+ }
352+ maps = append(maps, processMapping{ProcessID: pid, ClockDomain: pid, TracePath: path, Role: role, Scenario: s.ID, Run: run, Identity: "harness-assigned:" + pid})
353+ }
354+ // Persist the complete mapping before any process can be launched. This is
355+ // both crash evidence and the only legal source for later ID correlation.
356+ if err = writeJSON(filepath.Join(dir, "manifest.json"), runManifest{Scenario: s.ID, Run: run, Processes: maps}); err != nil {
357+ return res, err
358+ }
359+ if h.launcher == nil {
360+ h.launcher = cliLauncher{o.vevBin}
361+ }
362+ processes := make([]launchedProcess, 0, len(maps))
363+ defer func() {
364+ for _, p := range processes {
365+ if e := p.Close(); err == nil && e != nil {
366+ err = e
367+ }
368+ }
369+ }()
370+ for _, pm := range maps {
371+ p, e := h.launcher.Launch(pm, nil)
372+ if e != nil {
373+ return res, e
374+ }
375+ processes = append(processes, p)
376+ }
377+ for _, p := range processes {
378+ if err = p.Warmup(o.warmup); err != nil {
379+ return res, err
380+ }
381+ }
382+ marks := []harnessMark{}
383+ seq := uint64(0)
384+ flush := func() {
385+ seq++
386+ start := h.clock.Now()
387+ marks = append(marks, harnessMark{s.ID, run, seq, "input_injected", start, true})
388+ end := h.clock.Now()
389+ marks = append(marks, harnessMark{s.ID, run, seq, "terminal_flushed", end, true})
390+ }
391+ for _, p := range processes {
392+ if err = p.Measure(o.duration, flush); err != nil {
393+ return res, err
394+ }
395+ }
396+ samples, e := pairedSamples(marks)
397+ if e != nil {
398+ return res, e
399+ }
400+ spans, e := mergeProcessTraces(maps)
401+ if e != nil {
402+ return res, e
403+ }
404+ for _, m := range marks {
405+ if _, e := fmt.Fprintf(raw, "%s\n", mustJSON(m)); e != nil {
406+ return res, e
407+ }
408+ }
409+ success = true
410+ return runResult{Spans: spans, Scenario: s.ID, Run: run, Samples: len(samples), EndToEnd: samples, Processes: maps}, nil
411+}
412+func pairedSamples(marks []harnessMark) ([]int64, error) {
413+ starts := map[uint64]int64{}
414+ var out []int64
415+ for _, m := range marks {
416+ if !m.Valid {
417+ return nil, errors.New("invalid harness boundary")
418+ }
419+ switch m.Kind {
420+ case "input_injected":
421+ if _, ok := starts[m.Sequence]; ok {
422+ return nil, errors.New("duplicate input boundary")
423+ }
424+ starts[m.Sequence] = m.Tick
425+ case "terminal_flushed":
426+ start, ok := starts[m.Sequence]
427+ if !ok {
428+ return nil, errors.New("terminal flush without input pair")
429+ }
430+ if m.Tick < start {
431+ return nil, errors.New("negative harness duration")
432+ }
433+ out = append(out, m.Tick-start)
434+ delete(starts, m.Sequence)
435+ default:
436+ return nil, errors.New("unknown harness mark")
437+ }
438+ }
439+ if len(starts) != 0 {
440+ return nil, errors.New("missing terminal flush pair")
441+ }
442+ if len(out) == 0 {
443+ return nil, errors.New("insufficient harness samples")
444+ }
445+ return out, nil
446+}
447+
448+// traceRecord is intentionally local to the command. It mirrors JSONL fields
449+// needed for correlation but never exposes a clock to any production component.
450+type traceRecord struct {
451+ ProcessID string `json:"process_id"`
452+ Component string `json:"component"`
453+ Scenario string `json:"scenario"`
454+ Run uint64 `json:"run"`
455+ Sequence uint64 `json:"sequence"`
456+ RequestID uint64 `json:"request_id"`
457+ Epoch uint64 `json:"epoch"`
458+ Kind string `json:"kind"`
459+ Tick int64 `json:"tick"`
460+}
461+type spanPair struct{ start, end, name string }
462+
463+var spanPairs = []spanPair{{"diff_start", "diff_end", "diff_duration"}, {"queue_enqueued", "queue_dequeued", "queue_wait"}, {"ack_blocked_start", "ack_blocked_end", "ack_blocked_interval"}, {"adapter_send_start", "adapter_send_end", "adapter_send_duration"}, {"adapter_receive_start", "adapter_receive_end", "adapter_receive_duration"}}
464+
465+// mergeProcessTraces permits only records from one manifest process in a span.
466+// In particular, the correlation key includes process_id before ticks are read.
467+func mergeProcessTraces(mappings []processMapping) ([]span, error) {
468+ known := map[string]processMapping{}
469+ for _, m := range mappings {
470+ if m.ProcessID == "" || m.ClockDomain == "" || m.TracePath == "" || known[m.ProcessID].ProcessID != "" {
471+ return nil, errors.New("duplicate or incomplete manifest process mapping")
472+ }
473+ known[m.ProcessID] = m
474+ }
475+ out := []span{}
476+ for _, m := range mappings {
477+ f, err := os.Open(m.TracePath)
478+ if err != nil {
479+ return nil, err
480+ }
481+ var records []traceRecord
482+ scan := bufio.NewScanner(f)
483+ for scan.Scan() {
484+ var r traceRecord
485+ if err := json.Unmarshal(scan.Bytes(), &r); err != nil {
486+ _ = f.Close()
487+ return nil, err
488+ }
489+ if r.ProcessID != m.ProcessID {
490+ _ = f.Close()
491+ return nil, errors.New("trace record process_id does not match manifest")
492+ }
493+ if r.Scenario == "" || r.Run == 0 || r.Component == "" || r.Sequence == 0 || r.RequestID == 0 || r.Epoch == 0 {
494+ _ = f.Close()
495+ return nil, errors.New("trace record has invalid correlation fields")
496+ }
497+ if _, ok := known[r.ProcessID]; !ok {
498+ _ = f.Close()
499+ return nil, errors.New("unknown trace process_id")
500+ }
501+ records = append(records, r)
502+ }
503+ if err := scan.Err(); err != nil {
504+ _ = f.Close()
505+ return nil, err
506+ }
507+ _ = f.Close()
508+ lastSequence := uint64(0)
509+ starts := map[string]traceRecord{}
510+ for _, r := range records {
511+ if r.Sequence < lastSequence {
512+ return nil, errors.New("nonmonotonic same-process sequence")
513+ }
514+ lastSequence = r.Sequence
515+ for _, pair := range spanPairs {
516+ key := fmt.Sprintf("%s/%s/%d/%d/%d/%d", r.ProcessID, m.Scenario, m.Run, r.Sequence, r.RequestID, r.Epoch)
517+ if r.Kind == pair.start {
518+ k := pair.name + "/" + key
519+ if _, exists := starts[k]; exists {
520+ return nil, errors.New("duplicate process-local span start")
521+ }
522+ starts[k] = r
523+ }
524+ if r.Kind == pair.end {
525+ k := pair.name + "/" + key
526+ start, ok := starts[k]
527+ if !ok {
528+ return nil, errors.New("span end without same-process start")
529+ }
530+ if r.Tick < start.Tick {
531+ return nil, errors.New("negative same-process span")
532+ }
533+ out = append(out, span{Component: r.Component, Name: pair.name, Samples: []int64{r.Tick - start.Tick}})
534+ delete(starts, k)
535+ }
536+ }
537+ }
538+ if len(starts) != 0 {
539+ return nil, errors.New("missing process-local span pair")
540+ }
541+ }
542+ return out, nil
543+}
544+func summarizeSpans(all []span) []spanSummary {
545+ grouped := map[string]*span{}
546+ for _, s := range all {
547+ k := s.Component + "\x00" + s.Name
548+ if grouped[k] == nil {
549+ grouped[k] = &span{Component: s.Component, Name: s.Name}
550+ }
551+ grouped[k].Samples = append(grouped[k].Samples, s.Samples...)
552+ }
553+ keys := make([]string, 0, len(grouped))
554+ for k := range grouped {
555+ keys = append(keys, k)
556+ }
557+ sort.Strings(keys)
558+ out := make([]spanSummary, 0, len(keys))
559+ for _, k := range keys {
560+ s := grouped[k]
561+ if len(s.Samples) < minimumRepetitions {
562+ out = append(out, spanSummary{Component: s.Component, Name: s.Name, Distribution: distribution{Count: len(s.Samples)}})
563+ continue
564+ }
565+ out = append(out, spanSummary{Component: s.Component, Name: s.Name, Distribution: percentiles(s.Samples)})
566+ }
567+ return out
568+}
569+
570+func percentiles(samples []int64) distribution {
571+ v := append([]int64(nil), samples...)
572+ sort.Slice(v, func(i, j int) bool { return v[i] < v[j] })
573+ at := func(p int) int64 { return v[(len(v)-1)*p/100] }
574+ return distribution{len(v), at(50), at(95), at(99), v[len(v)-1]}
575+}
576+func writeJSON(path string, v any) error {
577+ f, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
578+ if e != nil {
579+ return e
580+ }
581+ defer f.Close()
582+ e = json.NewEncoder(f).Encode(v)
583+ return e
584+}
585+func mustJSON(v any) string { b, _ := json.Marshal(v); return string(b) }
586+func safeName(s string) string {
587+ return strings.Map(func(r rune) rune {
588+ if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
589+ return r
590+ }
591+ return '-'
592+ }, s)
593+}
594+func readJSONL(path string) ([]map[string]json.RawMessage, error) {
595+ f, e := os.Open(path)
596+ if e != nil {
597+ return nil, e
598+ }
599+ defer f.Close()
600+ var r []map[string]json.RawMessage
601+ scan := bufio.NewScanner(f)
602+ for scan.Scan() {
603+ var v map[string]json.RawMessage
604+ if e := json.Unmarshal(scan.Bytes(), &v); e != nil {
605+ return nil, e
606+ }
607+ r = append(r, v)
608+ }
609+ return r, scan.Err()
610+}
A cmd/vev-perf-harness/main_test.go +262 -0
@@ -0,0 +1,262 @@
1+package main
2+
3+import (
4+ "bytes"
5+ "encoding/json"
6+ "errors"
7+ "os"
8+ "path/filepath"
9+ "testing"
10+ "time"
11+)
12+
13+type fakeClock struct{ tick int64 }
14+
15+func (c *fakeClock) Now() int64 { c.tick += 10; return c.tick }
16+func (*fakeClock) Sleep(time.Duration) {}
17+
18+type fakeProcess struct {
19+ fail bool
20+ warmups []time.Duration
21+ measures []time.Duration
22+ closed bool
23+}
24+
25+func (p *fakeProcess) Warmup(d time.Duration) error { p.warmups = append(p.warmups, d); return nil }
26+func (p *fakeProcess) Measure(d time.Duration, flush func()) error {
27+ p.measures = append(p.measures, d)
28+ if p.fail {
29+ return errors.New("measure failure")
30+ }
31+ flush()
32+ return nil
33+}
34+func (p *fakeProcess) Close() error { p.closed = true; return nil }
35+
36+type fakeLauncher struct {
37+ mappings []processMapping
38+ process []*fakeProcess
39+ manifestPresent bool
40+}
41+
42+func (l *fakeLauncher) Launch(m processMapping, _ []string) (launchedProcess, error) {
43+ l.mappings = append(l.mappings, m)
44+ _, e := os.Stat(filepath.Join(filepath.Dir(m.TracePath), "manifest.json"))
45+ l.manifestPresent = e == nil
46+ p := &fakeProcess{}
47+ l.process = append(l.process, p)
48+ return p, nil
49+}
50+
51+func TestHarnessFlagsRejectShortMeasurements(t *testing.T) {
52+ for _, args := range [][]string{
53+ {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "29s", "--repetitions", "10"},
54+ {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "30s", "--repetitions", "9"},
55+ {"--manifest", "m", "--out", "o", "--duration", "30s", "--repetitions", "10"},
56+ } {
57+ if _, err := parseOptions(args); err == nil {
58+ t.Fatalf("parseOptions(%v) succeeded", args)
59+ }
60+ }
61+ if _, err := parseOptions([]string{"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "30s", "--repetitions", "10"}); err == nil {
62+ t.Fatal("missing --warmup accepted")
63+ }
64+ if _, err := parseOptions([]string{"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}); err != nil {
65+ t.Fatal(err)
66+ }
67+}
68+
69+func TestHarnessManifestCoversCanonicalMatrix(t *testing.T) {
70+ m, err := readManifest(filepath.Join("..", "..", "testdata", "perf", "manifest.json"))
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ if err := validateManifest(m); err != nil {
75+ t.Fatal(err)
76+ }
77+ if len(m.Scenarios) != 4*9*6 {
78+ t.Fatalf("scenarios=%d", len(m.Scenarios))
79+ }
80+ m.Scenarios = m.Scenarios[:len(m.Scenarios)-1]
81+ if err := validateManifest(m); err == nil {
82+ t.Fatal("missing combination accepted")
83+ }
84+}
85+
86+func TestHarnessCreatesExclusiveTraceManifestAndEvidence(t *testing.T) {
87+ dir := t.TempDir()
88+ raw, err := os.OpenFile(filepath.Join(dir, "raw-harness.jsonl"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
89+ if err != nil {
90+ t.Fatal(err)
91+ }
92+ defer raw.Close()
93+ launcher := &fakeLauncher{}
94+ h := defaultHarness()
95+ h.clock = &fakeClock{}
96+ h.launcher = launcher
97+ o := options{vevBin: "ignored", out: dir, warmup: time.Second, duration: minimumDuration, repetitions: minimumRepetitions}
98+ r, err := h.runOne(o, manifest{}, scenario{ID: "4x4-local", Roles: []string{"daemon", "client"}}, 1, raw)
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ if !launcher.manifestPresent {
103+ t.Fatal("launched before complete run manifest")
104+ }
105+ if len(r.EndToEnd) != 2 || r.Samples != 2 {
106+ t.Fatalf("samples=%+v", r)
107+ }
108+ seen := map[string]bool{}
109+ for _, m := range r.Processes {
110+ if m.ProcessID == "" || m.ClockDomain != m.ProcessID || m.TracePath == "" || seen[m.TracePath] {
111+ t.Fatalf("bad mapping %+v", m)
112+ }
113+ seen[m.TracePath] = true
114+ if _, err := os.Stat(m.TracePath); err != nil {
115+ t.Fatal(err)
116+ }
117+ }
118+ b, err := os.ReadFile(filepath.Join(dir, "4x4-local-run-001", "manifest.json"))
119+ if err != nil {
120+ t.Fatal(err)
121+ }
122+ var got runManifest
123+ if err := json.Unmarshal(b, &got); err != nil {
124+ t.Fatal(err)
125+ }
126+ if len(got.Processes) != 2 {
127+ t.Fatalf("manifest=%+v", got)
128+ }
129+ if len(launcher.process) != 2 || !launcher.process[0].closed || launcher.process[0].warmups[0] != time.Second || launcher.process[0].measures[0] != minimumDuration {
130+ t.Fatalf("process lifecycle not recorded: %+v", launcher.process)
131+ }
132+}
133+
134+func TestHarnessRejectsInvalidBoundaryPairs(t *testing.T) {
135+ for _, marks := range [][]harnessMark{
136+ {{Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}},
137+ {{Sequence: 1, Kind: "input_injected", Tick: 2, Valid: true}, {Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}},
138+ {{Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}},
139+ {{Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Sequence: 1, Kind: "input_injected", Tick: 2, Valid: true}},
140+ } {
141+ if _, err := pairedSamples(marks); err == nil {
142+ t.Fatalf("invalid marks accepted: %+v", marks)
143+ }
144+ }
145+ got, err := pairedSamples([]harnessMark{{Sequence: 9, Kind: "input_injected", Tick: 1, Valid: true}, {Sequence: 9, Kind: "terminal_flushed", Tick: 5, Valid: true}})
146+ if err != nil || len(got) != 1 || got[0] != 4 {
147+ t.Fatalf("got %v %v", got, err)
148+ }
149+}
150+
151+func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) {
152+ var b bytes.Buffer
153+ for _, m := range []harnessMark{{Scenario: "s", Run: 1, Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Scenario: "s", Run: 1, Sequence: 1, Kind: "terminal_flushed", Tick: 2, Valid: true}} {
154+ b.WriteString(mustJSON(m))
155+ b.WriteByte('\n')
156+ }
157+ path := filepath.Join(t.TempDir(), "raw.jsonl")
158+ if err := os.WriteFile(path, b.Bytes(), 0o600); err != nil {
159+ t.Fatal(err)
160+ }
161+ if got, err := readJSONL(path); err != nil || len(got) != 2 {
162+ t.Fatalf("records=%d err=%v", len(got), err)
163+ }
164+}
165+
166+type traceLauncher struct{ fakeLauncher }
167+
168+func (l *traceLauncher) Launch(m processMapping, args []string) (launchedProcess, error) {
169+ p, err := l.fakeLauncher.Launch(m, args)
170+ if err != nil {
171+ return nil, err
172+ }
173+ f, err := os.OpenFile(m.TracePath, os.O_WRONLY|os.O_APPEND, 0o600)
174+ if err != nil {
175+ return nil, err
176+ }
177+ defer f.Close()
178+ for i, pair := range spanPairs {
179+ for n, kind := range []string{pair.start, pair.end} {
180+ b, _ := json.Marshal(traceRecord{ProcessID: m.ProcessID, Component: m.Role, Scenario: m.Scenario, Run: uint64(m.Run), Sequence: uint64(i + 1), RequestID: 1, Epoch: 1, Kind: kind, Tick: int64(i*20 + n*5)})
181+ if _, err := f.Write(append(b, '\n')); err != nil {
182+ return nil, err
183+ }
184+ }
185+ }
186+ return p, nil
187+}
188+func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
189+ base, err := readManifest(filepath.Join("..", "..", "testdata", "perf", "manifest.json"))
190+ if err != nil {
191+ t.Fatal(err)
192+ }
193+ for i := range base.Scenarios {
194+ if i > 0 {
195+ base.Scenarios[i].InapplicableReason = "fixture limits this test to one public topology"
196+ base.Scenarios[i].Roles = nil
197+ }
198+ }
199+ dir := t.TempDir()
200+ manifestPath := filepath.Join(dir, "manifest.json")
201+ if err := os.WriteFile(manifestPath, []byte(mustJSON(base)), 0o600); err != nil {
202+ t.Fatal(err)
203+ }
204+ l := &traceLauncher{}
205+ h := defaultHarness()
206+ h.clock = &fakeClock{}
207+ h.launcher = l
208+ h.gitSHA = func() string { return "test-sha" }
209+ if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", filepath.Join(dir, "out"), "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
210+ t.Fatal(err)
211+ }
212+ for _, name := range []string{"raw-harness.jsonl", "runs.json", "summary.json"} {
213+ if _, err := os.Stat(filepath.Join(dir, "out", name)); err != nil {
214+ t.Errorf("%s: %v", name, err)
215+ }
216+ }
217+ var got summary
218+ b, err := os.ReadFile(filepath.Join(dir, "out", "summary.json"))
219+ if err != nil {
220+ t.Fatal(err)
221+ }
222+ if err := json.Unmarshal(b, &got); err != nil {
223+ t.Fatal(err)
224+ }
225+ if got.EndToEnd.Count != 20 || got.Repetitions != 10 || len(got.Spans) == 0 {
226+ t.Fatalf("summary=%+v", got)
227+ }
228+ for _, s := range got.Spans {
229+ if s.Distribution.Count < 10 {
230+ t.Fatalf("insufficient span summary: %+v", s)
231+ }
232+ }
233+}
234+func TestHarnessRejectsCrossProcessAndBadTraceSpans(t *testing.T) {
235+ write := func(t *testing.T, records ...traceRecord) processMapping {
236+ t.Helper()
237+ path := filepath.Join(t.TempDir(), "trace.jsonl")
238+ f, err := os.Create(path)
239+ if err != nil {
240+ t.Fatal(err)
241+ }
242+ for _, r := range records {
243+ b, _ := json.Marshal(r)
244+ _, _ = f.Write(append(b, '\n'))
245+ }
246+ _ = f.Close()
247+ return processMapping{ProcessID: "one", ClockDomain: "one", TracePath: path, Scenario: "s", Run: 1}
248+ }
249+ base := func(kind string, tick int64) traceRecord {
250+ return traceRecord{ProcessID: "one", Component: "daemon", Scenario: "s", Run: 1, Sequence: 1, RequestID: 1, Epoch: 1, Kind: kind, Tick: tick}
251+ }
252+ for _, records := range [][]traceRecord{{base("diff_end", 1)}, {base("diff_start", 2), base("diff_end", 1)}, {base("diff_start", 1)}, {func() traceRecord { r := base("diff_start", 1); r.ProcessID = "other"; return r }()}} {
253+ if _, err := mergeProcessTraces([]processMapping{write(t, records...)}); err == nil {
254+ t.Fatalf("invalid trace accepted: %+v", records)
255+ }
256+ }
257+ m := write(t, base("diff_start", 1), base("diff_end", 3))
258+ spans, err := mergeProcessTraces([]processMapping{m})
259+ if err != nil || len(spans) != 1 || spans[0].Samples[0] != 2 {
260+ t.Fatalf("spans=%+v err=%v", spans, err)
261+ }
262+}
@@ -0,0 +1,262 @@
1+package main
2+
3+import (
4+ "bytes"
5+ "encoding/json"
6+ "errors"
7+ "os"
8+ "path/filepath"
9+ "testing"
10+ "time"
11+)
12+
13+type fakeClock struct{ tick int64 }
14+
15+func (c *fakeClock) Now() int64 { c.tick += 10; return c.tick }
16+func (*fakeClock) Sleep(time.Duration) {}
17+
18+type fakeProcess struct {
19+ fail bool
20+ warmups []time.Duration
21+ measures []time.Duration
22+ closed bool
23+}
24+
25+func (p *fakeProcess) Warmup(d time.Duration) error { p.warmups = append(p.warmups, d); return nil }
26+func (p *fakeProcess) Measure(d time.Duration, flush func()) error {
27+ p.measures = append(p.measures, d)
28+ if p.fail {
29+ return errors.New("measure failure")
30+ }
31+ flush()
32+ return nil
33+}
34+func (p *fakeProcess) Close() error { p.closed = true; return nil }
35+
36+type fakeLauncher struct {
37+ mappings []processMapping
38+ process []*fakeProcess
39+ manifestPresent bool
40+}
41+
42+func (l *fakeLauncher) Launch(m processMapping, _ []string) (launchedProcess, error) {
43+ l.mappings = append(l.mappings, m)
44+ _, e := os.Stat(filepath.Join(filepath.Dir(m.TracePath), "manifest.json"))
45+ l.manifestPresent = e == nil
46+ p := &fakeProcess{}
47+ l.process = append(l.process, p)
48+ return p, nil
49+}
50+
51+func TestHarnessFlagsRejectShortMeasurements(t *testing.T) {
52+ for _, args := range [][]string{
53+ {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "29s", "--repetitions", "10"},
54+ {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "30s", "--repetitions", "9"},
55+ {"--manifest", "m", "--out", "o", "--duration", "30s", "--repetitions", "10"},
56+ } {
57+ if _, err := parseOptions(args); err == nil {
58+ t.Fatalf("parseOptions(%v) succeeded", args)
59+ }
60+ }
61+ if _, err := parseOptions([]string{"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "30s", "--repetitions", "10"}); err == nil {
62+ t.Fatal("missing --warmup accepted")
63+ }
64+ if _, err := parseOptions([]string{"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}); err != nil {
65+ t.Fatal(err)
66+ }
67+}
68+
69+func TestHarnessManifestCoversCanonicalMatrix(t *testing.T) {
70+ m, err := readManifest(filepath.Join("..", "..", "testdata", "perf", "manifest.json"))
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ if err := validateManifest(m); err != nil {
75+ t.Fatal(err)
76+ }
77+ if len(m.Scenarios) != 4*9*6 {
78+ t.Fatalf("scenarios=%d", len(m.Scenarios))
79+ }
80+ m.Scenarios = m.Scenarios[:len(m.Scenarios)-1]
81+ if err := validateManifest(m); err == nil {
82+ t.Fatal("missing combination accepted")
83+ }
84+}
85+
86+func TestHarnessCreatesExclusiveTraceManifestAndEvidence(t *testing.T) {
87+ dir := t.TempDir()
88+ raw, err := os.OpenFile(filepath.Join(dir, "raw-harness.jsonl"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
89+ if err != nil {
90+ t.Fatal(err)
91+ }
92+ defer raw.Close()
93+ launcher := &fakeLauncher{}
94+ h := defaultHarness()
95+ h.clock = &fakeClock{}
96+ h.launcher = launcher
97+ o := options{vevBin: "ignored", out: dir, warmup: time.Second, duration: minimumDuration, repetitions: minimumRepetitions}
98+ r, err := h.runOne(o, manifest{}, scenario{ID: "4x4-local", Roles: []string{"daemon", "client"}}, 1, raw)
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ if !launcher.manifestPresent {
103+ t.Fatal("launched before complete run manifest")
104+ }
105+ if len(r.EndToEnd) != 2 || r.Samples != 2 {
106+ t.Fatalf("samples=%+v", r)
107+ }
108+ seen := map[string]bool{}
109+ for _, m := range r.Processes {
110+ if m.ProcessID == "" || m.ClockDomain != m.ProcessID || m.TracePath == "" || seen[m.TracePath] {
111+ t.Fatalf("bad mapping %+v", m)
112+ }
113+ seen[m.TracePath] = true
114+ if _, err := os.Stat(m.TracePath); err != nil {
115+ t.Fatal(err)
116+ }
117+ }
118+ b, err := os.ReadFile(filepath.Join(dir, "4x4-local-run-001", "manifest.json"))
119+ if err != nil {
120+ t.Fatal(err)
121+ }
122+ var got runManifest
123+ if err := json.Unmarshal(b, &got); err != nil {
124+ t.Fatal(err)
125+ }
126+ if len(got.Processes) != 2 {
127+ t.Fatalf("manifest=%+v", got)
128+ }
129+ if len(launcher.process) != 2 || !launcher.process[0].closed || launcher.process[0].warmups[0] != time.Second || launcher.process[0].measures[0] != minimumDuration {
130+ t.Fatalf("process lifecycle not recorded: %+v", launcher.process)
131+ }
132+}
133+
134+func TestHarnessRejectsInvalidBoundaryPairs(t *testing.T) {
135+ for _, marks := range [][]harnessMark{
136+ {{Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}},
137+ {{Sequence: 1, Kind: "input_injected", Tick: 2, Valid: true}, {Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}},
138+ {{Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}},
139+ {{Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Sequence: 1, Kind: "input_injected", Tick: 2, Valid: true}},
140+ } {
141+ if _, err := pairedSamples(marks); err == nil {
142+ t.Fatalf("invalid marks accepted: %+v", marks)
143+ }
144+ }
145+ got, err := pairedSamples([]harnessMark{{Sequence: 9, Kind: "input_injected", Tick: 1, Valid: true}, {Sequence: 9, Kind: "terminal_flushed", Tick: 5, Valid: true}})
146+ if err != nil || len(got) != 1 || got[0] != 4 {
147+ t.Fatalf("got %v %v", got, err)
148+ }
149+}
150+
151+func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) {
152+ var b bytes.Buffer
153+ for _, m := range []harnessMark{{Scenario: "s", Run: 1, Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Scenario: "s", Run: 1, Sequence: 1, Kind: "terminal_flushed", Tick: 2, Valid: true}} {
154+ b.WriteString(mustJSON(m))
155+ b.WriteByte('\n')
156+ }
157+ path := filepath.Join(t.TempDir(), "raw.jsonl")
158+ if err := os.WriteFile(path, b.Bytes(), 0o600); err != nil {
159+ t.Fatal(err)
160+ }
161+ if got, err := readJSONL(path); err != nil || len(got) != 2 {
162+ t.Fatalf("records=%d err=%v", len(got), err)
163+ }
164+}
165+
166+type traceLauncher struct{ fakeLauncher }
167+
168+func (l *traceLauncher) Launch(m processMapping, args []string) (launchedProcess, error) {
169+ p, err := l.fakeLauncher.Launch(m, args)
170+ if err != nil {
171+ return nil, err
172+ }
173+ f, err := os.OpenFile(m.TracePath, os.O_WRONLY|os.O_APPEND, 0o600)
174+ if err != nil {
175+ return nil, err
176+ }
177+ defer f.Close()
178+ for i, pair := range spanPairs {
179+ for n, kind := range []string{pair.start, pair.end} {
180+ b, _ := json.Marshal(traceRecord{ProcessID: m.ProcessID, Component: m.Role, Scenario: m.Scenario, Run: uint64(m.Run), Sequence: uint64(i + 1), RequestID: 1, Epoch: 1, Kind: kind, Tick: int64(i*20 + n*5)})
181+ if _, err := f.Write(append(b, '\n')); err != nil {
182+ return nil, err
183+ }
184+ }
185+ }
186+ return p, nil
187+}
188+func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
189+ base, err := readManifest(filepath.Join("..", "..", "testdata", "perf", "manifest.json"))
190+ if err != nil {
191+ t.Fatal(err)
192+ }
193+ for i := range base.Scenarios {
194+ if i > 0 {
195+ base.Scenarios[i].InapplicableReason = "fixture limits this test to one public topology"
196+ base.Scenarios[i].Roles = nil
197+ }
198+ }
199+ dir := t.TempDir()
200+ manifestPath := filepath.Join(dir, "manifest.json")
201+ if err := os.WriteFile(manifestPath, []byte(mustJSON(base)), 0o600); err != nil {
202+ t.Fatal(err)
203+ }
204+ l := &traceLauncher{}
205+ h := defaultHarness()
206+ h.clock = &fakeClock{}
207+ h.launcher = l
208+ h.gitSHA = func() string { return "test-sha" }
209+ if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", filepath.Join(dir, "out"), "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
210+ t.Fatal(err)
211+ }
212+ for _, name := range []string{"raw-harness.jsonl", "runs.json", "summary.json"} {
213+ if _, err := os.Stat(filepath.Join(dir, "out", name)); err != nil {
214+ t.Errorf("%s: %v", name, err)
215+ }
216+ }
217+ var got summary
218+ b, err := os.ReadFile(filepath.Join(dir, "out", "summary.json"))
219+ if err != nil {
220+ t.Fatal(err)
221+ }
222+ if err := json.Unmarshal(b, &got); err != nil {
223+ t.Fatal(err)
224+ }
225+ if got.EndToEnd.Count != 20 || got.Repetitions != 10 || len(got.Spans) == 0 {
226+ t.Fatalf("summary=%+v", got)
227+ }
228+ for _, s := range got.Spans {
229+ if s.Distribution.Count < 10 {
230+ t.Fatalf("insufficient span summary: %+v", s)
231+ }
232+ }
233+}
234+func TestHarnessRejectsCrossProcessAndBadTraceSpans(t *testing.T) {
235+ write := func(t *testing.T, records ...traceRecord) processMapping {
236+ t.Helper()
237+ path := filepath.Join(t.TempDir(), "trace.jsonl")
238+ f, err := os.Create(path)
239+ if err != nil {
240+ t.Fatal(err)
241+ }
242+ for _, r := range records {
243+ b, _ := json.Marshal(r)
244+ _, _ = f.Write(append(b, '\n'))
245+ }
246+ _ = f.Close()
247+ return processMapping{ProcessID: "one", ClockDomain: "one", TracePath: path, Scenario: "s", Run: 1}
248+ }
249+ base := func(kind string, tick int64) traceRecord {
250+ return traceRecord{ProcessID: "one", Component: "daemon", Scenario: "s", Run: 1, Sequence: 1, RequestID: 1, Epoch: 1, Kind: kind, Tick: tick}
251+ }
252+ for _, records := range [][]traceRecord{{base("diff_end", 1)}, {base("diff_start", 2), base("diff_end", 1)}, {base("diff_start", 1)}, {func() traceRecord { r := base("diff_start", 1); r.ProcessID = "other"; return r }()}} {
253+ if _, err := mergeProcessTraces([]processMapping{write(t, records...)}); err == nil {
254+ t.Fatalf("invalid trace accepted: %+v", records)
255+ }
256+ }
257+ m := write(t, base("diff_start", 1), base("diff_end", 3))
258+ spans, err := mergeProcessTraces([]processMapping{m})
259+ if err != nil || len(spans) != 1 || spans[0].Samples[0] != 2 {
260+ t.Fatalf("spans=%+v err=%v", spans, err)
261+ }
262+}
A testdata/perf/README.md +24 -0
@@ -0,0 +1,24 @@
1+# Public transport-observability harness
2+
3+`manifest.json` is the complete S5a measurement matrix: 1x4, 4x1, 4x4, and
4+8x1 layouts at 120x40 with 10,000 rows per pane; every canonical workload; and
5+local, SSH stdio, UDP baseline, 25/100ms RTT, and 1% loss transport fixtures.
6+Each listed scenario is applicable. A future unavailable combination must remain
7+in the manifest with `inapplicable_reason` and no launched roles; omission is
8+invalid.
9+
10+Run only after building the public binary:
11+
12+```sh
13+go build -o /tmp/vev-perf ./
14+go run ./cmd/vev-perf-harness --vev-bin /tmp/vev-perf \
15+ --manifest testdata/perf/manifest.json --out /tmp/vev-perf-results \
16+ --warmup 10s --duration 30s --repetitions 10
17+```
18+
19+The harness creates one `O_EXCL` JSONL path per launched role before launch and
20+records the assigned process ID, clock domain, role, and path in that run's
21+manifest. `raw-harness.jsonl` contains only harness-clock input/terminal-flush
22+pairs. Process traces are correlated by IDs, never by timestamps across process
23+clock domains. Results with missing pairs, bad order, insufficient samples, or
24+cross-process tick arithmetic are invalid.
@@ -0,0 +1,24 @@
1+# Public transport-observability harness
2+
3+`manifest.json` is the complete S5a measurement matrix: 1x4, 4x1, 4x4, and
4+8x1 layouts at 120x40 with 10,000 rows per pane; every canonical workload; and
5+local, SSH stdio, UDP baseline, 25/100ms RTT, and 1% loss transport fixtures.
6+Each listed scenario is applicable. A future unavailable combination must remain
7+in the manifest with `inapplicable_reason` and no launched roles; omission is
8+invalid.
9+
10+Run only after building the public binary:
11+
12+```sh
13+go build -o /tmp/vev-perf ./
14+go run ./cmd/vev-perf-harness --vev-bin /tmp/vev-perf \
15+ --manifest testdata/perf/manifest.json --out /tmp/vev-perf-results \
16+ --warmup 10s --duration 30s --repetitions 10
17+```
18+
19+The harness creates one `O_EXCL` JSONL path per launched role before launch and
20+records the assigned process ID, clock domain, role, and path in that run's
21+manifest. `raw-harness.jsonl` contains only harness-clock input/terminal-flush
22+pairs. Process traces are correlated by IDs, never by timestamps across process
23+clock domains. Results with missing pairs, bad order, insufficient samples, or
24+cross-process tick arithmetic are invalid.
A testdata/perf/manifest.json +2407 -0
@@ -0,0 +1,2407 @@
1+{
2+ "schema": 1,
3+ "topologies": [
4+ {
5+ "id": "1x4",
6+ "geometry": "120x40",
7+ "rows_per_pane": 10000
8+ },
9+ {
10+ "id": "4x1",
11+ "geometry": "120x40",
12+ "rows_per_pane": 10000
13+ },
14+ {
15+ "id": "4x4",
16+ "geometry": "120x40",
17+ "rows_per_pane": 10000
18+ },
19+ {
20+ "id": "8x1",
21+ "geometry": "120x40",
22+ "rows_per_pane": 10000
23+ }
24+ ],
25+ "workloads": [
26+ "idle",
27+ "active_output",
28+ "all_output",
29+ "inactive_output",
30+ "interactive_flood",
31+ "copy_search",
32+ "resize_sweep",
33+ "snapshot_output_resize",
34+ "attach_restore_tab_switch"
35+ ],
36+ "transports": [
37+ {
38+ "id": "local",
39+ "kind": "local"
40+ },
41+ {
42+ "id": "ssh_stdio",
43+ "kind": "ssh_stdio"
44+ },
45+ {
46+ "id": "udp_baseline",
47+ "kind": "udp"
48+ },
49+ {
50+ "id": "udp_25ms",
51+ "kind": "udp",
52+ "rtt_ms": 25
53+ },
54+ {
55+ "id": "udp_100ms",
56+ "kind": "udp",
57+ "rtt_ms": 100
58+ },
59+ {
60+ "id": "udp_loss_1pct",
61+ "kind": "udp",
62+ "loss_percent": 1
63+ }
64+ ],
65+ "scenarios": [
66+ {
67+ "id": "1x4-idle-local",
68+ "topology": "1x4",
69+ "workload": "idle",
70+ "transport": "local",
71+ "roles": [
72+ "daemon",
73+ "client"
74+ ]
75+ },
76+ {
77+ "id": "1x4-idle-ssh_stdio",
78+ "topology": "1x4",
79+ "workload": "idle",
80+ "transport": "ssh_stdio",
81+ "roles": [
82+ "daemon",
83+ "client",
84+ "ssh_stdio_peer"
85+ ]
86+ },
87+ {
88+ "id": "1x4-idle-udp_baseline",
89+ "topology": "1x4",
90+ "workload": "idle",
91+ "transport": "udp_baseline",
92+ "roles": [
93+ "daemon",
94+ "client",
95+ "udp_peer"
96+ ]
97+ },
98+ {
99+ "id": "1x4-idle-udp_25ms",
100+ "topology": "1x4",
101+ "workload": "idle",
102+ "transport": "udp_25ms",
103+ "roles": [
104+ "daemon",
105+ "client",
106+ "udp_peer"
107+ ]
108+ },
109+ {
110+ "id": "1x4-idle-udp_100ms",
111+ "topology": "1x4",
112+ "workload": "idle",
113+ "transport": "udp_100ms",
114+ "roles": [
115+ "daemon",
116+ "client",
117+ "udp_peer"
118+ ]
119+ },
120+ {
121+ "id": "1x4-idle-udp_loss_1pct",
122+ "topology": "1x4",
123+ "workload": "idle",
124+ "transport": "udp_loss_1pct",
125+ "roles": [
126+ "daemon",
127+ "client",
128+ "udp_peer"
129+ ]
130+ },
131+ {
132+ "id": "1x4-active_output-local",
133+ "topology": "1x4",
134+ "workload": "active_output",
135+ "transport": "local",
136+ "roles": [
137+ "daemon",
138+ "client"
139+ ]
140+ },
141+ {
142+ "id": "1x4-active_output-ssh_stdio",
143+ "topology": "1x4",
144+ "workload": "active_output",
145+ "transport": "ssh_stdio",
146+ "roles": [
147+ "daemon",
148+ "client",
149+ "ssh_stdio_peer"
150+ ]
151+ },
152+ {
153+ "id": "1x4-active_output-udp_baseline",
154+ "topology": "1x4",
155+ "workload": "active_output",
156+ "transport": "udp_baseline",
157+ "roles": [
158+ "daemon",
159+ "client",
160+ "udp_peer"
161+ ]
162+ },
163+ {
164+ "id": "1x4-active_output-udp_25ms",
165+ "topology": "1x4",
166+ "workload": "active_output",
167+ "transport": "udp_25ms",
168+ "roles": [
169+ "daemon",
170+ "client",
171+ "udp_peer"
172+ ]
173+ },
174+ {
175+ "id": "1x4-active_output-udp_100ms",
176+ "topology": "1x4",
177+ "workload": "active_output",
178+ "transport": "udp_100ms",
179+ "roles": [
180+ "daemon",
181+ "client",
182+ "udp_peer"
183+ ]
184+ },
185+ {
186+ "id": "1x4-active_output-udp_loss_1pct",
187+ "topology": "1x4",
188+ "workload": "active_output",
189+ "transport": "udp_loss_1pct",
190+ "roles": [
191+ "daemon",
192+ "client",
193+ "udp_peer"
194+ ]
195+ },
196+ {
197+ "id": "1x4-all_output-local",
198+ "topology": "1x4",
199+ "workload": "all_output",
200+ "transport": "local",
201+ "roles": [
202+ "daemon",
203+ "client"
204+ ]
205+ },
206+ {
207+ "id": "1x4-all_output-ssh_stdio",
208+ "topology": "1x4",
209+ "workload": "all_output",
210+ "transport": "ssh_stdio",
211+ "roles": [
212+ "daemon",
213+ "client",
214+ "ssh_stdio_peer"
215+ ]
216+ },
217+ {
218+ "id": "1x4-all_output-udp_baseline",
219+ "topology": "1x4",
220+ "workload": "all_output",
221+ "transport": "udp_baseline",
222+ "roles": [
223+ "daemon",
224+ "client",
225+ "udp_peer"
226+ ]
227+ },
228+ {
229+ "id": "1x4-all_output-udp_25ms",
230+ "topology": "1x4",
231+ "workload": "all_output",
232+ "transport": "udp_25ms",
233+ "roles": [
234+ "daemon",
235+ "client",
236+ "udp_peer"
237+ ]
238+ },
239+ {
240+ "id": "1x4-all_output-udp_100ms",
241+ "topology": "1x4",
242+ "workload": "all_output",
243+ "transport": "udp_100ms",
244+ "roles": [
245+ "daemon",
246+ "client",
247+ "udp_peer"
248+ ]
249+ },
250+ {
251+ "id": "1x4-all_output-udp_loss_1pct",
252+ "topology": "1x4",
253+ "workload": "all_output",
254+ "transport": "udp_loss_1pct",
255+ "roles": [
256+ "daemon",
257+ "client",
258+ "udp_peer"
259+ ]
260+ },
261+ {
262+ "id": "1x4-inactive_output-local",
263+ "topology": "1x4",
264+ "workload": "inactive_output",
265+ "transport": "local",
266+ "roles": [
267+ "daemon",
268+ "client"
269+ ]
270+ },
271+ {
272+ "id": "1x4-inactive_output-ssh_stdio",
273+ "topology": "1x4",
274+ "workload": "inactive_output",
275+ "transport": "ssh_stdio",
276+ "roles": [
277+ "daemon",
278+ "client",
279+ "ssh_stdio_peer"
280+ ]
281+ },
282+ {
283+ "id": "1x4-inactive_output-udp_baseline",
284+ "topology": "1x4",
285+ "workload": "inactive_output",
286+ "transport": "udp_baseline",
287+ "roles": [
288+ "daemon",
289+ "client",
290+ "udp_peer"
291+ ]
292+ },
293+ {
294+ "id": "1x4-inactive_output-udp_25ms",
295+ "topology": "1x4",
296+ "workload": "inactive_output",
297+ "transport": "udp_25ms",
298+ "roles": [
299+ "daemon",
300+ "client",
301+ "udp_peer"
302+ ]
303+ },
304+ {
305+ "id": "1x4-inactive_output-udp_100ms",
306+ "topology": "1x4",
307+ "workload": "inactive_output",
308+ "transport": "udp_100ms",
309+ "roles": [
310+ "daemon",
311+ "client",
312+ "udp_peer"
313+ ]
314+ },
315+ {
316+ "id": "1x4-inactive_output-udp_loss_1pct",
317+ "topology": "1x4",
318+ "workload": "inactive_output",
319+ "transport": "udp_loss_1pct",
320+ "roles": [
321+ "daemon",
322+ "client",
323+ "udp_peer"
324+ ]
325+ },
326+ {
327+ "id": "1x4-interactive_flood-local",
328+ "topology": "1x4",
329+ "workload": "interactive_flood",
330+ "transport": "local",
331+ "roles": [
332+ "daemon",
333+ "client"
334+ ]
335+ },
336+ {
337+ "id": "1x4-interactive_flood-ssh_stdio",
338+ "topology": "1x4",
339+ "workload": "interactive_flood",
340+ "transport": "ssh_stdio",
341+ "roles": [
342+ "daemon",
343+ "client",
344+ "ssh_stdio_peer"
345+ ]
346+ },
347+ {
348+ "id": "1x4-interactive_flood-udp_baseline",
349+ "topology": "1x4",
350+ "workload": "interactive_flood",
351+ "transport": "udp_baseline",
352+ "roles": [
353+ "daemon",
354+ "client",
355+ "udp_peer"
356+ ]
357+ },
358+ {
359+ "id": "1x4-interactive_flood-udp_25ms",
360+ "topology": "1x4",
361+ "workload": "interactive_flood",
362+ "transport": "udp_25ms",
363+ "roles": [
364+ "daemon",
365+ "client",
366+ "udp_peer"
367+ ]
368+ },
369+ {
370+ "id": "1x4-interactive_flood-udp_100ms",
371+ "topology": "1x4",
372+ "workload": "interactive_flood",
373+ "transport": "udp_100ms",
374+ "roles": [
375+ "daemon",
376+ "client",
377+ "udp_peer"
378+ ]
379+ },
380+ {
381+ "id": "1x4-interactive_flood-udp_loss_1pct",
382+ "topology": "1x4",
383+ "workload": "interactive_flood",
384+ "transport": "udp_loss_1pct",
385+ "roles": [
386+ "daemon",
387+ "client",
388+ "udp_peer"
389+ ]
390+ },
391+ {
392+ "id": "1x4-copy_search-local",
393+ "topology": "1x4",
394+ "workload": "copy_search",
395+ "transport": "local",
396+ "roles": [
397+ "daemon",
398+ "client"
399+ ]
400+ },
401+ {
402+ "id": "1x4-copy_search-ssh_stdio",
403+ "topology": "1x4",
404+ "workload": "copy_search",
405+ "transport": "ssh_stdio",
406+ "roles": [
407+ "daemon",
408+ "client",
409+ "ssh_stdio_peer"
410+ ]
411+ },
412+ {
413+ "id": "1x4-copy_search-udp_baseline",
414+ "topology": "1x4",
415+ "workload": "copy_search",
416+ "transport": "udp_baseline",
417+ "roles": [
418+ "daemon",
419+ "client",
420+ "udp_peer"
421+ ]
422+ },
423+ {
424+ "id": "1x4-copy_search-udp_25ms",
425+ "topology": "1x4",
426+ "workload": "copy_search",
427+ "transport": "udp_25ms",
428+ "roles": [
429+ "daemon",
430+ "client",
431+ "udp_peer"
432+ ]
433+ },
434+ {
435+ "id": "1x4-copy_search-udp_100ms",
436+ "topology": "1x4",
437+ "workload": "copy_search",
438+ "transport": "udp_100ms",
439+ "roles": [
440+ "daemon",
441+ "client",
442+ "udp_peer"
443+ ]
444+ },
445+ {
446+ "id": "1x4-copy_search-udp_loss_1pct",
447+ "topology": "1x4",
448+ "workload": "copy_search",
449+ "transport": "udp_loss_1pct",
450+ "roles": [
451+ "daemon",
452+ "client",
453+ "udp_peer"
454+ ]
455+ },
456+ {
457+ "id": "1x4-resize_sweep-local",
458+ "topology": "1x4",
459+ "workload": "resize_sweep",
460+ "transport": "local",
461+ "roles": [
462+ "daemon",
463+ "client"
464+ ]
465+ },
466+ {
467+ "id": "1x4-resize_sweep-ssh_stdio",
468+ "topology": "1x4",
469+ "workload": "resize_sweep",
470+ "transport": "ssh_stdio",
471+ "roles": [
472+ "daemon",
473+ "client",
474+ "ssh_stdio_peer"
475+ ]
476+ },
477+ {
478+ "id": "1x4-resize_sweep-udp_baseline",
479+ "topology": "1x4",
480+ "workload": "resize_sweep",
481+ "transport": "udp_baseline",
482+ "roles": [
483+ "daemon",
484+ "client",
485+ "udp_peer"
486+ ]
487+ },
488+ {
489+ "id": "1x4-resize_sweep-udp_25ms",
490+ "topology": "1x4",
491+ "workload": "resize_sweep",
492+ "transport": "udp_25ms",
493+ "roles": [
494+ "daemon",
495+ "client",
496+ "udp_peer"
497+ ]
498+ },
499+ {
500+ "id": "1x4-resize_sweep-udp_100ms",
501+ "topology": "1x4",
502+ "workload": "resize_sweep",
503+ "transport": "udp_100ms",
504+ "roles": [
505+ "daemon",
506+ "client",
507+ "udp_peer"
508+ ]
509+ },
510+ {
511+ "id": "1x4-resize_sweep-udp_loss_1pct",
512+ "topology": "1x4",
513+ "workload": "resize_sweep",
514+ "transport": "udp_loss_1pct",
515+ "roles": [
516+ "daemon",
517+ "client",
518+ "udp_peer"
519+ ]
520+ },
521+ {
522+ "id": "1x4-snapshot_output_resize-local",
523+ "topology": "1x4",
524+ "workload": "snapshot_output_resize",
525+ "transport": "local",
526+ "roles": [
527+ "daemon",
528+ "client"
529+ ]
530+ },
531+ {
532+ "id": "1x4-snapshot_output_resize-ssh_stdio",
533+ "topology": "1x4",
534+ "workload": "snapshot_output_resize",
535+ "transport": "ssh_stdio",
536+ "roles": [
537+ "daemon",
538+ "client",
539+ "ssh_stdio_peer"
540+ ]
541+ },
542+ {
543+ "id": "1x4-snapshot_output_resize-udp_baseline",
544+ "topology": "1x4",
545+ "workload": "snapshot_output_resize",
546+ "transport": "udp_baseline",
547+ "roles": [
548+ "daemon",
549+ "client",
550+ "udp_peer"
551+ ]
552+ },
553+ {
554+ "id": "1x4-snapshot_output_resize-udp_25ms",
555+ "topology": "1x4",
556+ "workload": "snapshot_output_resize",
557+ "transport": "udp_25ms",
558+ "roles": [
559+ "daemon",
560+ "client",
561+ "udp_peer"
562+ ]
563+ },
564+ {
565+ "id": "1x4-snapshot_output_resize-udp_100ms",
566+ "topology": "1x4",
567+ "workload": "snapshot_output_resize",
568+ "transport": "udp_100ms",
569+ "roles": [
570+ "daemon",
571+ "client",
572+ "udp_peer"
573+ ]
574+ },
575+ {
576+ "id": "1x4-snapshot_output_resize-udp_loss_1pct",
577+ "topology": "1x4",
578+ "workload": "snapshot_output_resize",
579+ "transport": "udp_loss_1pct",
580+ "roles": [
581+ "daemon",
582+ "client",
583+ "udp_peer"
584+ ]
585+ },
586+ {
587+ "id": "1x4-attach_restore_tab_switch-local",
588+ "topology": "1x4",
589+ "workload": "attach_restore_tab_switch",
590+ "transport": "local",
591+ "roles": [
592+ "daemon",
593+ "client"
594+ ]
595+ },
596+ {
597+ "id": "1x4-attach_restore_tab_switch-ssh_stdio",
598+ "topology": "1x4",
599+ "workload": "attach_restore_tab_switch",
600+ "transport": "ssh_stdio",
601+ "roles": [
602+ "daemon",
603+ "client",
604+ "ssh_stdio_peer"
605+ ]
606+ },
607+ {
608+ "id": "1x4-attach_restore_tab_switch-udp_baseline",
609+ "topology": "1x4",
610+ "workload": "attach_restore_tab_switch",
611+ "transport": "udp_baseline",
612+ "roles": [
613+ "daemon",
614+ "client",
615+ "udp_peer"
616+ ]
617+ },
618+ {
619+ "id": "1x4-attach_restore_tab_switch-udp_25ms",
620+ "topology": "1x4",
621+ "workload": "attach_restore_tab_switch",
622+ "transport": "udp_25ms",
623+ "roles": [
624+ "daemon",
625+ "client",
626+ "udp_peer"
627+ ]
628+ },
629+ {
630+ "id": "1x4-attach_restore_tab_switch-udp_100ms",
631+ "topology": "1x4",
632+ "workload": "attach_restore_tab_switch",
633+ "transport": "udp_100ms",
634+ "roles": [
635+ "daemon",
636+ "client",
637+ "udp_peer"
638+ ]
639+ },
640+ {
641+ "id": "1x4-attach_restore_tab_switch-udp_loss_1pct",
642+ "topology": "1x4",
643+ "workload": "attach_restore_tab_switch",
644+ "transport": "udp_loss_1pct",
645+ "roles": [
646+ "daemon",
647+ "client",
648+ "udp_peer"
649+ ]
650+ },
651+ {
652+ "id": "4x1-idle-local",
653+ "topology": "4x1",
654+ "workload": "idle",
655+ "transport": "local",
656+ "roles": [
657+ "daemon",
658+ "client"
659+ ]
660+ },
661+ {
662+ "id": "4x1-idle-ssh_stdio",
663+ "topology": "4x1",
664+ "workload": "idle",
665+ "transport": "ssh_stdio",
666+ "roles": [
667+ "daemon",
668+ "client",
669+ "ssh_stdio_peer"
670+ ]
671+ },
672+ {
673+ "id": "4x1-idle-udp_baseline",
674+ "topology": "4x1",
675+ "workload": "idle",
676+ "transport": "udp_baseline",
677+ "roles": [
678+ "daemon",
679+ "client",
680+ "udp_peer"
681+ ]
682+ },
683+ {
684+ "id": "4x1-idle-udp_25ms",
685+ "topology": "4x1",
686+ "workload": "idle",
687+ "transport": "udp_25ms",
688+ "roles": [
689+ "daemon",
690+ "client",
691+ "udp_peer"
692+ ]
693+ },
694+ {
695+ "id": "4x1-idle-udp_100ms",
696+ "topology": "4x1",
697+ "workload": "idle",
698+ "transport": "udp_100ms",
699+ "roles": [
700+ "daemon",
701+ "client",
702+ "udp_peer"
703+ ]
704+ },
705+ {
706+ "id": "4x1-idle-udp_loss_1pct",
707+ "topology": "4x1",
708+ "workload": "idle",
709+ "transport": "udp_loss_1pct",
710+ "roles": [
711+ "daemon",
712+ "client",
713+ "udp_peer"
714+ ]
715+ },
716+ {
717+ "id": "4x1-active_output-local",
718+ "topology": "4x1",
719+ "workload": "active_output",
720+ "transport": "local",
721+ "roles": [
722+ "daemon",
723+ "client"
724+ ]
725+ },
726+ {
727+ "id": "4x1-active_output-ssh_stdio",
728+ "topology": "4x1",
729+ "workload": "active_output",
730+ "transport": "ssh_stdio",
731+ "roles": [
732+ "daemon",
733+ "client",
734+ "ssh_stdio_peer"
735+ ]
736+ },
737+ {
738+ "id": "4x1-active_output-udp_baseline",
739+ "topology": "4x1",
740+ "workload": "active_output",
741+ "transport": "udp_baseline",
742+ "roles": [
743+ "daemon",
744+ "client",
745+ "udp_peer"
746+ ]
747+ },
748+ {
749+ "id": "4x1-active_output-udp_25ms",
750+ "topology": "4x1",
751+ "workload": "active_output",
752+ "transport": "udp_25ms",
753+ "roles": [
754+ "daemon",
755+ "client",
756+ "udp_peer"
757+ ]
758+ },
759+ {
760+ "id": "4x1-active_output-udp_100ms",
761+ "topology": "4x1",
762+ "workload": "active_output",
763+ "transport": "udp_100ms",
764+ "roles": [
765+ "daemon",
766+ "client",
767+ "udp_peer"
768+ ]
769+ },
770+ {
771+ "id": "4x1-active_output-udp_loss_1pct",
772+ "topology": "4x1",
773+ "workload": "active_output",
774+ "transport": "udp_loss_1pct",
775+ "roles": [
776+ "daemon",
777+ "client",
778+ "udp_peer"
779+ ]
780+ },
781+ {
782+ "id": "4x1-all_output-local",
783+ "topology": "4x1",
784+ "workload": "all_output",
785+ "transport": "local",
786+ "roles": [
787+ "daemon",
788+ "client"
789+ ]
790+ },
791+ {
792+ "id": "4x1-all_output-ssh_stdio",
793+ "topology": "4x1",
794+ "workload": "all_output",
795+ "transport": "ssh_stdio",
796+ "roles": [
797+ "daemon",
798+ "client",
799+ "ssh_stdio_peer"
800+ ]
801+ },
802+ {
803+ "id": "4x1-all_output-udp_baseline",
804+ "topology": "4x1",
805+ "workload": "all_output",
806+ "transport": "udp_baseline",
807+ "roles": [
808+ "daemon",
809+ "client",
810+ "udp_peer"
811+ ]
812+ },
813+ {
814+ "id": "4x1-all_output-udp_25ms",
815+ "topology": "4x1",
816+ "workload": "all_output",
817+ "transport": "udp_25ms",
818+ "roles": [
819+ "daemon",
820+ "client",
821+ "udp_peer"
822+ ]
823+ },
824+ {
825+ "id": "4x1-all_output-udp_100ms",
826+ "topology": "4x1",
827+ "workload": "all_output",
828+ "transport": "udp_100ms",
829+ "roles": [
830+ "daemon",
831+ "client",
832+ "udp_peer"
833+ ]
834+ },
835+ {
836+ "id": "4x1-all_output-udp_loss_1pct",
837+ "topology": "4x1",
838+ "workload": "all_output",
839+ "transport": "udp_loss_1pct",
840+ "roles": [
841+ "daemon",
842+ "client",
843+ "udp_peer"
844+ ]
845+ },
846+ {
847+ "id": "4x1-inactive_output-local",
848+ "topology": "4x1",
849+ "workload": "inactive_output",
850+ "transport": "local",
851+ "roles": [
852+ "daemon",
853+ "client"
854+ ]
855+ },
856+ {
857+ "id": "4x1-inactive_output-ssh_stdio",
858+ "topology": "4x1",
859+ "workload": "inactive_output",
860+ "transport": "ssh_stdio",
861+ "roles": [
862+ "daemon",
863+ "client",
864+ "ssh_stdio_peer"
865+ ]
866+ },
867+ {
868+ "id": "4x1-inactive_output-udp_baseline",
869+ "topology": "4x1",
870+ "workload": "inactive_output",
871+ "transport": "udp_baseline",
872+ "roles": [
873+ "daemon",
874+ "client",
875+ "udp_peer"
876+ ]
877+ },
878+ {
879+ "id": "4x1-inactive_output-udp_25ms",
880+ "topology": "4x1",
881+ "workload": "inactive_output",
882+ "transport": "udp_25ms",
883+ "roles": [
884+ "daemon",
885+ "client",
886+ "udp_peer"
887+ ]
888+ },
889+ {
890+ "id": "4x1-inactive_output-udp_100ms",
891+ "topology": "4x1",
892+ "workload": "inactive_output",
893+ "transport": "udp_100ms",
894+ "roles": [
895+ "daemon",
896+ "client",
897+ "udp_peer"
898+ ]
899+ },
900+ {
901+ "id": "4x1-inactive_output-udp_loss_1pct",
902+ "topology": "4x1",
903+ "workload": "inactive_output",
904+ "transport": "udp_loss_1pct",
905+ "roles": [
906+ "daemon",
907+ "client",
908+ "udp_peer"
909+ ]
910+ },
911+ {
912+ "id": "4x1-interactive_flood-local",
913+ "topology": "4x1",
914+ "workload": "interactive_flood",
915+ "transport": "local",
916+ "roles": [
917+ "daemon",
918+ "client"
919+ ]
920+ },
921+ {
922+ "id": "4x1-interactive_flood-ssh_stdio",
923+ "topology": "4x1",
924+ "workload": "interactive_flood",
925+ "transport": "ssh_stdio",
926+ "roles": [
927+ "daemon",
928+ "client",
929+ "ssh_stdio_peer"
930+ ]
931+ },
932+ {
933+ "id": "4x1-interactive_flood-udp_baseline",
934+ "topology": "4x1",
935+ "workload": "interactive_flood",
936+ "transport": "udp_baseline",
937+ "roles": [
938+ "daemon",
939+ "client",
940+ "udp_peer"
941+ ]
942+ },
943+ {
944+ "id": "4x1-interactive_flood-udp_25ms",
945+ "topology": "4x1",
946+ "workload": "interactive_flood",
947+ "transport": "udp_25ms",
948+ "roles": [
949+ "daemon",
950+ "client",
951+ "udp_peer"
952+ ]
953+ },
954+ {
955+ "id": "4x1-interactive_flood-udp_100ms",
956+ "topology": "4x1",
957+ "workload": "interactive_flood",
958+ "transport": "udp_100ms",
959+ "roles": [
960+ "daemon",
961+ "client",
962+ "udp_peer"
963+ ]
964+ },
965+ {
966+ "id": "4x1-interactive_flood-udp_loss_1pct",
967+ "topology": "4x1",
968+ "workload": "interactive_flood",
969+ "transport": "udp_loss_1pct",
970+ "roles": [
971+ "daemon",
972+ "client",
973+ "udp_peer"
974+ ]
975+ },
976+ {
977+ "id": "4x1-copy_search-local",
978+ "topology": "4x1",
979+ "workload": "copy_search",
980+ "transport": "local",
981+ "roles": [
982+ "daemon",
983+ "client"
984+ ]
985+ },
986+ {
987+ "id": "4x1-copy_search-ssh_stdio",
988+ "topology": "4x1",
989+ "workload": "copy_search",
990+ "transport": "ssh_stdio",
991+ "roles": [
992+ "daemon",
993+ "client",
994+ "ssh_stdio_peer"
995+ ]
996+ },
997+ {
998+ "id": "4x1-copy_search-udp_baseline",
999+ "topology": "4x1",
1000+ "workload": "copy_search",
1001+ "transport": "udp_baseline",
1002+ "roles": [
1003+ "daemon",
1004+ "client",
1005+ "udp_peer"
1006+ ]
1007+ },
1008+ {
1009+ "id": "4x1-copy_search-udp_25ms",
1010+ "topology": "4x1",
1011+ "workload": "copy_search",
1012+ "transport": "udp_25ms",
1013+ "roles": [
1014+ "daemon",
1015+ "client",
1016+ "udp_peer"
1017+ ]
1018+ },
1019+ {
1020+ "id": "4x1-copy_search-udp_100ms",
1021+ "topology": "4x1",
1022+ "workload": "copy_search",
1023+ "transport": "udp_100ms",
1024+ "roles": [
1025+ "daemon",
1026+ "client",
1027+ "udp_peer"
1028+ ]
1029+ },
1030+ {
1031+ "id": "4x1-copy_search-udp_loss_1pct",
1032+ "topology": "4x1",
1033+ "workload": "copy_search",
1034+ "transport": "udp_loss_1pct",
1035+ "roles": [
1036+ "daemon",
1037+ "client",
1038+ "udp_peer"
1039+ ]
1040+ },
1041+ {
1042+ "id": "4x1-resize_sweep-local",
1043+ "topology": "4x1",
1044+ "workload": "resize_sweep",
1045+ "transport": "local",
1046+ "roles": [
1047+ "daemon",
1048+ "client"
1049+ ]
1050+ },
1051+ {
1052+ "id": "4x1-resize_sweep-ssh_stdio",
1053+ "topology": "4x1",
1054+ "workload": "resize_sweep",
1055+ "transport": "ssh_stdio",
1056+ "roles": [
1057+ "daemon",
1058+ "client",
1059+ "ssh_stdio_peer"
1060+ ]
1061+ },
1062+ {
1063+ "id": "4x1-resize_sweep-udp_baseline",
1064+ "topology": "4x1",
1065+ "workload": "resize_sweep",
1066+ "transport": "udp_baseline",
1067+ "roles": [
1068+ "daemon",
1069+ "client",
1070+ "udp_peer"
1071+ ]
1072+ },
1073+ {
1074+ "id": "4x1-resize_sweep-udp_25ms",
1075+ "topology": "4x1",
1076+ "workload": "resize_sweep",
1077+ "transport": "udp_25ms",
1078+ "roles": [
1079+ "daemon",
1080+ "client",
1081+ "udp_peer"
1082+ ]
1083+ },
1084+ {
1085+ "id": "4x1-resize_sweep-udp_100ms",
1086+ "topology": "4x1",
1087+ "workload": "resize_sweep",
1088+ "transport": "udp_100ms",
1089+ "roles": [
1090+ "daemon",
1091+ "client",
1092+ "udp_peer"
1093+ ]
1094+ },
1095+ {
1096+ "id": "4x1-resize_sweep-udp_loss_1pct",
1097+ "topology": "4x1",
1098+ "workload": "resize_sweep",
1099+ "transport": "udp_loss_1pct",
1100+ "roles": [
1101+ "daemon",
1102+ "client",
1103+ "udp_peer"
1104+ ]
1105+ },
1106+ {
1107+ "id": "4x1-snapshot_output_resize-local",
1108+ "topology": "4x1",
1109+ "workload": "snapshot_output_resize",
1110+ "transport": "local",
1111+ "roles": [
1112+ "daemon",
1113+ "client"
1114+ ]
1115+ },
1116+ {
1117+ "id": "4x1-snapshot_output_resize-ssh_stdio",
1118+ "topology": "4x1",
1119+ "workload": "snapshot_output_resize",
1120+ "transport": "ssh_stdio",
1121+ "roles": [
1122+ "daemon",
1123+ "client",
1124+ "ssh_stdio_peer"
1125+ ]
1126+ },
1127+ {
1128+ "id": "4x1-snapshot_output_resize-udp_baseline",
1129+ "topology": "4x1",
1130+ "workload": "snapshot_output_resize",
1131+ "transport": "udp_baseline",
1132+ "roles": [
1133+ "daemon",
1134+ "client",
1135+ "udp_peer"
1136+ ]
1137+ },
1138+ {
1139+ "id": "4x1-snapshot_output_resize-udp_25ms",
1140+ "topology": "4x1",
1141+ "workload": "snapshot_output_resize",
1142+ "transport": "udp_25ms",
1143+ "roles": [
1144+ "daemon",
1145+ "client",
1146+ "udp_peer"
1147+ ]
1148+ },
1149+ {
1150+ "id": "4x1-snapshot_output_resize-udp_100ms",
1151+ "topology": "4x1",
1152+ "workload": "snapshot_output_resize",
1153+ "transport": "udp_100ms",
1154+ "roles": [
1155+ "daemon",
1156+ "client",
1157+ "udp_peer"
1158+ ]
1159+ },
1160+ {
1161+ "id": "4x1-snapshot_output_resize-udp_loss_1pct",
1162+ "topology": "4x1",
1163+ "workload": "snapshot_output_resize",
1164+ "transport": "udp_loss_1pct",
1165+ "roles": [
1166+ "daemon",
1167+ "client",
1168+ "udp_peer"
1169+ ]
1170+ },
1171+ {
1172+ "id": "4x1-attach_restore_tab_switch-local",
1173+ "topology": "4x1",
1174+ "workload": "attach_restore_tab_switch",
1175+ "transport": "local",
1176+ "roles": [
1177+ "daemon",
1178+ "client"
1179+ ]
1180+ },
1181+ {
1182+ "id": "4x1-attach_restore_tab_switch-ssh_stdio",
1183+ "topology": "4x1",
1184+ "workload": "attach_restore_tab_switch",
1185+ "transport": "ssh_stdio",
1186+ "roles": [
1187+ "daemon",
1188+ "client",
1189+ "ssh_stdio_peer"
1190+ ]
1191+ },
1192+ {
1193+ "id": "4x1-attach_restore_tab_switch-udp_baseline",
1194+ "topology": "4x1",
1195+ "workload": "attach_restore_tab_switch",
1196+ "transport": "udp_baseline",
1197+ "roles": [
1198+ "daemon",
1199+ "client",
1200+ "udp_peer"
1201+ ]
1202+ },
1203+ {
1204+ "id": "4x1-attach_restore_tab_switch-udp_25ms",
1205+ "topology": "4x1",
1206+ "workload": "attach_restore_tab_switch",
1207+ "transport": "udp_25ms",
1208+ "roles": [
1209+ "daemon",
1210+ "client",
1211+ "udp_peer"
1212+ ]
1213+ },
1214+ {
1215+ "id": "4x1-attach_restore_tab_switch-udp_100ms",
1216+ "topology": "4x1",
1217+ "workload": "attach_restore_tab_switch",
1218+ "transport": "udp_100ms",
1219+ "roles": [
1220+ "daemon",
1221+ "client",
1222+ "udp_peer"
1223+ ]
1224+ },
1225+ {
1226+ "id": "4x1-attach_restore_tab_switch-udp_loss_1pct",
1227+ "topology": "4x1",
1228+ "workload": "attach_restore_tab_switch",
1229+ "transport": "udp_loss_1pct",
1230+ "roles": [
1231+ "daemon",
1232+ "client",
1233+ "udp_peer"
1234+ ]
1235+ },
1236+ {
1237+ "id": "4x4-idle-local",
1238+ "topology": "4x4",
1239+ "workload": "idle",
1240+ "transport": "local",
1241+ "roles": [
1242+ "daemon",
1243+ "client"
1244+ ]
1245+ },
1246+ {
1247+ "id": "4x4-idle-ssh_stdio",
1248+ "topology": "4x4",
1249+ "workload": "idle",
1250+ "transport": "ssh_stdio",
1251+ "roles": [
1252+ "daemon",
1253+ "client",
1254+ "ssh_stdio_peer"
1255+ ]
1256+ },
1257+ {
1258+ "id": "4x4-idle-udp_baseline",
1259+ "topology": "4x4",
1260+ "workload": "idle",
1261+ "transport": "udp_baseline",
1262+ "roles": [
1263+ "daemon",
1264+ "client",
1265+ "udp_peer"
1266+ ]
1267+ },
1268+ {
1269+ "id": "4x4-idle-udp_25ms",
1270+ "topology": "4x4",
1271+ "workload": "idle",
1272+ "transport": "udp_25ms",
1273+ "roles": [
1274+ "daemon",
1275+ "client",
1276+ "udp_peer"
1277+ ]
1278+ },
1279+ {
1280+ "id": "4x4-idle-udp_100ms",
1281+ "topology": "4x4",
1282+ "workload": "idle",
1283+ "transport": "udp_100ms",
1284+ "roles": [
1285+ "daemon",
1286+ "client",
1287+ "udp_peer"
1288+ ]
1289+ },
1290+ {
1291+ "id": "4x4-idle-udp_loss_1pct",
1292+ "topology": "4x4",
1293+ "workload": "idle",
1294+ "transport": "udp_loss_1pct",
1295+ "roles": [
1296+ "daemon",
1297+ "client",
1298+ "udp_peer"
1299+ ]
1300+ },
1301+ {
1302+ "id": "4x4-active_output-local",
1303+ "topology": "4x4",
1304+ "workload": "active_output",
1305+ "transport": "local",
1306+ "roles": [
1307+ "daemon",
1308+ "client"
1309+ ]
1310+ },
1311+ {
1312+ "id": "4x4-active_output-ssh_stdio",
1313+ "topology": "4x4",
1314+ "workload": "active_output",
1315+ "transport": "ssh_stdio",
1316+ "roles": [
1317+ "daemon",
1318+ "client",
1319+ "ssh_stdio_peer"
1320+ ]
1321+ },
1322+ {
1323+ "id": "4x4-active_output-udp_baseline",
1324+ "topology": "4x4",
1325+ "workload": "active_output",
1326+ "transport": "udp_baseline",
1327+ "roles": [
1328+ "daemon",
1329+ "client",
1330+ "udp_peer"
1331+ ]
1332+ },
1333+ {
1334+ "id": "4x4-active_output-udp_25ms",
1335+ "topology": "4x4",
1336+ "workload": "active_output",
1337+ "transport": "udp_25ms",
1338+ "roles": [
1339+ "daemon",
1340+ "client",
1341+ "udp_peer"
1342+ ]
1343+ },
1344+ {
1345+ "id": "4x4-active_output-udp_100ms",
1346+ "topology": "4x4",
1347+ "workload": "active_output",
1348+ "transport": "udp_100ms",
1349+ "roles": [
1350+ "daemon",
1351+ "client",
1352+ "udp_peer"
1353+ ]
1354+ },
1355+ {
1356+ "id": "4x4-active_output-udp_loss_1pct",
1357+ "topology": "4x4",
1358+ "workload": "active_output",
1359+ "transport": "udp_loss_1pct",
1360+ "roles": [
1361+ "daemon",
1362+ "client",
1363+ "udp_peer"
1364+ ]
1365+ },
1366+ {
1367+ "id": "4x4-all_output-local",
1368+ "topology": "4x4",
1369+ "workload": "all_output",
1370+ "transport": "local",
1371+ "roles": [
1372+ "daemon",
1373+ "client"
1374+ ]
1375+ },
1376+ {
1377+ "id": "4x4-all_output-ssh_stdio",
1378+ "topology": "4x4",
1379+ "workload": "all_output",
1380+ "transport": "ssh_stdio",
1381+ "roles": [
1382+ "daemon",
1383+ "client",
1384+ "ssh_stdio_peer"
1385+ ]
1386+ },
1387+ {
1388+ "id": "4x4-all_output-udp_baseline",
1389+ "topology": "4x4",
1390+ "workload": "all_output",
1391+ "transport": "udp_baseline",
1392+ "roles": [
1393+ "daemon",
1394+ "client",
1395+ "udp_peer"
1396+ ]
1397+ },
1398+ {
1399+ "id": "4x4-all_output-udp_25ms",
1400+ "topology": "4x4",
1401+ "workload": "all_output",
1402+ "transport": "udp_25ms",
1403+ "roles": [
1404+ "daemon",
1405+ "client",
1406+ "udp_peer"
1407+ ]
1408+ },
1409+ {
1410+ "id": "4x4-all_output-udp_100ms",
1411+ "topology": "4x4",
1412+ "workload": "all_output",
1413+ "transport": "udp_100ms",
1414+ "roles": [
1415+ "daemon",
1416+ "client",
1417+ "udp_peer"
1418+ ]
1419+ },
1420+ {
1421+ "id": "4x4-all_output-udp_loss_1pct",
1422+ "topology": "4x4",
1423+ "workload": "all_output",
1424+ "transport": "udp_loss_1pct",
1425+ "roles": [
1426+ "daemon",
1427+ "client",
1428+ "udp_peer"
1429+ ]
1430+ },
1431+ {
1432+ "id": "4x4-inactive_output-local",
1433+ "topology": "4x4",
1434+ "workload": "inactive_output",
1435+ "transport": "local",
1436+ "roles": [
1437+ "daemon",
1438+ "client"
1439+ ]
1440+ },
1441+ {
1442+ "id": "4x4-inactive_output-ssh_stdio",
1443+ "topology": "4x4",
1444+ "workload": "inactive_output",
1445+ "transport": "ssh_stdio",
1446+ "roles": [
1447+ "daemon",
1448+ "client",
1449+ "ssh_stdio_peer"
1450+ ]
1451+ },
1452+ {
1453+ "id": "4x4-inactive_output-udp_baseline",
1454+ "topology": "4x4",
1455+ "workload": "inactive_output",
1456+ "transport": "udp_baseline",
1457+ "roles": [
1458+ "daemon",
1459+ "client",
1460+ "udp_peer"
1461+ ]
1462+ },
1463+ {
1464+ "id": "4x4-inactive_output-udp_25ms",
1465+ "topology": "4x4",
1466+ "workload": "inactive_output",
1467+ "transport": "udp_25ms",
1468+ "roles": [
1469+ "daemon",
1470+ "client",
1471+ "udp_peer"
1472+ ]
1473+ },
1474+ {
1475+ "id": "4x4-inactive_output-udp_100ms",
1476+ "topology": "4x4",
1477+ "workload": "inactive_output",
1478+ "transport": "udp_100ms",
1479+ "roles": [
1480+ "daemon",
1481+ "client",
1482+ "udp_peer"
1483+ ]
1484+ },
1485+ {
1486+ "id": "4x4-inactive_output-udp_loss_1pct",
1487+ "topology": "4x4",
1488+ "workload": "inactive_output",
1489+ "transport": "udp_loss_1pct",
1490+ "roles": [
1491+ "daemon",
1492+ "client",
1493+ "udp_peer"
1494+ ]
1495+ },
1496+ {
1497+ "id": "4x4-interactive_flood-local",
1498+ "topology": "4x4",
1499+ "workload": "interactive_flood",
1500+ "transport": "local",
1501+ "roles": [
1502+ "daemon",
1503+ "client"
1504+ ]
1505+ },
1506+ {
1507+ "id": "4x4-interactive_flood-ssh_stdio",
1508+ "topology": "4x4",
1509+ "workload": "interactive_flood",
1510+ "transport": "ssh_stdio",
1511+ "roles": [
1512+ "daemon",
1513+ "client",
1514+ "ssh_stdio_peer"
1515+ ]
1516+ },
1517+ {
1518+ "id": "4x4-interactive_flood-udp_baseline",
1519+ "topology": "4x4",
1520+ "workload": "interactive_flood",
1521+ "transport": "udp_baseline",
1522+ "roles": [
1523+ "daemon",
1524+ "client",
1525+ "udp_peer"
1526+ ]
1527+ },
1528+ {
1529+ "id": "4x4-interactive_flood-udp_25ms",
1530+ "topology": "4x4",
1531+ "workload": "interactive_flood",
1532+ "transport": "udp_25ms",
1533+ "roles": [
1534+ "daemon",
1535+ "client",
1536+ "udp_peer"
1537+ ]
1538+ },
1539+ {
1540+ "id": "4x4-interactive_flood-udp_100ms",
1541+ "topology": "4x4",
1542+ "workload": "interactive_flood",
1543+ "transport": "udp_100ms",
1544+ "roles": [
1545+ "daemon",
1546+ "client",
1547+ "udp_peer"
1548+ ]
1549+ },
1550+ {
1551+ "id": "4x4-interactive_flood-udp_loss_1pct",
1552+ "topology": "4x4",
1553+ "workload": "interactive_flood",
1554+ "transport": "udp_loss_1pct",
1555+ "roles": [
1556+ "daemon",
1557+ "client",
1558+ "udp_peer"
1559+ ]
1560+ },
1561+ {
1562+ "id": "4x4-copy_search-local",
1563+ "topology": "4x4",
1564+ "workload": "copy_search",
1565+ "transport": "local",
1566+ "roles": [
1567+ "daemon",
1568+ "client"
1569+ ]
1570+ },
1571+ {
1572+ "id": "4x4-copy_search-ssh_stdio",
1573+ "topology": "4x4",
1574+ "workload": "copy_search",
1575+ "transport": "ssh_stdio",
1576+ "roles": [
1577+ "daemon",
1578+ "client",
1579+ "ssh_stdio_peer"
1580+ ]
1581+ },
1582+ {
1583+ "id": "4x4-copy_search-udp_baseline",
1584+ "topology": "4x4",
1585+ "workload": "copy_search",
1586+ "transport": "udp_baseline",
1587+ "roles": [
1588+ "daemon",
1589+ "client",
1590+ "udp_peer"
1591+ ]
1592+ },
1593+ {
1594+ "id": "4x4-copy_search-udp_25ms",
1595+ "topology": "4x4",
1596+ "workload": "copy_search",
1597+ "transport": "udp_25ms",
1598+ "roles": [
1599+ "daemon",
1600+ "client",
1601+ "udp_peer"
1602+ ]
1603+ },
1604+ {
1605+ "id": "4x4-copy_search-udp_100ms",
1606+ "topology": "4x4",
1607+ "workload": "copy_search",
1608+ "transport": "udp_100ms",
1609+ "roles": [
1610+ "daemon",
1611+ "client",
1612+ "udp_peer"
1613+ ]
1614+ },
1615+ {
1616+ "id": "4x4-copy_search-udp_loss_1pct",
1617+ "topology": "4x4",
1618+ "workload": "copy_search",
1619+ "transport": "udp_loss_1pct",
1620+ "roles": [
1621+ "daemon",
1622+ "client",
1623+ "udp_peer"
1624+ ]
1625+ },
1626+ {
1627+ "id": "4x4-resize_sweep-local",
1628+ "topology": "4x4",
1629+ "workload": "resize_sweep",
1630+ "transport": "local",
1631+ "roles": [
1632+ "daemon",
1633+ "client"
1634+ ]
1635+ },
1636+ {
1637+ "id": "4x4-resize_sweep-ssh_stdio",
1638+ "topology": "4x4",
1639+ "workload": "resize_sweep",
1640+ "transport": "ssh_stdio",
1641+ "roles": [
1642+ "daemon",
1643+ "client",
1644+ "ssh_stdio_peer"
1645+ ]
1646+ },
1647+ {
1648+ "id": "4x4-resize_sweep-udp_baseline",
1649+ "topology": "4x4",
1650+ "workload": "resize_sweep",
1651+ "transport": "udp_baseline",
1652+ "roles": [
1653+ "daemon",
1654+ "client",
1655+ "udp_peer"
1656+ ]
1657+ },
1658+ {
1659+ "id": "4x4-resize_sweep-udp_25ms",
1660+ "topology": "4x4",
1661+ "workload": "resize_sweep",
1662+ "transport": "udp_25ms",
1663+ "roles": [
1664+ "daemon",
1665+ "client",
1666+ "udp_peer"
1667+ ]
1668+ },
1669+ {
1670+ "id": "4x4-resize_sweep-udp_100ms",
1671+ "topology": "4x4",
1672+ "workload": "resize_sweep",
1673+ "transport": "udp_100ms",
1674+ "roles": [
1675+ "daemon",
1676+ "client",
1677+ "udp_peer"
1678+ ]
1679+ },
1680+ {
1681+ "id": "4x4-resize_sweep-udp_loss_1pct",
1682+ "topology": "4x4",
1683+ "workload": "resize_sweep",
1684+ "transport": "udp_loss_1pct",
1685+ "roles": [
1686+ "daemon",
1687+ "client",
1688+ "udp_peer"
1689+ ]
1690+ },
1691+ {
1692+ "id": "4x4-snapshot_output_resize-local",
1693+ "topology": "4x4",
1694+ "workload": "snapshot_output_resize",
1695+ "transport": "local",
1696+ "roles": [
1697+ "daemon",
1698+ "client"
1699+ ]
1700+ },
1701+ {
1702+ "id": "4x4-snapshot_output_resize-ssh_stdio",
1703+ "topology": "4x4",
1704+ "workload": "snapshot_output_resize",
1705+ "transport": "ssh_stdio",
1706+ "roles": [
1707+ "daemon",
1708+ "client",
1709+ "ssh_stdio_peer"
1710+ ]
1711+ },
1712+ {
1713+ "id": "4x4-snapshot_output_resize-udp_baseline",
1714+ "topology": "4x4",
1715+ "workload": "snapshot_output_resize",
1716+ "transport": "udp_baseline",
1717+ "roles": [
1718+ "daemon",
1719+ "client",
1720+ "udp_peer"
1721+ ]
1722+ },
1723+ {
1724+ "id": "4x4-snapshot_output_resize-udp_25ms",
1725+ "topology": "4x4",
1726+ "workload": "snapshot_output_resize",
1727+ "transport": "udp_25ms",
1728+ "roles": [
1729+ "daemon",
1730+ "client",
1731+ "udp_peer"
1732+ ]
1733+ },
1734+ {
1735+ "id": "4x4-snapshot_output_resize-udp_100ms",
1736+ "topology": "4x4",
1737+ "workload": "snapshot_output_resize",
1738+ "transport": "udp_100ms",
1739+ "roles": [
1740+ "daemon",
1741+ "client",
1742+ "udp_peer"
1743+ ]
1744+ },
1745+ {
1746+ "id": "4x4-snapshot_output_resize-udp_loss_1pct",
1747+ "topology": "4x4",
1748+ "workload": "snapshot_output_resize",
1749+ "transport": "udp_loss_1pct",
1750+ "roles": [
1751+ "daemon",
1752+ "client",
1753+ "udp_peer"
1754+ ]
1755+ },
1756+ {
1757+ "id": "4x4-attach_restore_tab_switch-local",
1758+ "topology": "4x4",
1759+ "workload": "attach_restore_tab_switch",
1760+ "transport": "local",
1761+ "roles": [
1762+ "daemon",
1763+ "client"
1764+ ]
1765+ },
1766+ {
1767+ "id": "4x4-attach_restore_tab_switch-ssh_stdio",
1768+ "topology": "4x4",
1769+ "workload": "attach_restore_tab_switch",
1770+ "transport": "ssh_stdio",
1771+ "roles": [
1772+ "daemon",
1773+ "client",
1774+ "ssh_stdio_peer"
1775+ ]
1776+ },
1777+ {
1778+ "id": "4x4-attach_restore_tab_switch-udp_baseline",
1779+ "topology": "4x4",
1780+ "workload": "attach_restore_tab_switch",
1781+ "transport": "udp_baseline",
1782+ "roles": [
1783+ "daemon",
1784+ "client",
1785+ "udp_peer"
1786+ ]
1787+ },
1788+ {
1789+ "id": "4x4-attach_restore_tab_switch-udp_25ms",
1790+ "topology": "4x4",
1791+ "workload": "attach_restore_tab_switch",
1792+ "transport": "udp_25ms",
1793+ "roles": [
1794+ "daemon",
1795+ "client",
1796+ "udp_peer"
1797+ ]
1798+ },
1799+ {
1800+ "id": "4x4-attach_restore_tab_switch-udp_100ms",
1801+ "topology": "4x4",
1802+ "workload": "attach_restore_tab_switch",
1803+ "transport": "udp_100ms",
1804+ "roles": [
1805+ "daemon",
1806+ "client",
1807+ "udp_peer"
1808+ ]
1809+ },
1810+ {
1811+ "id": "4x4-attach_restore_tab_switch-udp_loss_1pct",
1812+ "topology": "4x4",
1813+ "workload": "attach_restore_tab_switch",
1814+ "transport": "udp_loss_1pct",
1815+ "roles": [
1816+ "daemon",
1817+ "client",
1818+ "udp_peer"
1819+ ]
1820+ },
1821+ {
1822+ "id": "8x1-idle-local",
1823+ "topology": "8x1",
1824+ "workload": "idle",
1825+ "transport": "local",
1826+ "roles": [
1827+ "daemon",
1828+ "client"
1829+ ]
1830+ },
1831+ {
1832+ "id": "8x1-idle-ssh_stdio",
1833+ "topology": "8x1",
1834+ "workload": "idle",
1835+ "transport": "ssh_stdio",
1836+ "roles": [
1837+ "daemon",
1838+ "client",
1839+ "ssh_stdio_peer"
1840+ ]
1841+ },
1842+ {
1843+ "id": "8x1-idle-udp_baseline",
1844+ "topology": "8x1",
1845+ "workload": "idle",
1846+ "transport": "udp_baseline",
1847+ "roles": [
1848+ "daemon",
1849+ "client",
1850+ "udp_peer"
1851+ ]
1852+ },
1853+ {
1854+ "id": "8x1-idle-udp_25ms",
1855+ "topology": "8x1",
1856+ "workload": "idle",
1857+ "transport": "udp_25ms",
1858+ "roles": [
1859+ "daemon",
1860+ "client",
1861+ "udp_peer"
1862+ ]
1863+ },
1864+ {
1865+ "id": "8x1-idle-udp_100ms",
1866+ "topology": "8x1",
1867+ "workload": "idle",
1868+ "transport": "udp_100ms",
1869+ "roles": [
1870+ "daemon",
1871+ "client",
1872+ "udp_peer"
1873+ ]
1874+ },
1875+ {
1876+ "id": "8x1-idle-udp_loss_1pct",
1877+ "topology": "8x1",
1878+ "workload": "idle",
1879+ "transport": "udp_loss_1pct",
1880+ "roles": [
1881+ "daemon",
1882+ "client",
1883+ "udp_peer"
1884+ ]
1885+ },
1886+ {
1887+ "id": "8x1-active_output-local",
1888+ "topology": "8x1",
1889+ "workload": "active_output",
1890+ "transport": "local",
1891+ "roles": [
1892+ "daemon",
1893+ "client"
1894+ ]
1895+ },
1896+ {
1897+ "id": "8x1-active_output-ssh_stdio",
1898+ "topology": "8x1",
1899+ "workload": "active_output",
1900+ "transport": "ssh_stdio",
1901+ "roles": [
1902+ "daemon",
1903+ "client",
1904+ "ssh_stdio_peer"
1905+ ]
1906+ },
1907+ {
1908+ "id": "8x1-active_output-udp_baseline",
1909+ "topology": "8x1",
1910+ "workload": "active_output",
1911+ "transport": "udp_baseline",
1912+ "roles": [
1913+ "daemon",
1914+ "client",
1915+ "udp_peer"
1916+ ]
1917+ },
1918+ {
1919+ "id": "8x1-active_output-udp_25ms",
1920+ "topology": "8x1",
1921+ "workload": "active_output",
1922+ "transport": "udp_25ms",
1923+ "roles": [
1924+ "daemon",
1925+ "client",
1926+ "udp_peer"
1927+ ]
1928+ },
1929+ {
1930+ "id": "8x1-active_output-udp_100ms",
1931+ "topology": "8x1",
1932+ "workload": "active_output",
1933+ "transport": "udp_100ms",
1934+ "roles": [
1935+ "daemon",
1936+ "client",
1937+ "udp_peer"
1938+ ]
1939+ },
1940+ {
1941+ "id": "8x1-active_output-udp_loss_1pct",
1942+ "topology": "8x1",
1943+ "workload": "active_output",
1944+ "transport": "udp_loss_1pct",
1945+ "roles": [
1946+ "daemon",
1947+ "client",
1948+ "udp_peer"
1949+ ]
1950+ },
1951+ {
1952+ "id": "8x1-all_output-local",
1953+ "topology": "8x1",
1954+ "workload": "all_output",
1955+ "transport": "local",
1956+ "roles": [
1957+ "daemon",
1958+ "client"
1959+ ]
1960+ },
1961+ {
1962+ "id": "8x1-all_output-ssh_stdio",
1963+ "topology": "8x1",
1964+ "workload": "all_output",
1965+ "transport": "ssh_stdio",
1966+ "roles": [
1967+ "daemon",
1968+ "client",
1969+ "ssh_stdio_peer"
1970+ ]
1971+ },
1972+ {
1973+ "id": "8x1-all_output-udp_baseline",
1974+ "topology": "8x1",
1975+ "workload": "all_output",
1976+ "transport": "udp_baseline",
1977+ "roles": [
1978+ "daemon",
1979+ "client",
1980+ "udp_peer"
1981+ ]
1982+ },
1983+ {
1984+ "id": "8x1-all_output-udp_25ms",
1985+ "topology": "8x1",
1986+ "workload": "all_output",
1987+ "transport": "udp_25ms",
1988+ "roles": [
1989+ "daemon",
1990+ "client",
1991+ "udp_peer"
1992+ ]
1993+ },
1994+ {
1995+ "id": "8x1-all_output-udp_100ms",
1996+ "topology": "8x1",
1997+ "workload": "all_output",
1998+ "transport": "udp_100ms",
1999+ "roles": [
2000+ "daemon",
2001+ "client",
2002+ "udp_peer"
2003+ ]
2004+ },
2005+ {
2006+ "id": "8x1-all_output-udp_loss_1pct",
2007+ "topology": "8x1",
2008+ "workload": "all_output",
2009+ "transport": "udp_loss_1pct",
2010+ "roles": [
2011+ "daemon",
2012+ "client",
2013+ "udp_peer"
2014+ ]
2015+ },
2016+ {
2017+ "id": "8x1-inactive_output-local",
2018+ "topology": "8x1",
2019+ "workload": "inactive_output",
2020+ "transport": "local",
2021+ "roles": [
2022+ "daemon",
2023+ "client"
2024+ ]
2025+ },
2026+ {
2027+ "id": "8x1-inactive_output-ssh_stdio",
2028+ "topology": "8x1",
2029+ "workload": "inactive_output",
2030+ "transport": "ssh_stdio",
2031+ "roles": [
2032+ "daemon",
2033+ "client",
2034+ "ssh_stdio_peer"
2035+ ]
2036+ },
2037+ {
2038+ "id": "8x1-inactive_output-udp_baseline",
2039+ "topology": "8x1",
2040+ "workload": "inactive_output",
2041+ "transport": "udp_baseline",
2042+ "roles": [
2043+ "daemon",
2044+ "client",
2045+ "udp_peer"
2046+ ]
2047+ },
2048+ {
2049+ "id": "8x1-inactive_output-udp_25ms",
2050+ "topology": "8x1",
2051+ "workload": "inactive_output",
2052+ "transport": "udp_25ms",
2053+ "roles": [
2054+ "daemon",
2055+ "client",
2056+ "udp_peer"
2057+ ]
2058+ },
2059+ {
2060+ "id": "8x1-inactive_output-udp_100ms",
2061+ "topology": "8x1",
2062+ "workload": "inactive_output",
2063+ "transport": "udp_100ms",
2064+ "roles": [
2065+ "daemon",
2066+ "client",
2067+ "udp_peer"
2068+ ]
2069+ },
2070+ {
2071+ "id": "8x1-inactive_output-udp_loss_1pct",
2072+ "topology": "8x1",
2073+ "workload": "inactive_output",
2074+ "transport": "udp_loss_1pct",
2075+ "roles": [
2076+ "daemon",
2077+ "client",
2078+ "udp_peer"
2079+ ]
2080+ },
2081+ {
2082+ "id": "8x1-interactive_flood-local",
2083+ "topology": "8x1",
2084+ "workload": "interactive_flood",
2085+ "transport": "local",
2086+ "roles": [
2087+ "daemon",
2088+ "client"
2089+ ]
2090+ },
2091+ {
2092+ "id": "8x1-interactive_flood-ssh_stdio",
2093+ "topology": "8x1",
2094+ "workload": "interactive_flood",
2095+ "transport": "ssh_stdio",
2096+ "roles": [
2097+ "daemon",
2098+ "client",
2099+ "ssh_stdio_peer"
2100+ ]
2101+ },
2102+ {
2103+ "id": "8x1-interactive_flood-udp_baseline",
2104+ "topology": "8x1",
2105+ "workload": "interactive_flood",
2106+ "transport": "udp_baseline",
2107+ "roles": [
2108+ "daemon",
2109+ "client",
2110+ "udp_peer"
2111+ ]
2112+ },
2113+ {
2114+ "id": "8x1-interactive_flood-udp_25ms",
2115+ "topology": "8x1",
2116+ "workload": "interactive_flood",
2117+ "transport": "udp_25ms",
2118+ "roles": [
2119+ "daemon",
2120+ "client",
2121+ "udp_peer"
2122+ ]
2123+ },
2124+ {
2125+ "id": "8x1-interactive_flood-udp_100ms",
2126+ "topology": "8x1",
2127+ "workload": "interactive_flood",
2128+ "transport": "udp_100ms",
2129+ "roles": [
2130+ "daemon",
2131+ "client",
2132+ "udp_peer"
2133+ ]
2134+ },
2135+ {
2136+ "id": "8x1-interactive_flood-udp_loss_1pct",
2137+ "topology": "8x1",
2138+ "workload": "interactive_flood",
2139+ "transport": "udp_loss_1pct",
2140+ "roles": [
2141+ "daemon",
2142+ "client",
2143+ "udp_peer"
2144+ ]
2145+ },
2146+ {
2147+ "id": "8x1-copy_search-local",
2148+ "topology": "8x1",
2149+ "workload": "copy_search",
2150+ "transport": "local",
2151+ "roles": [
2152+ "daemon",
2153+ "client"
2154+ ]
2155+ },
2156+ {
2157+ "id": "8x1-copy_search-ssh_stdio",
2158+ "topology": "8x1",
2159+ "workload": "copy_search",
2160+ "transport": "ssh_stdio",
2161+ "roles": [
2162+ "daemon",
2163+ "client",
2164+ "ssh_stdio_peer"
2165+ ]
2166+ },
2167+ {
2168+ "id": "8x1-copy_search-udp_baseline",
2169+ "topology": "8x1",
2170+ "workload": "copy_search",
2171+ "transport": "udp_baseline",
2172+ "roles": [
2173+ "daemon",
2174+ "client",
2175+ "udp_peer"
2176+ ]
2177+ },
2178+ {
2179+ "id": "8x1-copy_search-udp_25ms",
2180+ "topology": "8x1",
2181+ "workload": "copy_search",
2182+ "transport": "udp_25ms",
2183+ "roles": [
2184+ "daemon",
2185+ "client",
2186+ "udp_peer"
2187+ ]
2188+ },
2189+ {
2190+ "id": "8x1-copy_search-udp_100ms",
2191+ "topology": "8x1",
2192+ "workload": "copy_search",
2193+ "transport": "udp_100ms",
2194+ "roles": [
2195+ "daemon",
2196+ "client",
2197+ "udp_peer"
2198+ ]
2199+ },
2200+ {
2201+ "id": "8x1-copy_search-udp_loss_1pct",
2202+ "topology": "8x1",
2203+ "workload": "copy_search",
2204+ "transport": "udp_loss_1pct",
2205+ "roles": [
2206+ "daemon",
2207+ "client",
2208+ "udp_peer"
2209+ ]
2210+ },
2211+ {
2212+ "id": "8x1-resize_sweep-local",
2213+ "topology": "8x1",
2214+ "workload": "resize_sweep",
2215+ "transport": "local",
2216+ "roles": [
2217+ "daemon",
2218+ "client"
2219+ ]
2220+ },
2221+ {
2222+ "id": "8x1-resize_sweep-ssh_stdio",
2223+ "topology": "8x1",
2224+ "workload": "resize_sweep",
2225+ "transport": "ssh_stdio",
2226+ "roles": [
2227+ "daemon",
2228+ "client",
2229+ "ssh_stdio_peer"
2230+ ]
2231+ },
2232+ {
2233+ "id": "8x1-resize_sweep-udp_baseline",
2234+ "topology": "8x1",
2235+ "workload": "resize_sweep",
2236+ "transport": "udp_baseline",
2237+ "roles": [
2238+ "daemon",
2239+ "client",
2240+ "udp_peer"
2241+ ]
2242+ },
2243+ {
2244+ "id": "8x1-resize_sweep-udp_25ms",
2245+ "topology": "8x1",
2246+ "workload": "resize_sweep",
2247+ "transport": "udp_25ms",
2248+ "roles": [
2249+ "daemon",
2250+ "client",
2251+ "udp_peer"
2252+ ]
2253+ },
2254+ {
2255+ "id": "8x1-resize_sweep-udp_100ms",
2256+ "topology": "8x1",
2257+ "workload": "resize_sweep",
2258+ "transport": "udp_100ms",
2259+ "roles": [
2260+ "daemon",
2261+ "client",
2262+ "udp_peer"
2263+ ]
2264+ },
2265+ {
2266+ "id": "8x1-resize_sweep-udp_loss_1pct",
2267+ "topology": "8x1",
2268+ "workload": "resize_sweep",
2269+ "transport": "udp_loss_1pct",
2270+ "roles": [
2271+ "daemon",
2272+ "client",
2273+ "udp_peer"
2274+ ]
2275+ },
2276+ {
2277+ "id": "8x1-snapshot_output_resize-local",
2278+ "topology": "8x1",
2279+ "workload": "snapshot_output_resize",
2280+ "transport": "local",
2281+ "roles": [
2282+ "daemon",
2283+ "client"
2284+ ]
2285+ },
2286+ {
2287+ "id": "8x1-snapshot_output_resize-ssh_stdio",
2288+ "topology": "8x1",
2289+ "workload": "snapshot_output_resize",
2290+ "transport": "ssh_stdio",
2291+ "roles": [
2292+ "daemon",
2293+ "client",
2294+ "ssh_stdio_peer"
2295+ ]
2296+ },
2297+ {
2298+ "id": "8x1-snapshot_output_resize-udp_baseline",
2299+ "topology": "8x1",
2300+ "workload": "snapshot_output_resize",
2301+ "transport": "udp_baseline",
2302+ "roles": [
2303+ "daemon",
2304+ "client",
2305+ "udp_peer"
2306+ ]
2307+ },
2308+ {
2309+ "id": "8x1-snapshot_output_resize-udp_25ms",
2310+ "topology": "8x1",
2311+ "workload": "snapshot_output_resize",
2312+ "transport": "udp_25ms",
2313+ "roles": [
2314+ "daemon",
2315+ "client",
2316+ "udp_peer"
2317+ ]
2318+ },
2319+ {
2320+ "id": "8x1-snapshot_output_resize-udp_100ms",
2321+ "topology": "8x1",
2322+ "workload": "snapshot_output_resize",
2323+ "transport": "udp_100ms",
2324+ "roles": [
2325+ "daemon",
2326+ "client",
2327+ "udp_peer"
2328+ ]
2329+ },
2330+ {
2331+ "id": "8x1-snapshot_output_resize-udp_loss_1pct",
2332+ "topology": "8x1",
2333+ "workload": "snapshot_output_resize",
2334+ "transport": "udp_loss_1pct",
2335+ "roles": [
2336+ "daemon",
2337+ "client",
2338+ "udp_peer"
2339+ ]
2340+ },
2341+ {
2342+ "id": "8x1-attach_restore_tab_switch-local",
2343+ "topology": "8x1",
2344+ "workload": "attach_restore_tab_switch",
2345+ "transport": "local",
2346+ "roles": [
2347+ "daemon",
2348+ "client"
2349+ ]
2350+ },
2351+ {
2352+ "id": "8x1-attach_restore_tab_switch-ssh_stdio",
2353+ "topology": "8x1",
2354+ "workload": "attach_restore_tab_switch",
2355+ "transport": "ssh_stdio",
2356+ "roles": [
2357+ "daemon",
2358+ "client",
2359+ "ssh_stdio_peer"
2360+ ]
2361+ },
2362+ {
2363+ "id": "8x1-attach_restore_tab_switch-udp_baseline",
2364+ "topology": "8x1",
2365+ "workload": "attach_restore_tab_switch",
2366+ "transport": "udp_baseline",
2367+ "roles": [
2368+ "daemon",
2369+ "client",
2370+ "udp_peer"
2371+ ]
2372+ },
2373+ {
2374+ "id": "8x1-attach_restore_tab_switch-udp_25ms",
2375+ "topology": "8x1",
2376+ "workload": "attach_restore_tab_switch",
2377+ "transport": "udp_25ms",
2378+ "roles": [
2379+ "daemon",
2380+ "client",
2381+ "udp_peer"
2382+ ]
2383+ },
2384+ {
2385+ "id": "8x1-attach_restore_tab_switch-udp_100ms",
2386+ "topology": "8x1",
2387+ "workload": "attach_restore_tab_switch",
2388+ "transport": "udp_100ms",
2389+ "roles": [
2390+ "daemon",
2391+ "client",
2392+ "udp_peer"
2393+ ]
2394+ },
2395+ {
2396+ "id": "8x1-attach_restore_tab_switch-udp_loss_1pct",
2397+ "topology": "8x1",
2398+ "workload": "attach_restore_tab_switch",
2399+ "transport": "udp_loss_1pct",
2400+ "roles": [
2401+ "daemon",
2402+ "client",
2403+ "udp_peer"
2404+ ]
2405+ }
2406+ ]
2407+}
@@ -0,0 +1,2407 @@
1+{
2+ "schema": 1,
3+ "topologies": [
4+ {
5+ "id": "1x4",
6+ "geometry": "120x40",
7+ "rows_per_pane": 10000
8+ },
9+ {
10+ "id": "4x1",
11+ "geometry": "120x40",
12+ "rows_per_pane": 10000
13+ },
14+ {
15+ "id": "4x4",
16+ "geometry": "120x40",
17+ "rows_per_pane": 10000
18+ },
19+ {
20+ "id": "8x1",
21+ "geometry": "120x40",
22+ "rows_per_pane": 10000
23+ }
24+ ],
25+ "workloads": [
26+ "idle",
27+ "active_output",
28+ "all_output",
29+ "inactive_output",
30+ "interactive_flood",
31+ "copy_search",
32+ "resize_sweep",
33+ "snapshot_output_resize",
34+ "attach_restore_tab_switch"
35+ ],
36+ "transports": [
37+ {
38+ "id": "local",
39+ "kind": "local"
40+ },
41+ {
42+ "id": "ssh_stdio",
43+ "kind": "ssh_stdio"
44+ },
45+ {
46+ "id": "udp_baseline",
47+ "kind": "udp"
48+ },
49+ {
50+ "id": "udp_25ms",
51+ "kind": "udp",
52+ "rtt_ms": 25
53+ },
54+ {
55+ "id": "udp_100ms",
56+ "kind": "udp",
57+ "rtt_ms": 100
58+ },
59+ {
60+ "id": "udp_loss_1pct",
61+ "kind": "udp",
62+ "loss_percent": 1
63+ }
64+ ],
65+ "scenarios": [
66+ {
67+ "id": "1x4-idle-local",
68+ "topology": "1x4",
69+ "workload": "idle",
70+ "transport": "local",
71+ "roles": [
72+ "daemon",
73+ "client"
74+ ]
75+ },
76+ {
77+ "id": "1x4-idle-ssh_stdio",
78+ "topology": "1x4",
79+ "workload": "idle",
80+ "transport": "ssh_stdio",
81+ "roles": [
82+ "daemon",
83+ "client",
84+ "ssh_stdio_peer"
85+ ]
86+ },
87+ {
88+ "id": "1x4-idle-udp_baseline",
89+ "topology": "1x4",
90+ "workload": "idle",
91+ "transport": "udp_baseline",
92+ "roles": [
93+ "daemon",
94+ "client",
95+ "udp_peer"
96+ ]
97+ },
98+ {
99+ "id": "1x4-idle-udp_25ms",
100+ "topology": "1x4",
101+ "workload": "idle",
102+ "transport": "udp_25ms",
103+ "roles": [
104+ "daemon",
105+ "client",
106+ "udp_peer"
107+ ]
108+ },
109+ {
110+ "id": "1x4-idle-udp_100ms",
111+ "topology": "1x4",
112+ "workload": "idle",
113+ "transport": "udp_100ms",
114+ "roles": [
115+ "daemon",
116+ "client",
117+ "udp_peer"
118+ ]
119+ },
120+ {
121+ "id": "1x4-idle-udp_loss_1pct",
122+ "topology": "1x4",
123+ "workload": "idle",
124+ "transport": "udp_loss_1pct",
125+ "roles": [
126+ "daemon",
127+ "client",
128+ "udp_peer"
129+ ]
130+ },
131+ {
132+ "id": "1x4-active_output-local",
133+ "topology": "1x4",
134+ "workload": "active_output",
135+ "transport": "local",
136+ "roles": [
137+ "daemon",
138+ "client"
139+ ]
140+ },
141+ {
142+ "id": "1x4-active_output-ssh_stdio",
143+ "topology": "1x4",
144+ "workload": "active_output",
145+ "transport": "ssh_stdio",
146+ "roles": [
147+ "daemon",
148+ "client",
149+ "ssh_stdio_peer"
150+ ]
151+ },
152+ {
153+ "id": "1x4-active_output-udp_baseline",
154+ "topology": "1x4",
155+ "workload": "active_output",
156+ "transport": "udp_baseline",
157+ "roles": [
158+ "daemon",
159+ "client",
160+ "udp_peer"
161+ ]
162+ },
163+ {
164+ "id": "1x4-active_output-udp_25ms",
165+ "topology": "1x4",
166+ "workload": "active_output",
167+ "transport": "udp_25ms",
168+ "roles": [
169+ "daemon",
170+ "client",
171+ "udp_peer"
172+ ]
173+ },
174+ {
175+ "id": "1x4-active_output-udp_100ms",
176+ "topology": "1x4",
177+ "workload": "active_output",
178+ "transport": "udp_100ms",
179+ "roles": [
180+ "daemon",
181+ "client",
182+ "udp_peer"
183+ ]
184+ },
185+ {
186+ "id": "1x4-active_output-udp_loss_1pct",
187+ "topology": "1x4",
188+ "workload": "active_output",
189+ "transport": "udp_loss_1pct",
190+ "roles": [
191+ "daemon",
192+ "client",
193+ "udp_peer"
194+ ]
195+ },
196+ {
197+ "id": "1x4-all_output-local",
198+ "topology": "1x4",
199+ "workload": "all_output",
200+ "transport": "local",
201+ "roles": [
202+ "daemon",
203+ "client"
204+ ]
205+ },
206+ {
207+ "id": "1x4-all_output-ssh_stdio",
208+ "topology": "1x4",
209+ "workload": "all_output",
210+ "transport": "ssh_stdio",
211+ "roles": [
212+ "daemon",
213+ "client",
214+ "ssh_stdio_peer"
215+ ]
216+ },
217+ {
218+ "id": "1x4-all_output-udp_baseline",
219+ "topology": "1x4",
220+ "workload": "all_output",
221+ "transport": "udp_baseline",
222+ "roles": [
223+ "daemon",
224+ "client",
225+ "udp_peer"
226+ ]
227+ },
228+ {
229+ "id": "1x4-all_output-udp_25ms",
230+ "topology": "1x4",
231+ "workload": "all_output",
232+ "transport": "udp_25ms",
233+ "roles": [
234+ "daemon",
235+ "client",
236+ "udp_peer"
237+ ]
238+ },
239+ {
240+ "id": "1x4-all_output-udp_100ms",
241+ "topology": "1x4",
242+ "workload": "all_output",
243+ "transport": "udp_100ms",
244+ "roles": [
245+ "daemon",
246+ "client",
247+ "udp_peer"
248+ ]
249+ },
250+ {
251+ "id": "1x4-all_output-udp_loss_1pct",
252+ "topology": "1x4",
253+ "workload": "all_output",
254+ "transport": "udp_loss_1pct",
255+ "roles": [
256+ "daemon",
257+ "client",
258+ "udp_peer"
259+ ]
260+ },
261+ {
262+ "id": "1x4-inactive_output-local",
263+ "topology": "1x4",
264+ "workload": "inactive_output",
265+ "transport": "local",
266+ "roles": [
267+ "daemon",
268+ "client"
269+ ]
270+ },
271+ {
272+ "id": "1x4-inactive_output-ssh_stdio",
273+ "topology": "1x4",
274+ "workload": "inactive_output",
275+ "transport": "ssh_stdio",
276+ "roles": [
277+ "daemon",
278+ "client",
279+ "ssh_stdio_peer"
280+ ]
281+ },
282+ {
283+ "id": "1x4-inactive_output-udp_baseline",
284+ "topology": "1x4",
285+ "workload": "inactive_output",
286+ "transport": "udp_baseline",
287+ "roles": [
288+ "daemon",
289+ "client",
290+ "udp_peer"
291+ ]
292+ },
293+ {
294+ "id": "1x4-inactive_output-udp_25ms",
295+ "topology": "1x4",
296+ "workload": "inactive_output",
297+ "transport": "udp_25ms",
298+ "roles": [
299+ "daemon",
300+ "client",
301+ "udp_peer"
302+ ]
303+ },
304+ {
305+ "id": "1x4-inactive_output-udp_100ms",
306+ "topology": "1x4",
307+ "workload": "inactive_output",
308+ "transport": "udp_100ms",
309+ "roles": [
310+ "daemon",
311+ "client",
312+ "udp_peer"
313+ ]
314+ },
315+ {
316+ "id": "1x4-inactive_output-udp_loss_1pct",
317+ "topology": "1x4",
318+ "workload": "inactive_output",
319+ "transport": "udp_loss_1pct",
320+ "roles": [
321+ "daemon",
322+ "client",
323+ "udp_peer"
324+ ]
325+ },
326+ {
327+ "id": "1x4-interactive_flood-local",
328+ "topology": "1x4",
329+ "workload": "interactive_flood",
330+ "transport": "local",
331+ "roles": [
332+ "daemon",
333+ "client"
334+ ]
335+ },
336+ {
337+ "id": "1x4-interactive_flood-ssh_stdio",
338+ "topology": "1x4",
339+ "workload": "interactive_flood",
340+ "transport": "ssh_stdio",
341+ "roles": [
342+ "daemon",
343+ "client",
344+ "ssh_stdio_peer"
345+ ]
346+ },
347+ {
348+ "id": "1x4-interactive_flood-udp_baseline",
349+ "topology": "1x4",
350+ "workload": "interactive_flood",
351+ "transport": "udp_baseline",
352+ "roles": [
353+ "daemon",
354+ "client",
355+ "udp_peer"
356+ ]
357+ },
358+ {
359+ "id": "1x4-interactive_flood-udp_25ms",
360+ "topology": "1x4",
361+ "workload": "interactive_flood",
362+ "transport": "udp_25ms",
363+ "roles": [
364+ "daemon",
365+ "client",
366+ "udp_peer"
367+ ]
368+ },
369+ {
370+ "id": "1x4-interactive_flood-udp_100ms",
371+ "topology": "1x4",
372+ "workload": "interactive_flood",
373+ "transport": "udp_100ms",
374+ "roles": [
375+ "daemon",
376+ "client",
377+ "udp_peer"
378+ ]
379+ },
380+ {
381+ "id": "1x4-interactive_flood-udp_loss_1pct",
382+ "topology": "1x4",
383+ "workload": "interactive_flood",
384+ "transport": "udp_loss_1pct",
385+ "roles": [
386+ "daemon",
387+ "client",
388+ "udp_peer"
389+ ]
390+ },
391+ {
392+ "id": "1x4-copy_search-local",
393+ "topology": "1x4",
394+ "workload": "copy_search",
395+ "transport": "local",
396+ "roles": [
397+ "daemon",
398+ "client"
399+ ]
400+ },
401+ {
402+ "id": "1x4-copy_search-ssh_stdio",
403+ "topology": "1x4",
404+ "workload": "copy_search",
405+ "transport": "ssh_stdio",
406+ "roles": [
407+ "daemon",
408+ "client",
409+ "ssh_stdio_peer"
410+ ]
411+ },
412+ {
413+ "id": "1x4-copy_search-udp_baseline",
414+ "topology": "1x4",
415+ "workload": "copy_search",
416+ "transport": "udp_baseline",
417+ "roles": [
418+ "daemon",
419+ "client",
420+ "udp_peer"
421+ ]
422+ },
423+ {
424+ "id": "1x4-copy_search-udp_25ms",
425+ "topology": "1x4",
426+ "workload": "copy_search",
427+ "transport": "udp_25ms",
428+ "roles": [
429+ "daemon",
430+ "client",
431+ "udp_peer"
432+ ]
433+ },
434+ {
435+ "id": "1x4-copy_search-udp_100ms",
436+ "topology": "1x4",
437+ "workload": "copy_search",
438+ "transport": "udp_100ms",
439+ "roles": [
440+ "daemon",
441+ "client",
442+ "udp_peer"
443+ ]
444+ },
445+ {
446+ "id": "1x4-copy_search-udp_loss_1pct",
447+ "topology": "1x4",
448+ "workload": "copy_search",
449+ "transport": "udp_loss_1pct",
450+ "roles": [
451+ "daemon",
452+ "client",
453+ "udp_peer"
454+ ]
455+ },
456+ {
457+ "id": "1x4-resize_sweep-local",
458+ "topology": "1x4",
459+ "workload": "resize_sweep",
460+ "transport": "local",
461+ "roles": [
462+ "daemon",
463+ "client"
464+ ]
465+ },
466+ {
467+ "id": "1x4-resize_sweep-ssh_stdio",
468+ "topology": "1x4",
469+ "workload": "resize_sweep",
470+ "transport": "ssh_stdio",
471+ "roles": [
472+ "daemon",
473+ "client",
474+ "ssh_stdio_peer"
475+ ]
476+ },
477+ {
478+ "id": "1x4-resize_sweep-udp_baseline",
479+ "topology": "1x4",
480+ "workload": "resize_sweep",
481+ "transport": "udp_baseline",
482+ "roles": [
483+ "daemon",
484+ "client",
485+ "udp_peer"
486+ ]
487+ },
488+ {
489+ "id": "1x4-resize_sweep-udp_25ms",
490+ "topology": "1x4",
491+ "workload": "resize_sweep",
492+ "transport": "udp_25ms",
493+ "roles": [
494+ "daemon",
495+ "client",
496+ "udp_peer"
497+ ]
498+ },
499+ {
500+ "id": "1x4-resize_sweep-udp_100ms",
501+ "topology": "1x4",
502+ "workload": "resize_sweep",
503+ "transport": "udp_100ms",
504+ "roles": [
505+ "daemon",
506+ "client",
507+ "udp_peer"
508+ ]
509+ },
510+ {
511+ "id": "1x4-resize_sweep-udp_loss_1pct",
512+ "topology": "1x4",
513+ "workload": "resize_sweep",
514+ "transport": "udp_loss_1pct",
515+ "roles": [
516+ "daemon",
517+ "client",
518+ "udp_peer"
519+ ]
520+ },
521+ {
522+ "id": "1x4-snapshot_output_resize-local",
523+ "topology": "1x4",
524+ "workload": "snapshot_output_resize",
525+ "transport": "local",
526+ "roles": [
527+ "daemon",
528+ "client"
529+ ]
530+ },
531+ {
532+ "id": "1x4-snapshot_output_resize-ssh_stdio",
533+ "topology": "1x4",
534+ "workload": "snapshot_output_resize",
535+ "transport": "ssh_stdio",
536+ "roles": [
537+ "daemon",
538+ "client",
539+ "ssh_stdio_peer"
540+ ]
541+ },
542+ {
543+ "id": "1x4-snapshot_output_resize-udp_baseline",
544+ "topology": "1x4",
545+ "workload": "snapshot_output_resize",
546+ "transport": "udp_baseline",
547+ "roles": [
548+ "daemon",
549+ "client",
550+ "udp_peer"
551+ ]
552+ },
553+ {
554+ "id": "1x4-snapshot_output_resize-udp_25ms",
555+ "topology": "1x4",
556+ "workload": "snapshot_output_resize",
557+ "transport": "udp_25ms",
558+ "roles": [
559+ "daemon",
560+ "client",
561+ "udp_peer"
562+ ]
563+ },
564+ {
565+ "id": "1x4-snapshot_output_resize-udp_100ms",
566+ "topology": "1x4",
567+ "workload": "snapshot_output_resize",
568+ "transport": "udp_100ms",
569+ "roles": [
570+ "daemon",
571+ "client",
572+ "udp_peer"
573+ ]
574+ },
575+ {
576+ "id": "1x4-snapshot_output_resize-udp_loss_1pct",
577+ "topology": "1x4",
578+ "workload": "snapshot_output_resize",
579+ "transport": "udp_loss_1pct",
580+ "roles": [
581+ "daemon",
582+ "client",
583+ "udp_peer"
584+ ]
585+ },
586+ {
587+ "id": "1x4-attach_restore_tab_switch-local",
588+ "topology": "1x4",
589+ "workload": "attach_restore_tab_switch",
590+ "transport": "local",
591+ "roles": [
592+ "daemon",
593+ "client"
594+ ]
595+ },
596+ {
597+ "id": "1x4-attach_restore_tab_switch-ssh_stdio",
598+ "topology": "1x4",
599+ "workload": "attach_restore_tab_switch",
600+ "transport": "ssh_stdio",
601+ "roles": [
602+ "daemon",
603+ "client",
604+ "ssh_stdio_peer"
605+ ]
606+ },
607+ {
608+ "id": "1x4-attach_restore_tab_switch-udp_baseline",
609+ "topology": "1x4",
610+ "workload": "attach_restore_tab_switch",
611+ "transport": "udp_baseline",
612+ "roles": [
613+ "daemon",
614+ "client",
615+ "udp_peer"
616+ ]
617+ },
618+ {
619+ "id": "1x4-attach_restore_tab_switch-udp_25ms",
620+ "topology": "1x4",
621+ "workload": "attach_restore_tab_switch",
622+ "transport": "udp_25ms",
623+ "roles": [
624+ "daemon",
625+ "client",
626+ "udp_peer"
627+ ]
628+ },
629+ {
630+ "id": "1x4-attach_restore_tab_switch-udp_100ms",
631+ "topology": "1x4",
632+ "workload": "attach_restore_tab_switch",
633+ "transport": "udp_100ms",
634+ "roles": [
635+ "daemon",
636+ "client",
637+ "udp_peer"
638+ ]
639+ },
640+ {
641+ "id": "1x4-attach_restore_tab_switch-udp_loss_1pct",
642+ "topology": "1x4",
643+ "workload": "attach_restore_tab_switch",
644+ "transport": "udp_loss_1pct",
645+ "roles": [
646+ "daemon",
647+ "client",
648+ "udp_peer"
649+ ]
650+ },
651+ {
652+ "id": "4x1-idle-local",
653+ "topology": "4x1",
654+ "workload": "idle",
655+ "transport": "local",
656+ "roles": [
657+ "daemon",
658+ "client"
659+ ]
660+ },
661+ {
662+ "id": "4x1-idle-ssh_stdio",
663+ "topology": "4x1",
664+ "workload": "idle",
665+ "transport": "ssh_stdio",
666+ "roles": [
667+ "daemon",
668+ "client",
669+ "ssh_stdio_peer"
670+ ]
671+ },
672+ {
673+ "id": "4x1-idle-udp_baseline",
674+ "topology": "4x1",
675+ "workload": "idle",
676+ "transport": "udp_baseline",
677+ "roles": [
678+ "daemon",
679+ "client",
680+ "udp_peer"
681+ ]
682+ },
683+ {
684+ "id": "4x1-idle-udp_25ms",
685+ "topology": "4x1",
686+ "workload": "idle",
687+ "transport": "udp_25ms",
688+ "roles": [
689+ "daemon",
690+ "client",
691+ "udp_peer"
692+ ]
693+ },
694+ {
695+ "id": "4x1-idle-udp_100ms",
696+ "topology": "4x1",
697+ "workload": "idle",
698+ "transport": "udp_100ms",
699+ "roles": [
700+ "daemon",
701+ "client",
702+ "udp_peer"
703+ ]
704+ },
705+ {
706+ "id": "4x1-idle-udp_loss_1pct",
707+ "topology": "4x1",
708+ "workload": "idle",
709+ "transport": "udp_loss_1pct",
710+ "roles": [
711+ "daemon",
712+ "client",
713+ "udp_peer"
714+ ]
715+ },
716+ {
717+ "id": "4x1-active_output-local",
718+ "topology": "4x1",
719+ "workload": "active_output",
720+ "transport": "local",
721+ "roles": [
722+ "daemon",
723+ "client"
724+ ]
725+ },
726+ {
727+ "id": "4x1-active_output-ssh_stdio",
728+ "topology": "4x1",
729+ "workload": "active_output",
730+ "transport": "ssh_stdio",
731+ "roles": [
732+ "daemon",
733+ "client",
734+ "ssh_stdio_peer"
735+ ]
736+ },
737+ {
738+ "id": "4x1-active_output-udp_baseline",
739+ "topology": "4x1",
740+ "workload": "active_output",
741+ "transport": "udp_baseline",
742+ "roles": [
743+ "daemon",
744+ "client",
745+ "udp_peer"
746+ ]
747+ },
748+ {
749+ "id": "4x1-active_output-udp_25ms",
750+ "topology": "4x1",
751+ "workload": "active_output",
752+ "transport": "udp_25ms",
753+ "roles": [
754+ "daemon",
755+ "client",
756+ "udp_peer"
757+ ]
758+ },
759+ {
760+ "id": "4x1-active_output-udp_100ms",
761+ "topology": "4x1",
762+ "workload": "active_output",
763+ "transport": "udp_100ms",
764+ "roles": [
765+ "daemon",
766+ "client",
767+ "udp_peer"
768+ ]
769+ },
770+ {
771+ "id": "4x1-active_output-udp_loss_1pct",
772+ "topology": "4x1",
773+ "workload": "active_output",
774+ "transport": "udp_loss_1pct",
775+ "roles": [
776+ "daemon",
777+ "client",
778+ "udp_peer"
779+ ]
780+ },
781+ {
782+ "id": "4x1-all_output-local",
783+ "topology": "4x1",
784+ "workload": "all_output",
785+ "transport": "local",
786+ "roles": [
787+ "daemon",
788+ "client"
789+ ]
790+ },
791+ {
792+ "id": "4x1-all_output-ssh_stdio",
793+ "topology": "4x1",
794+ "workload": "all_output",
795+ "transport": "ssh_stdio",
796+ "roles": [
797+ "daemon",
798+ "client",
799+ "ssh_stdio_peer"
800+ ]
801+ },
802+ {
803+ "id": "4x1-all_output-udp_baseline",
804+ "topology": "4x1",
805+ "workload": "all_output",
806+ "transport": "udp_baseline",
807+ "roles": [
808+ "daemon",
809+ "client",
810+ "udp_peer"
811+ ]
812+ },
813+ {
814+ "id": "4x1-all_output-udp_25ms",
815+ "topology": "4x1",
816+ "workload": "all_output",
817+ "transport": "udp_25ms",
818+ "roles": [
819+ "daemon",
820+ "client",
821+ "udp_peer"
822+ ]
823+ },
824+ {
825+ "id": "4x1-all_output-udp_100ms",
826+ "topology": "4x1",
827+ "workload": "all_output",
828+ "transport": "udp_100ms",
829+ "roles": [
830+ "daemon",
831+ "client",
832+ "udp_peer"
833+ ]
834+ },
835+ {
836+ "id": "4x1-all_output-udp_loss_1pct",
837+ "topology": "4x1",
838+ "workload": "all_output",
839+ "transport": "udp_loss_1pct",
840+ "roles": [
841+ "daemon",
842+ "client",
843+ "udp_peer"
844+ ]
845+ },
846+ {
847+ "id": "4x1-inactive_output-local",
848+ "topology": "4x1",
849+ "workload": "inactive_output",
850+ "transport": "local",
851+ "roles": [
852+ "daemon",
853+ "client"
854+ ]
855+ },
856+ {
857+ "id": "4x1-inactive_output-ssh_stdio",
858+ "topology": "4x1",
859+ "workload": "inactive_output",
860+ "transport": "ssh_stdio",
861+ "roles": [
862+ "daemon",
863+ "client",
864+ "ssh_stdio_peer"
865+ ]
866+ },
867+ {
868+ "id": "4x1-inactive_output-udp_baseline",
869+ "topology": "4x1",
870+ "workload": "inactive_output",
871+ "transport": "udp_baseline",
872+ "roles": [
873+ "daemon",
874+ "client",
875+ "udp_peer"
876+ ]
877+ },
878+ {
879+ "id": "4x1-inactive_output-udp_25ms",
880+ "topology": "4x1",
881+ "workload": "inactive_output",
882+ "transport": "udp_25ms",
883+ "roles": [
884+ "daemon",
885+ "client",
886+ "udp_peer"
887+ ]
888+ },
889+ {
890+ "id": "4x1-inactive_output-udp_100ms",
891+ "topology": "4x1",
892+ "workload": "inactive_output",
893+ "transport": "udp_100ms",
894+ "roles": [
895+ "daemon",
896+ "client",
897+ "udp_peer"
898+ ]
899+ },
900+ {
901+ "id": "4x1-inactive_output-udp_loss_1pct",
902+ "topology": "4x1",
903+ "workload": "inactive_output",
904+ "transport": "udp_loss_1pct",
905+ "roles": [
906+ "daemon",
907+ "client",
908+ "udp_peer"
909+ ]
910+ },
911+ {
912+ "id": "4x1-interactive_flood-local",
913+ "topology": "4x1",
914+ "workload": "interactive_flood",
915+ "transport": "local",
916+ "roles": [
917+ "daemon",
918+ "client"
919+ ]
920+ },
921+ {
922+ "id": "4x1-interactive_flood-ssh_stdio",
923+ "topology": "4x1",
924+ "workload": "interactive_flood",
925+ "transport": "ssh_stdio",
926+ "roles": [
927+ "daemon",
928+ "client",
929+ "ssh_stdio_peer"
930+ ]
931+ },
932+ {
933+ "id": "4x1-interactive_flood-udp_baseline",
934+ "topology": "4x1",
935+ "workload": "interactive_flood",
936+ "transport": "udp_baseline",
937+ "roles": [
938+ "daemon",
939+ "client",
940+ "udp_peer"
941+ ]
942+ },
943+ {
944+ "id": "4x1-interactive_flood-udp_25ms",
945+ "topology": "4x1",
946+ "workload": "interactive_flood",
947+ "transport": "udp_25ms",
948+ "roles": [
949+ "daemon",
950+ "client",
951+ "udp_peer"
952+ ]
953+ },
954+ {
955+ "id": "4x1-interactive_flood-udp_100ms",
956+ "topology": "4x1",
957+ "workload": "interactive_flood",
958+ "transport": "udp_100ms",
959+ "roles": [
960+ "daemon",
961+ "client",
962+ "udp_peer"
963+ ]
964+ },
965+ {
966+ "id": "4x1-interactive_flood-udp_loss_1pct",
967+ "topology": "4x1",
968+ "workload": "interactive_flood",
969+ "transport": "udp_loss_1pct",
970+ "roles": [
971+ "daemon",
972+ "client",
973+ "udp_peer"
974+ ]
975+ },
976+ {
977+ "id": "4x1-copy_search-local",
978+ "topology": "4x1",
979+ "workload": "copy_search",
980+ "transport": "local",
981+ "roles": [
982+ "daemon",
983+ "client"
984+ ]
985+ },
986+ {
987+ "id": "4x1-copy_search-ssh_stdio",
988+ "topology": "4x1",
989+ "workload": "copy_search",
990+ "transport": "ssh_stdio",
991+ "roles": [
992+ "daemon",
993+ "client",
994+ "ssh_stdio_peer"
995+ ]
996+ },
997+ {
998+ "id": "4x1-copy_search-udp_baseline",
999+ "topology": "4x1",
1000+ "workload": "copy_search",
1001+ "transport": "udp_baseline",
1002+ "roles": [
1003+ "daemon",
1004+ "client",
1005+ "udp_peer"
1006+ ]
1007+ },
1008+ {
1009+ "id": "4x1-copy_search-udp_25ms",
1010+ "topology": "4x1",
1011+ "workload": "copy_search",
1012+ "transport": "udp_25ms",
1013+ "roles": [
1014+ "daemon",
1015+ "client",
1016+ "udp_peer"
1017+ ]
1018+ },
1019+ {
1020+ "id": "4x1-copy_search-udp_100ms",
1021+ "topology": "4x1",
1022+ "workload": "copy_search",
1023+ "transport": "udp_100ms",
1024+ "roles": [
1025+ "daemon",
1026+ "client",
1027+ "udp_peer"
1028+ ]
1029+ },
1030+ {
1031+ "id": "4x1-copy_search-udp_loss_1pct",
1032+ "topology": "4x1",
1033+ "workload": "copy_search",
1034+ "transport": "udp_loss_1pct",
1035+ "roles": [
1036+ "daemon",
1037+ "client",
1038+ "udp_peer"
1039+ ]
1040+ },
1041+ {
1042+ "id": "4x1-resize_sweep-local",
1043+ "topology": "4x1",
1044+ "workload": "resize_sweep",
1045+ "transport": "local",
1046+ "roles": [
1047+ "daemon",
1048+ "client"
1049+ ]
1050+ },
1051+ {
1052+ "id": "4x1-resize_sweep-ssh_stdio",
1053+ "topology": "4x1",
1054+ "workload": "resize_sweep",
1055+ "transport": "ssh_stdio",
1056+ "roles": [
1057+ "daemon",
1058+ "client",
1059+ "ssh_stdio_peer"
1060+ ]
1061+ },
1062+ {
1063+ "id": "4x1-resize_sweep-udp_baseline",
1064+ "topology": "4x1",
1065+ "workload": "resize_sweep",
1066+ "transport": "udp_baseline",
1067+ "roles": [
1068+ "daemon",
1069+ "client",
1070+ "udp_peer"
1071+ ]
1072+ },
1073+ {
1074+ "id": "4x1-resize_sweep-udp_25ms",
1075+ "topology": "4x1",
1076+ "workload": "resize_sweep",
1077+ "transport": "udp_25ms",
1078+ "roles": [
1079+ "daemon",
1080+ "client",
1081+ "udp_peer"
1082+ ]
1083+ },
1084+ {
1085+ "id": "4x1-resize_sweep-udp_100ms",
1086+ "topology": "4x1",
1087+ "workload": "resize_sweep",
1088+ "transport": "udp_100ms",
1089+ "roles": [
1090+ "daemon",
1091+ "client",
1092+ "udp_peer"
1093+ ]
1094+ },
1095+ {
1096+ "id": "4x1-resize_sweep-udp_loss_1pct",
1097+ "topology": "4x1",
1098+ "workload": "resize_sweep",
1099+ "transport": "udp_loss_1pct",
1100+ "roles": [
1101+ "daemon",
1102+ "client",
1103+ "udp_peer"
1104+ ]
1105+ },
1106+ {
1107+ "id": "4x1-snapshot_output_resize-local",
1108+ "topology": "4x1",
1109+ "workload": "snapshot_output_resize",
1110+ "transport": "local",
1111+ "roles": [
1112+ "daemon",
1113+ "client"
1114+ ]
1115+ },
1116+ {
1117+ "id": "4x1-snapshot_output_resize-ssh_stdio",
1118+ "topology": "4x1",
1119+ "workload": "snapshot_output_resize",
1120+ "transport": "ssh_stdio",
1121+ "roles": [
1122+ "daemon",
1123+ "client",
1124+ "ssh_stdio_peer"
1125+ ]
1126+ },
1127+ {
1128+ "id": "4x1-snapshot_output_resize-udp_baseline",
1129+ "topology": "4x1",
1130+ "workload": "snapshot_output_resize",
1131+ "transport": "udp_baseline",
1132+ "roles": [
1133+ "daemon",
1134+ "client",
1135+ "udp_peer"
1136+ ]
1137+ },
1138+ {
1139+ "id": "4x1-snapshot_output_resize-udp_25ms",
1140+ "topology": "4x1",
1141+ "workload": "snapshot_output_resize",
1142+ "transport": "udp_25ms",
1143+ "roles": [
1144+ "daemon",
1145+ "client",
1146+ "udp_peer"
1147+ ]
1148+ },
1149+ {
1150+ "id": "4x1-snapshot_output_resize-udp_100ms",
1151+ "topology": "4x1",
1152+ "workload": "snapshot_output_resize",
1153+ "transport": "udp_100ms",
1154+ "roles": [
1155+ "daemon",
1156+ "client",
1157+ "udp_peer"
1158+ ]
1159+ },
1160+ {
1161+ "id": "4x1-snapshot_output_resize-udp_loss_1pct",
1162+ "topology": "4x1",
1163+ "workload": "snapshot_output_resize",
1164+ "transport": "udp_loss_1pct",
1165+ "roles": [
1166+ "daemon",
1167+ "client",
1168+ "udp_peer"
1169+ ]
1170+ },
1171+ {
1172+ "id": "4x1-attach_restore_tab_switch-local",
1173+ "topology": "4x1",
1174+ "workload": "attach_restore_tab_switch",
1175+ "transport": "local",
1176+ "roles": [
1177+ "daemon",
1178+ "client"
1179+ ]
1180+ },
1181+ {
1182+ "id": "4x1-attach_restore_tab_switch-ssh_stdio",
1183+ "topology": "4x1",
1184+ "workload": "attach_restore_tab_switch",
1185+ "transport": "ssh_stdio",
1186+ "roles": [
1187+ "daemon",
1188+ "client",
1189+ "ssh_stdio_peer"
1190+ ]
1191+ },
1192+ {
1193+ "id": "4x1-attach_restore_tab_switch-udp_baseline",
1194+ "topology": "4x1",
1195+ "workload": "attach_restore_tab_switch",
1196+ "transport": "udp_baseline",
1197+ "roles": [
1198+ "daemon",
1199+ "client",
1200+ "udp_peer"
1201+ ]
1202+ },
1203+ {
1204+ "id": "4x1-attach_restore_tab_switch-udp_25ms",
1205+ "topology": "4x1",
1206+ "workload": "attach_restore_tab_switch",
1207+ "transport": "udp_25ms",
1208+ "roles": [
1209+ "daemon",
1210+ "client",
1211+ "udp_peer"
1212+ ]
1213+ },
1214+ {
1215+ "id": "4x1-attach_restore_tab_switch-udp_100ms",
1216+ "topology": "4x1",
1217+ "workload": "attach_restore_tab_switch",
1218+ "transport": "udp_100ms",
1219+ "roles": [
1220+ "daemon",
1221+ "client",
1222+ "udp_peer"
1223+ ]
1224+ },
1225+ {
1226+ "id": "4x1-attach_restore_tab_switch-udp_loss_1pct",
1227+ "topology": "4x1",
1228+ "workload": "attach_restore_tab_switch",
1229+ "transport": "udp_loss_1pct",
1230+ "roles": [
1231+ "daemon",
1232+ "client",
1233+ "udp_peer"
1234+ ]
1235+ },
1236+ {
1237+ "id": "4x4-idle-local",
1238+ "topology": "4x4",
1239+ "workload": "idle",
1240+ "transport": "local",
1241+ "roles": [
1242+ "daemon",
1243+ "client"
1244+ ]
1245+ },
1246+ {
1247+ "id": "4x4-idle-ssh_stdio",
1248+ "topology": "4x4",
1249+ "workload": "idle",
1250+ "transport": "ssh_stdio",
1251+ "roles": [
1252+ "daemon",
1253+ "client",
1254+ "ssh_stdio_peer"
1255+ ]
1256+ },
1257+ {
1258+ "id": "4x4-idle-udp_baseline",
1259+ "topology": "4x4",
1260+ "workload": "idle",
1261+ "transport": "udp_baseline",
1262+ "roles": [
1263+ "daemon",
1264+ "client",
1265+ "udp_peer"
1266+ ]
1267+ },
1268+ {
1269+ "id": "4x4-idle-udp_25ms",
1270+ "topology": "4x4",
1271+ "workload": "idle",
1272+ "transport": "udp_25ms",
1273+ "roles": [
1274+ "daemon",
1275+ "client",
1276+ "udp_peer"
1277+ ]
1278+ },
1279+ {
1280+ "id": "4x4-idle-udp_100ms",
1281+ "topology": "4x4",
1282+ "workload": "idle",
1283+ "transport": "udp_100ms",
1284+ "roles": [
1285+ "daemon",
1286+ "client",
1287+ "udp_peer"
1288+ ]
1289+ },
1290+ {
1291+ "id": "4x4-idle-udp_loss_1pct",
1292+ "topology": "4x4",
1293+ "workload": "idle",
1294+ "transport": "udp_loss_1pct",
1295+ "roles": [
1296+ "daemon",
1297+ "client",
1298+ "udp_peer"
1299+ ]
1300+ },
1301+ {
1302+ "id": "4x4-active_output-local",
1303+ "topology": "4x4",
1304+ "workload": "active_output",
1305+ "transport": "local",
1306+ "roles": [
1307+ "daemon",
1308+ "client"
1309+ ]
1310+ },
1311+ {
1312+ "id": "4x4-active_output-ssh_stdio",
1313+ "topology": "4x4",
1314+ "workload": "active_output",
1315+ "transport": "ssh_stdio",
1316+ "roles": [
1317+ "daemon",
1318+ "client",
1319+ "ssh_stdio_peer"
1320+ ]
1321+ },
1322+ {
1323+ "id": "4x4-active_output-udp_baseline",
1324+ "topology": "4x4",
1325+ "workload": "active_output",
1326+ "transport": "udp_baseline",
1327+ "roles": [
1328+ "daemon",
1329+ "client",
1330+ "udp_peer"
1331+ ]
1332+ },
1333+ {
1334+ "id": "4x4-active_output-udp_25ms",
1335+ "topology": "4x4",
1336+ "workload": "active_output",
1337+ "transport": "udp_25ms",
1338+ "roles": [
1339+ "daemon",
1340+ "client",
1341+ "udp_peer"
1342+ ]
1343+ },
1344+ {
1345+ "id": "4x4-active_output-udp_100ms",
1346+ "topology": "4x4",
1347+ "workload": "active_output",
1348+ "transport": "udp_100ms",
1349+ "roles": [
1350+ "daemon",
1351+ "client",
1352+ "udp_peer"
1353+ ]
1354+ },
1355+ {
1356+ "id": "4x4-active_output-udp_loss_1pct",
1357+ "topology": "4x4",
1358+ "workload": "active_output",
1359+ "transport": "udp_loss_1pct",
1360+ "roles": [
1361+ "daemon",
1362+ "client",
1363+ "udp_peer"
1364+ ]
1365+ },
1366+ {
1367+ "id": "4x4-all_output-local",
1368+ "topology": "4x4",
1369+ "workload": "all_output",
1370+ "transport": "local",
1371+ "roles": [
1372+ "daemon",
1373+ "client"
1374+ ]
1375+ },
1376+ {
1377+ "id": "4x4-all_output-ssh_stdio",
1378+ "topology": "4x4",
1379+ "workload": "all_output",
1380+ "transport": "ssh_stdio",
1381+ "roles": [
1382+ "daemon",
1383+ "client",
1384+ "ssh_stdio_peer"
1385+ ]
1386+ },
1387+ {
1388+ "id": "4x4-all_output-udp_baseline",
1389+ "topology": "4x4",
1390+ "workload": "all_output",
1391+ "transport": "udp_baseline",
1392+ "roles": [
1393+ "daemon",
1394+ "client",
1395+ "udp_peer"
1396+ ]
1397+ },
1398+ {
1399+ "id": "4x4-all_output-udp_25ms",
1400+ "topology": "4x4",
1401+ "workload": "all_output",
1402+ "transport": "udp_25ms",
1403+ "roles": [
1404+ "daemon",
1405+ "client",
1406+ "udp_peer"
1407+ ]
1408+ },
1409+ {
1410+ "id": "4x4-all_output-udp_100ms",
1411+ "topology": "4x4",
1412+ "workload": "all_output",
1413+ "transport": "udp_100ms",
1414+ "roles": [
1415+ "daemon",
1416+ "client",
1417+ "udp_peer"
1418+ ]
1419+ },
1420+ {
1421+ "id": "4x4-all_output-udp_loss_1pct",
1422+ "topology": "4x4",
1423+ "workload": "all_output",
1424+ "transport": "udp_loss_1pct",
1425+ "roles": [
1426+ "daemon",
1427+ "client",
1428+ "udp_peer"
1429+ ]
1430+ },
1431+ {
1432+ "id": "4x4-inactive_output-local",
1433+ "topology": "4x4",
1434+ "workload": "inactive_output",
1435+ "transport": "local",
1436+ "roles": [
1437+ "daemon",
1438+ "client"
1439+ ]
1440+ },
1441+ {
1442+ "id": "4x4-inactive_output-ssh_stdio",
1443+ "topology": "4x4",
1444+ "workload": "inactive_output",
1445+ "transport": "ssh_stdio",
1446+ "roles": [
1447+ "daemon",
1448+ "client",
1449+ "ssh_stdio_peer"
1450+ ]
1451+ },
1452+ {
1453+ "id": "4x4-inactive_output-udp_baseline",
1454+ "topology": "4x4",
1455+ "workload": "inactive_output",
1456+ "transport": "udp_baseline",
1457+ "roles": [
1458+ "daemon",
1459+ "client",
1460+ "udp_peer"
1461+ ]
1462+ },
1463+ {
1464+ "id": "4x4-inactive_output-udp_25ms",
1465+ "topology": "4x4",
1466+ "workload": "inactive_output",
1467+ "transport": "udp_25ms",
1468+ "roles": [
1469+ "daemon",
1470+ "client",
1471+ "udp_peer"
1472+ ]
1473+ },
1474+ {
1475+ "id": "4x4-inactive_output-udp_100ms",
1476+ "topology": "4x4",
1477+ "workload": "inactive_output",
1478+ "transport": "udp_100ms",
1479+ "roles": [
1480+ "daemon",
1481+ "client",
1482+ "udp_peer"
1483+ ]
1484+ },
1485+ {
1486+ "id": "4x4-inactive_output-udp_loss_1pct",
1487+ "topology": "4x4",
1488+ "workload": "inactive_output",
1489+ "transport": "udp_loss_1pct",
1490+ "roles": [
1491+ "daemon",
1492+ "client",
1493+ "udp_peer"
1494+ ]
1495+ },
1496+ {
1497+ "id": "4x4-interactive_flood-local",
1498+ "topology": "4x4",
1499+ "workload": "interactive_flood",
1500+ "transport": "local",
1501+ "roles": [
1502+ "daemon",
1503+ "client"
1504+ ]
1505+ },
1506+ {
1507+ "id": "4x4-interactive_flood-ssh_stdio",
1508+ "topology": "4x4",
1509+ "workload": "interactive_flood",
1510+ "transport": "ssh_stdio",
1511+ "roles": [
1512+ "daemon",
1513+ "client",
1514+ "ssh_stdio_peer"
1515+ ]
1516+ },
1517+ {
1518+ "id": "4x4-interactive_flood-udp_baseline",
1519+ "topology": "4x4",
1520+ "workload": "interactive_flood",
1521+ "transport": "udp_baseline",
1522+ "roles": [
1523+ "daemon",
1524+ "client",
1525+ "udp_peer"
1526+ ]
1527+ },
1528+ {
1529+ "id": "4x4-interactive_flood-udp_25ms",
1530+ "topology": "4x4",
1531+ "workload": "interactive_flood",
1532+ "transport": "udp_25ms",
1533+ "roles": [
1534+ "daemon",
1535+ "client",
1536+ "udp_peer"
1537+ ]
1538+ },
1539+ {
1540+ "id": "4x4-interactive_flood-udp_100ms",
1541+ "topology": "4x4",
1542+ "workload": "interactive_flood",
1543+ "transport": "udp_100ms",
1544+ "roles": [
1545+ "daemon",
1546+ "client",
1547+ "udp_peer"
1548+ ]
1549+ },
1550+ {
1551+ "id": "4x4-interactive_flood-udp_loss_1pct",
1552+ "topology": "4x4",
1553+ "workload": "interactive_flood",
1554+ "transport": "udp_loss_1pct",
1555+ "roles": [
1556+ "daemon",
1557+ "client",
1558+ "udp_peer"
1559+ ]
1560+ },
1561+ {
1562+ "id": "4x4-copy_search-local",
1563+ "topology": "4x4",
1564+ "workload": "copy_search",
1565+ "transport": "local",
1566+ "roles": [
1567+ "daemon",
1568+ "client"
1569+ ]
1570+ },
1571+ {
1572+ "id": "4x4-copy_search-ssh_stdio",
1573+ "topology": "4x4",
1574+ "workload": "copy_search",
1575+ "transport": "ssh_stdio",
1576+ "roles": [
1577+ "daemon",
1578+ "client",
1579+ "ssh_stdio_peer"
1580+ ]
1581+ },
1582+ {
1583+ "id": "4x4-copy_search-udp_baseline",
1584+ "topology": "4x4",
1585+ "workload": "copy_search",
1586+ "transport": "udp_baseline",
1587+ "roles": [
1588+ "daemon",
1589+ "client",
1590+ "udp_peer"
1591+ ]
1592+ },
1593+ {
1594+ "id": "4x4-copy_search-udp_25ms",
1595+ "topology": "4x4",
1596+ "workload": "copy_search",
1597+ "transport": "udp_25ms",
1598+ "roles": [
1599+ "daemon",
1600+ "client",
1601+ "udp_peer"
1602+ ]
1603+ },
1604+ {
1605+ "id": "4x4-copy_search-udp_100ms",
1606+ "topology": "4x4",
1607+ "workload": "copy_search",
1608+ "transport": "udp_100ms",
1609+ "roles": [
1610+ "daemon",
1611+ "client",
1612+ "udp_peer"
1613+ ]
1614+ },
1615+ {
1616+ "id": "4x4-copy_search-udp_loss_1pct",
1617+ "topology": "4x4",
1618+ "workload": "copy_search",
1619+ "transport": "udp_loss_1pct",
1620+ "roles": [
1621+ "daemon",
1622+ "client",
1623+ "udp_peer"
1624+ ]
1625+ },
1626+ {
1627+ "id": "4x4-resize_sweep-local",
1628+ "topology": "4x4",
1629+ "workload": "resize_sweep",
1630+ "transport": "local",
1631+ "roles": [
1632+ "daemon",
1633+ "client"
1634+ ]
1635+ },
1636+ {
1637+ "id": "4x4-resize_sweep-ssh_stdio",
1638+ "topology": "4x4",
1639+ "workload": "resize_sweep",
1640+ "transport": "ssh_stdio",
1641+ "roles": [
1642+ "daemon",
1643+ "client",
1644+ "ssh_stdio_peer"
1645+ ]
1646+ },
1647+ {
1648+ "id": "4x4-resize_sweep-udp_baseline",
1649+ "topology": "4x4",
1650+ "workload": "resize_sweep",
1651+ "transport": "udp_baseline",
1652+ "roles": [
1653+ "daemon",
1654+ "client",
1655+ "udp_peer"
1656+ ]
1657+ },
1658+ {
1659+ "id": "4x4-resize_sweep-udp_25ms",
1660+ "topology": "4x4",
1661+ "workload": "resize_sweep",
1662+ "transport": "udp_25ms",
1663+ "roles": [
1664+ "daemon",
1665+ "client",
1666+ "udp_peer"
1667+ ]
1668+ },
1669+ {
1670+ "id": "4x4-resize_sweep-udp_100ms",
1671+ "topology": "4x4",
1672+ "workload": "resize_sweep",
1673+ "transport": "udp_100ms",
1674+ "roles": [
1675+ "daemon",
1676+ "client",
1677+ "udp_peer"
1678+ ]
1679+ },
1680+ {
1681+ "id": "4x4-resize_sweep-udp_loss_1pct",
1682+ "topology": "4x4",
1683+ "workload": "resize_sweep",
1684+ "transport": "udp_loss_1pct",
1685+ "roles": [
1686+ "daemon",
1687+ "client",
1688+ "udp_peer"
1689+ ]
1690+ },
1691+ {
1692+ "id": "4x4-snapshot_output_resize-local",
1693+ "topology": "4x4",
1694+ "workload": "snapshot_output_resize",
1695+ "transport": "local",
1696+ "roles": [
1697+ "daemon",
1698+ "client"
1699+ ]
1700+ },
1701+ {
1702+ "id": "4x4-snapshot_output_resize-ssh_stdio",
1703+ "topology": "4x4",
1704+ "workload": "snapshot_output_resize",
1705+ "transport": "ssh_stdio",
1706+ "roles": [
1707+ "daemon",
1708+ "client",
1709+ "ssh_stdio_peer"
1710+ ]
1711+ },
1712+ {
1713+ "id": "4x4-snapshot_output_resize-udp_baseline",
1714+ "topology": "4x4",
1715+ "workload": "snapshot_output_resize",
1716+ "transport": "udp_baseline",
1717+ "roles": [
1718+ "daemon",
1719+ "client",
1720+ "udp_peer"
1721+ ]
1722+ },
1723+ {
1724+ "id": "4x4-snapshot_output_resize-udp_25ms",
1725+ "topology": "4x4",
1726+ "workload": "snapshot_output_resize",
1727+ "transport": "udp_25ms",
1728+ "roles": [
1729+ "daemon",
1730+ "client",
1731+ "udp_peer"
1732+ ]
1733+ },
1734+ {
1735+ "id": "4x4-snapshot_output_resize-udp_100ms",
1736+ "topology": "4x4",
1737+ "workload": "snapshot_output_resize",
1738+ "transport": "udp_100ms",
1739+ "roles": [
1740+ "daemon",
1741+ "client",
1742+ "udp_peer"
1743+ ]
1744+ },
1745+ {
1746+ "id": "4x4-snapshot_output_resize-udp_loss_1pct",
1747+ "topology": "4x4",
1748+ "workload": "snapshot_output_resize",
1749+ "transport": "udp_loss_1pct",
1750+ "roles": [
1751+ "daemon",
1752+ "client",
1753+ "udp_peer"
1754+ ]
1755+ },
1756+ {
1757+ "id": "4x4-attach_restore_tab_switch-local",
1758+ "topology": "4x4",
1759+ "workload": "attach_restore_tab_switch",
1760+ "transport": "local",
1761+ "roles": [
1762+ "daemon",
1763+ "client"
1764+ ]
1765+ },
1766+ {
1767+ "id": "4x4-attach_restore_tab_switch-ssh_stdio",
1768+ "topology": "4x4",
1769+ "workload": "attach_restore_tab_switch",
1770+ "transport": "ssh_stdio",
1771+ "roles": [
1772+ "daemon",
1773+ "client",
1774+ "ssh_stdio_peer"
1775+ ]
1776+ },
1777+ {
1778+ "id": "4x4-attach_restore_tab_switch-udp_baseline",
1779+ "topology": "4x4",
1780+ "workload": "attach_restore_tab_switch",
1781+ "transport": "udp_baseline",
1782+ "roles": [
1783+ "daemon",
1784+ "client",
1785+ "udp_peer"
1786+ ]
1787+ },
1788+ {
1789+ "id": "4x4-attach_restore_tab_switch-udp_25ms",
1790+ "topology": "4x4",
1791+ "workload": "attach_restore_tab_switch",
1792+ "transport": "udp_25ms",
1793+ "roles": [
1794+ "daemon",
1795+ "client",
1796+ "udp_peer"
1797+ ]
1798+ },
1799+ {
1800+ "id": "4x4-attach_restore_tab_switch-udp_100ms",
1801+ "topology": "4x4",
1802+ "workload": "attach_restore_tab_switch",
1803+ "transport": "udp_100ms",
1804+ "roles": [
1805+ "daemon",
1806+ "client",
1807+ "udp_peer"
1808+ ]
1809+ },
1810+ {
1811+ "id": "4x4-attach_restore_tab_switch-udp_loss_1pct",
1812+ "topology": "4x4",
1813+ "workload": "attach_restore_tab_switch",
1814+ "transport": "udp_loss_1pct",
1815+ "roles": [
1816+ "daemon",
1817+ "client",
1818+ "udp_peer"
1819+ ]
1820+ },
1821+ {
1822+ "id": "8x1-idle-local",
1823+ "topology": "8x1",
1824+ "workload": "idle",
1825+ "transport": "local",
1826+ "roles": [
1827+ "daemon",
1828+ "client"
1829+ ]
1830+ },
1831+ {
1832+ "id": "8x1-idle-ssh_stdio",
1833+ "topology": "8x1",
1834+ "workload": "idle",
1835+ "transport": "ssh_stdio",
1836+ "roles": [
1837+ "daemon",
1838+ "client",
1839+ "ssh_stdio_peer"
1840+ ]
1841+ },
1842+ {
1843+ "id": "8x1-idle-udp_baseline",
1844+ "topology": "8x1",
1845+ "workload": "idle",
1846+ "transport": "udp_baseline",
1847+ "roles": [
1848+ "daemon",
1849+ "client",
1850+ "udp_peer"
1851+ ]
1852+ },
1853+ {
1854+ "id": "8x1-idle-udp_25ms",
1855+ "topology": "8x1",
1856+ "workload": "idle",
1857+ "transport": "udp_25ms",
1858+ "roles": [
1859+ "daemon",
1860+ "client",
1861+ "udp_peer"
1862+ ]
1863+ },
1864+ {
1865+ "id": "8x1-idle-udp_100ms",
1866+ "topology": "8x1",
1867+ "workload": "idle",
1868+ "transport": "udp_100ms",
1869+ "roles": [
1870+ "daemon",
1871+ "client",
1872+ "udp_peer"
1873+ ]
1874+ },
1875+ {
1876+ "id": "8x1-idle-udp_loss_1pct",
1877+ "topology": "8x1",
1878+ "workload": "idle",
1879+ "transport": "udp_loss_1pct",
1880+ "roles": [
1881+ "daemon",
1882+ "client",
1883+ "udp_peer"
1884+ ]
1885+ },
1886+ {
1887+ "id": "8x1-active_output-local",
1888+ "topology": "8x1",
1889+ "workload": "active_output",
1890+ "transport": "local",
1891+ "roles": [
1892+ "daemon",
1893+ "client"
1894+ ]
1895+ },
1896+ {
1897+ "id": "8x1-active_output-ssh_stdio",
1898+ "topology": "8x1",
1899+ "workload": "active_output",
1900+ "transport": "ssh_stdio",
1901+ "roles": [
1902+ "daemon",
1903+ "client",
1904+ "ssh_stdio_peer"
1905+ ]
1906+ },
1907+ {
1908+ "id": "8x1-active_output-udp_baseline",
1909+ "topology": "8x1",
1910+ "workload": "active_output",
1911+ "transport": "udp_baseline",
1912+ "roles": [
1913+ "daemon",
1914+ "client",
1915+ "udp_peer"
1916+ ]
1917+ },
1918+ {
1919+ "id": "8x1-active_output-udp_25ms",
1920+ "topology": "8x1",
1921+ "workload": "active_output",
1922+ "transport": "udp_25ms",
1923+ "roles": [
1924+ "daemon",
1925+ "client",
1926+ "udp_peer"
1927+ ]
1928+ },
1929+ {
1930+ "id": "8x1-active_output-udp_100ms",
1931+ "topology": "8x1",
1932+ "workload": "active_output",
1933+ "transport": "udp_100ms",
1934+ "roles": [
1935+ "daemon",
1936+ "client",
1937+ "udp_peer"
1938+ ]
1939+ },
1940+ {
1941+ "id": "8x1-active_output-udp_loss_1pct",
1942+ "topology": "8x1",
1943+ "workload": "active_output",
1944+ "transport": "udp_loss_1pct",
1945+ "roles": [
1946+ "daemon",
1947+ "client",
1948+ "udp_peer"
1949+ ]
1950+ },
1951+ {
1952+ "id": "8x1-all_output-local",
1953+ "topology": "8x1",
1954+ "workload": "all_output",
1955+ "transport": "local",
1956+ "roles": [
1957+ "daemon",
1958+ "client"
1959+ ]
1960+ },
1961+ {
1962+ "id": "8x1-all_output-ssh_stdio",
1963+ "topology": "8x1",
1964+ "workload": "all_output",
1965+ "transport": "ssh_stdio",
1966+ "roles": [
1967+ "daemon",
1968+ "client",
1969+ "ssh_stdio_peer"
1970+ ]
1971+ },
1972+ {
1973+ "id": "8x1-all_output-udp_baseline",
1974+ "topology": "8x1",
1975+ "workload": "all_output",
1976+ "transport": "udp_baseline",
1977+ "roles": [
1978+ "daemon",
1979+ "client",
1980+ "udp_peer"
1981+ ]
1982+ },
1983+ {
1984+ "id": "8x1-all_output-udp_25ms",
1985+ "topology": "8x1",
1986+ "workload": "all_output",
1987+ "transport": "udp_25ms",
1988+ "roles": [
1989+ "daemon",
1990+ "client",
1991+ "udp_peer"
1992+ ]
1993+ },
1994+ {
1995+ "id": "8x1-all_output-udp_100ms",
1996+ "topology": "8x1",
1997+ "workload": "all_output",
1998+ "transport": "udp_100ms",
1999+ "roles": [
2000+ "daemon",
2001+ "client",
2002+ "udp_peer"
2003+ ]
2004+ },
2005+ {
2006+ "id": "8x1-all_output-udp_loss_1pct",
2007+ "topology": "8x1",
2008+ "workload": "all_output",
2009+ "transport": "udp_loss_1pct",
2010+ "roles": [
2011+ "daemon",
2012+ "client",
2013+ "udp_peer"
2014+ ]
2015+ },
2016+ {
2017+ "id": "8x1-inactive_output-local",
2018+ "topology": "8x1",
2019+ "workload": "inactive_output",
2020+ "transport": "local",
2021+ "roles": [
2022+ "daemon",
2023+ "client"
2024+ ]
2025+ },
2026+ {
2027+ "id": "8x1-inactive_output-ssh_stdio",
2028+ "topology": "8x1",
2029+ "workload": "inactive_output",
2030+ "transport": "ssh_stdio",
2031+ "roles": [
2032+ "daemon",
2033+ "client",
2034+ "ssh_stdio_peer"
2035+ ]
2036+ },
2037+ {
2038+ "id": "8x1-inactive_output-udp_baseline",
2039+ "topology": "8x1",
2040+ "workload": "inactive_output",
2041+ "transport": "udp_baseline",
2042+ "roles": [
2043+ "daemon",
2044+ "client",
2045+ "udp_peer"
2046+ ]
2047+ },
2048+ {
2049+ "id": "8x1-inactive_output-udp_25ms",
2050+ "topology": "8x1",
2051+ "workload": "inactive_output",
2052+ "transport": "udp_25ms",
2053+ "roles": [
2054+ "daemon",
2055+ "client",
2056+ "udp_peer"
2057+ ]
2058+ },
2059+ {
2060+ "id": "8x1-inactive_output-udp_100ms",
2061+ "topology": "8x1",
2062+ "workload": "inactive_output",
2063+ "transport": "udp_100ms",
2064+ "roles": [
2065+ "daemon",
2066+ "client",
2067+ "udp_peer"
2068+ ]
2069+ },
2070+ {
2071+ "id": "8x1-inactive_output-udp_loss_1pct",
2072+ "topology": "8x1",
2073+ "workload": "inactive_output",
2074+ "transport": "udp_loss_1pct",
2075+ "roles": [
2076+ "daemon",
2077+ "client",
2078+ "udp_peer"
2079+ ]
2080+ },
2081+ {
2082+ "id": "8x1-interactive_flood-local",
2083+ "topology": "8x1",
2084+ "workload": "interactive_flood",
2085+ "transport": "local",
2086+ "roles": [
2087+ "daemon",
2088+ "client"
2089+ ]
2090+ },
2091+ {
2092+ "id": "8x1-interactive_flood-ssh_stdio",
2093+ "topology": "8x1",
2094+ "workload": "interactive_flood",
2095+ "transport": "ssh_stdio",
2096+ "roles": [
2097+ "daemon",
2098+ "client",
2099+ "ssh_stdio_peer"
2100+ ]
2101+ },
2102+ {
2103+ "id": "8x1-interactive_flood-udp_baseline",
2104+ "topology": "8x1",
2105+ "workload": "interactive_flood",
2106+ "transport": "udp_baseline",
2107+ "roles": [
2108+ "daemon",
2109+ "client",
2110+ "udp_peer"
2111+ ]
2112+ },
2113+ {
2114+ "id": "8x1-interactive_flood-udp_25ms",
2115+ "topology": "8x1",
2116+ "workload": "interactive_flood",
2117+ "transport": "udp_25ms",
2118+ "roles": [
2119+ "daemon",
2120+ "client",
2121+ "udp_peer"
2122+ ]
2123+ },
2124+ {
2125+ "id": "8x1-interactive_flood-udp_100ms",
2126+ "topology": "8x1",
2127+ "workload": "interactive_flood",
2128+ "transport": "udp_100ms",
2129+ "roles": [
2130+ "daemon",
2131+ "client",
2132+ "udp_peer"
2133+ ]
2134+ },
2135+ {
2136+ "id": "8x1-interactive_flood-udp_loss_1pct",
2137+ "topology": "8x1",
2138+ "workload": "interactive_flood",
2139+ "transport": "udp_loss_1pct",
2140+ "roles": [
2141+ "daemon",
2142+ "client",
2143+ "udp_peer"
2144+ ]
2145+ },
2146+ {
2147+ "id": "8x1-copy_search-local",
2148+ "topology": "8x1",
2149+ "workload": "copy_search",
2150+ "transport": "local",
2151+ "roles": [
2152+ "daemon",
2153+ "client"
2154+ ]
2155+ },
2156+ {
2157+ "id": "8x1-copy_search-ssh_stdio",
2158+ "topology": "8x1",
2159+ "workload": "copy_search",
2160+ "transport": "ssh_stdio",
2161+ "roles": [
2162+ "daemon",
2163+ "client",
2164+ "ssh_stdio_peer"
2165+ ]
2166+ },
2167+ {
2168+ "id": "8x1-copy_search-udp_baseline",
2169+ "topology": "8x1",
2170+ "workload": "copy_search",
2171+ "transport": "udp_baseline",
2172+ "roles": [
2173+ "daemon",
2174+ "client",
2175+ "udp_peer"
2176+ ]
2177+ },
2178+ {
2179+ "id": "8x1-copy_search-udp_25ms",
2180+ "topology": "8x1",
2181+ "workload": "copy_search",
2182+ "transport": "udp_25ms",
2183+ "roles": [
2184+ "daemon",
2185+ "client",
2186+ "udp_peer"
2187+ ]
2188+ },
2189+ {
2190+ "id": "8x1-copy_search-udp_100ms",
2191+ "topology": "8x1",
2192+ "workload": "copy_search",
2193+ "transport": "udp_100ms",
2194+ "roles": [
2195+ "daemon",
2196+ "client",
2197+ "udp_peer"
2198+ ]
2199+ },
2200+ {
2201+ "id": "8x1-copy_search-udp_loss_1pct",
2202+ "topology": "8x1",
2203+ "workload": "copy_search",
2204+ "transport": "udp_loss_1pct",
2205+ "roles": [
2206+ "daemon",
2207+ "client",
2208+ "udp_peer"
2209+ ]
2210+ },
2211+ {
2212+ "id": "8x1-resize_sweep-local",
2213+ "topology": "8x1",
2214+ "workload": "resize_sweep",
2215+ "transport": "local",
2216+ "roles": [
2217+ "daemon",
2218+ "client"
2219+ ]
2220+ },
2221+ {
2222+ "id": "8x1-resize_sweep-ssh_stdio",
2223+ "topology": "8x1",
2224+ "workload": "resize_sweep",
2225+ "transport": "ssh_stdio",
2226+ "roles": [
2227+ "daemon",
2228+ "client",
2229+ "ssh_stdio_peer"
2230+ ]
2231+ },
2232+ {
2233+ "id": "8x1-resize_sweep-udp_baseline",
2234+ "topology": "8x1",
2235+ "workload": "resize_sweep",
2236+ "transport": "udp_baseline",
2237+ "roles": [
2238+ "daemon",
2239+ "client",
2240+ "udp_peer"
2241+ ]
2242+ },
2243+ {
2244+ "id": "8x1-resize_sweep-udp_25ms",
2245+ "topology": "8x1",
2246+ "workload": "resize_sweep",
2247+ "transport": "udp_25ms",
2248+ "roles": [
2249+ "daemon",
2250+ "client",
2251+ "udp_peer"
2252+ ]
2253+ },
2254+ {
2255+ "id": "8x1-resize_sweep-udp_100ms",
2256+ "topology": "8x1",
2257+ "workload": "resize_sweep",
2258+ "transport": "udp_100ms",
2259+ "roles": [
2260+ "daemon",
2261+ "client",
2262+ "udp_peer"
2263+ ]
2264+ },
2265+ {
2266+ "id": "8x1-resize_sweep-udp_loss_1pct",
2267+ "topology": "8x1",
2268+ "workload": "resize_sweep",
2269+ "transport": "udp_loss_1pct",
2270+ "roles": [
2271+ "daemon",
2272+ "client",
2273+ "udp_peer"
2274+ ]
2275+ },
2276+ {
2277+ "id": "8x1-snapshot_output_resize-local",
2278+ "topology": "8x1",
2279+ "workload": "snapshot_output_resize",
2280+ "transport": "local",
2281+ "roles": [
2282+ "daemon",
2283+ "client"
2284+ ]
2285+ },
2286+ {
2287+ "id": "8x1-snapshot_output_resize-ssh_stdio",
2288+ "topology": "8x1",
2289+ "workload": "snapshot_output_resize",
2290+ "transport": "ssh_stdio",
2291+ "roles": [
2292+ "daemon",
2293+ "client",
2294+ "ssh_stdio_peer"
2295+ ]
2296+ },
2297+ {
2298+ "id": "8x1-snapshot_output_resize-udp_baseline",
2299+ "topology": "8x1",
2300+ "workload": "snapshot_output_resize",
2301+ "transport": "udp_baseline",
2302+ "roles": [
2303+ "daemon",
2304+ "client",
2305+ "udp_peer"
2306+ ]
2307+ },
2308+ {
2309+ "id": "8x1-snapshot_output_resize-udp_25ms",
2310+ "topology": "8x1",
2311+ "workload": "snapshot_output_resize",
2312+ "transport": "udp_25ms",
2313+ "roles": [
2314+ "daemon",
2315+ "client",
2316+ "udp_peer"
2317+ ]
2318+ },
2319+ {
2320+ "id": "8x1-snapshot_output_resize-udp_100ms",
2321+ "topology": "8x1",
2322+ "workload": "snapshot_output_resize",
2323+ "transport": "udp_100ms",
2324+ "roles": [
2325+ "daemon",
2326+ "client",
2327+ "udp_peer"
2328+ ]
2329+ },
2330+ {
2331+ "id": "8x1-snapshot_output_resize-udp_loss_1pct",
2332+ "topology": "8x1",
2333+ "workload": "snapshot_output_resize",
2334+ "transport": "udp_loss_1pct",
2335+ "roles": [
2336+ "daemon",
2337+ "client",
2338+ "udp_peer"
2339+ ]
2340+ },
2341+ {
2342+ "id": "8x1-attach_restore_tab_switch-local",
2343+ "topology": "8x1",
2344+ "workload": "attach_restore_tab_switch",
2345+ "transport": "local",
2346+ "roles": [
2347+ "daemon",
2348+ "client"
2349+ ]
2350+ },
2351+ {
2352+ "id": "8x1-attach_restore_tab_switch-ssh_stdio",
2353+ "topology": "8x1",
2354+ "workload": "attach_restore_tab_switch",
2355+ "transport": "ssh_stdio",
2356+ "roles": [
2357+ "daemon",
2358+ "client",
2359+ "ssh_stdio_peer"
2360+ ]
2361+ },
2362+ {
2363+ "id": "8x1-attach_restore_tab_switch-udp_baseline",
2364+ "topology": "8x1",
2365+ "workload": "attach_restore_tab_switch",
2366+ "transport": "udp_baseline",
2367+ "roles": [
2368+ "daemon",
2369+ "client",
2370+ "udp_peer"
2371+ ]
2372+ },
2373+ {
2374+ "id": "8x1-attach_restore_tab_switch-udp_25ms",
2375+ "topology": "8x1",
2376+ "workload": "attach_restore_tab_switch",
2377+ "transport": "udp_25ms",
2378+ "roles": [
2379+ "daemon",
2380+ "client",
2381+ "udp_peer"
2382+ ]
2383+ },
2384+ {
2385+ "id": "8x1-attach_restore_tab_switch-udp_100ms",
2386+ "topology": "8x1",
2387+ "workload": "attach_restore_tab_switch",
2388+ "transport": "udp_100ms",
2389+ "roles": [
2390+ "daemon",
2391+ "client",
2392+ "udp_peer"
2393+ ]
2394+ },
2395+ {
2396+ "id": "8x1-attach_restore_tab_switch-udp_loss_1pct",
2397+ "topology": "8x1",
2398+ "workload": "attach_restore_tab_switch",
2399+ "transport": "udp_loss_1pct",
2400+ "roles": [
2401+ "daemon",
2402+ "client",
2403+ "udp_peer"
2404+ ]
2405+ }
2406+ ]
2407+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close