fix(obs): route remote harness transports
- commit
- 1cb3d81ba404fa5398492b7bbad551c315ee2089
- author
- brice · · 2026-07-12 23:03 UTC
- parent
- 182a466
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +816 -46
M cmd/vev-perf-harness/main.go +289 -34
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | "os/exec" | |
| 15 | 15 | "path/filepath" | |
| 16 | 16 | "sort" | |
| 17 | + | "strconv" | |
| 17 | 18 | "strings" | |
| 18 | 19 | "sync" | |
| 19 | 20 | "syscall" | |
| @@ -133,13 +134,57 @@ type launchedProcess interface { | |||
| 133 | 134 | Measure([]byte, func() error, func() error) error | |
| 134 | 135 | Close() error | |
| 135 | 136 | } | |
| 137 | + | ||
| 138 | + | // roleCommand contains only argv accepted by app.parseArgs. Transport routing | |
| 139 | + | // metadata configures the harness's public-command seam; it is never passed as | |
| 140 | + | // an unrecognised vev flag. | |
| 141 | + | type roleCommand struct { | |
| 142 | + | Args []string | |
| 143 | + | Transport transport | |
| 144 | + | } | |
| 145 | + | ||
| 136 | 146 | type launcher interface { | |
| 137 | - | Launch(processMapping, []string) (launchedProcess, error) | |
| 147 | + | Launch(processMapping, roleCommand) (launchedProcess, error) | |
| 138 | 148 | } | |
| 139 | 149 | ||
| 140 | 150 | // cliLauncher is deliberately limited to executing the supplied public vev | |
| 141 | 151 | // binary. Scenario orchestration never reaches into an in-process daemon. | |
| 142 | - | type cliLauncher struct{ bin string } | |
| 152 | + | type cliLauncher struct { | |
| 153 | + | bin string | |
| 154 | + | mu sync.Mutex | |
| 155 | + | peers map[string]peerRoute | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | type peerRoute struct { | |
| 159 | + | mapping processMapping | |
| 160 | + | command roleCommand | |
| 161 | + | pidPath string | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | type preparedPeer struct{ route peerRoute } | |
| 165 | + | ||
| 166 | + | func (p preparedPeer) Warmup([]byte) error { return nil } | |
| 167 | + | func (p preparedPeer) Measure([]byte, func() error, func() error) error { return nil } | |
| 168 | + | func (p preparedPeer) Close() error { | |
| 169 | + | if p.route.pidPath == "" { | |
| 170 | + | return nil | |
| 171 | + | } | |
| 172 | + | b, err := os.ReadFile(p.route.pidPath) | |
| 173 | + | if errors.Is(err, os.ErrNotExist) { | |
| 174 | + | return nil | |
| 175 | + | } | |
| 176 | + | if err != nil { | |
| 177 | + | return err | |
| 178 | + | } | |
| 179 | + | pid, err := strconv.Atoi(strings.TrimSpace(string(b))) | |
| 180 | + | if err != nil || pid <= 0 { | |
| 181 | + | return fmt.Errorf("invalid owned peer pid: %q", b) | |
| 182 | + | } | |
| 183 | + | if err := syscall.Kill(pid, syscall.SIGTERM); err != nil && !errors.Is(err, syscall.ESRCH) { | |
| 184 | + | return err | |
| 185 | + | } | |
| 186 | + | return nil | |
| 187 | + | } | |
| 143 | 188 | ||
| 144 | 189 | type terminalOutput interface { | |
| 145 | 190 | io.Writer | |
| @@ -157,8 +202,8 @@ type cliProcess struct { | |||
| 157 | 202 | waitErr chan error | |
| 158 | 203 | } | |
| 159 | 204 | ||
| 160 | - | func (l cliLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | |
| 161 | - | if len(args) == 0 { | |
| 205 | + | func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) { | |
| 206 | + | if len(role.Args) == 0 { | |
| 162 | 207 | return nil, errors.New("public CLI role has no arguments") | |
| 163 | 208 | } | |
| 164 | 209 | if info, err := os.Stat(l.bin); err != nil { | |
| @@ -166,14 +211,30 @@ func (l cliLauncher) Launch(m processMapping, args []string) (launchedProcess, e | |||
| 166 | 211 | } else if info.IsDir() { | |
| 167 | 212 | return nil, fmt.Errorf("public CLI binary %q is a directory", l.bin) | |
| 168 | 213 | } | |
| 169 | - | cmd := exec.Command(l.bin, args...) | |
| 170 | - | cmd.Env = append(withoutEnv(os.Environ(), "VEV"), "VEV_PERF_TRACE="+m.TracePath, "VEV_PERF_PROCESS_ID="+m.ProcessID, | |
| 171 | - | "XDG_RUNTIME_DIR="+filepath.Join(filepath.Dir(m.TracePath), "runtime"), | |
| 172 | - | "XDG_STATE_HOME="+filepath.Join(filepath.Dir(m.TracePath), "state"), "TERM=xterm-256color") | |
| 173 | - | if err := safedir.EnsurePrivate(filepath.Join(filepath.Dir(m.TracePath), "runtime")); err != nil { | |
| 214 | + | if m.Role == "ssh_stdio_peer" || m.Role == "udp_peer" { | |
| 215 | + | return l.preparePeer(m, role) | |
| 216 | + | } | |
| 217 | + | cmd := exec.Command(l.bin, role.Args...) | |
| 218 | + | runDir := filepath.Dir(m.TracePath) | |
| 219 | + | cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_REMOTE_TRANSPORT"), "VEV_PERF_TRACE="+m.TracePath, "VEV_PERF_PROCESS_ID="+m.ProcessID, | |
| 220 | + | "XDG_RUNTIME_DIR="+filepath.Join(runDir, "runtime"), | |
| 221 | + | "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color") | |
| 222 | + | if m.Role == "client" { | |
| 223 | + | l.mu.Lock() | |
| 224 | + | peer, routed := l.peers[runDir] | |
| 225 | + | l.mu.Unlock() | |
| 226 | + | if routed { | |
| 227 | + | mode, err := remoteMode(peer.command.Transport) | |
| 228 | + | if err != nil { | |
| 229 | + | return nil, err | |
| 230 | + | } | |
| 231 | + | cmd.Env = append(cmd.Env, "PATH="+runDir+":"+os.Getenv("PATH"), "VEV_REMOTE_TRANSPORT="+mode) | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil { | |
| 174 | 235 | return nil, err | |
| 175 | 236 | } | |
| 176 | - | if err := safedir.EnsurePrivate(filepath.Join(filepath.Dir(m.TracePath), "state")); err != nil { | |
| 237 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil { | |
| 177 | 238 | return nil, err | |
| 178 | 239 | } | |
| 179 | 240 | p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1)} | |
| @@ -216,6 +277,82 @@ func (l cliLauncher) Launch(m processMapping, args []string) (launchedProcess, e | |||
| 216 | 277 | return p, nil | |
| 217 | 278 | } | |
| 218 | 279 | ||
| 280 | + | // preparePeer installs a per-run ssh command seam. The ordinary public client | |
| 281 | + | // still executes its documented remote attach command; its ssh child invokes | |
| 282 | + | // only the parsed public _stdio or _udp-proxy command. This makes the peer | |
| 283 | + | // carrying client traffic the declared, exclusively traced role rather than a | |
| 284 | + | // separately launched but unused process. | |
| 285 | + | func (l *cliLauncher) preparePeer(m processMapping, role roleCommand) (launchedProcess, error) { | |
| 286 | + | runDir := filepath.Dir(m.TracePath) | |
| 287 | + | if err := safedir.EnsurePrivate(runDir); err != nil { | |
| 288 | + | return nil, err | |
| 289 | + | } | |
| 290 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil { | |
| 291 | + | return nil, err | |
| 292 | + | } | |
| 293 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil { | |
| 294 | + | return nil, err | |
| 295 | + | } | |
| 296 | + | if len(role.Args) != 2 || (role.Args[0] != "_stdio" && role.Args[0] != "_udp-proxy") { | |
| 297 | + | return nil, fmt.Errorf("unsupported public peer command %q", role.Args) | |
| 298 | + | } | |
| 299 | + | route := peerRoute{mapping: m, command: role} | |
| 300 | + | if m.Role == "udp_peer" { | |
| 301 | + | route.pidPath = filepath.Join(runDir, "udp-peer.pid") | |
| 302 | + | } | |
| 303 | + | shim := filepath.Join(runDir, "ssh") | |
| 304 | + | ready := filepath.Join(runDir, "udp-peer.ready") | |
| 305 | + | stderr := filepath.Join(runDir, "udp-peer.stderr") | |
| 306 | + | // The bootstrap protocol requires the SSH command to exit after forwarding | |
| 307 | + | // the readiness line. The long-lived proxy is owned by pidPath and receives | |
| 308 | + | // the role's unique trace identity, never the bootstrap/client identity. | |
| 309 | + | body := fmt.Sprintf(`#!/bin/sh | |
| 310 | + | set -eu | |
| 311 | + | case "$*" in | |
| 312 | + | *"_stdio"*) | |
| 313 | + | exec env VEV_PERF_TRACE=%[1]q VEV_PERF_PROCESS_ID=%[2]q XDG_RUNTIME_DIR=%[3]q XDG_STATE_HOME=%[4]q TERM=xterm-256color %[5]q _stdio %[6]q | |
| 314 | + | ;; | |
| 315 | + | *"_udp-bootstrap"*) | |
| 316 | + | rm -f %[7]q %[8]q | |
| 317 | + | env VEV_PERF_TRACE=%[1]q VEV_PERF_PROCESS_ID=%[2]q XDG_RUNTIME_DIR=%[3]q XDG_STATE_HOME=%[4]q TERM=xterm-256color VEV_PERF_UDP_RTT_MS=%[9]q VEV_PERF_UDP_LOSS_PERCENT=%[10]q %[5]q _udp-proxy %[6]q >%[7]q 2>%[8]q & | |
| 318 | + | peer=$! | |
| 319 | + | printf '%%s\\n' "$peer" > %[11]q | |
| 320 | + | i=0 | |
| 321 | + | while [ ! -s %[7]q ] && [ "$i" -lt 1000 ]; do sleep 0.01; i=$((i+1)); done | |
| 322 | + | test -s %[7]q | |
| 323 | + | head -n 1 %[7]q | |
| 324 | + | exit 0 | |
| 325 | + | ;; | |
| 326 | + | *) echo 'vev harness ssh seam rejected non-vev command' >&2; exit 64 ;; | |
| 327 | + | esac | |
| 328 | + | `, m.TracePath, m.ProcessID, filepath.Join(runDir, "runtime"), filepath.Join(runDir, "state"), l.bin, role.Args[1], ready, stderr, strconv.Itoa(role.Transport.RTTMS), strconv.Itoa(role.Transport.LossPercent), route.pidPath) | |
| 329 | + | if err := os.WriteFile(shim, []byte(body), 0o700); err != nil { | |
| 330 | + | return nil, err | |
| 331 | + | } | |
| 332 | + | l.mu.Lock() | |
| 333 | + | if l.peers == nil { | |
| 334 | + | l.peers = make(map[string]peerRoute) | |
| 335 | + | } | |
| 336 | + | if _, exists := l.peers[runDir]; exists { | |
| 337 | + | l.mu.Unlock() | |
| 338 | + | return nil, fmt.Errorf("duplicate transport peer for %s", runDir) | |
| 339 | + | } | |
| 340 | + | l.peers[runDir] = route | |
| 341 | + | l.mu.Unlock() | |
| 342 | + | return preparedPeer{route: route}, nil | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func remoteMode(t transport) (string, error) { | |
| 346 | + | switch t.Kind { | |
| 347 | + | case "ssh_stdio": | |
| 348 | + | return "stdio", nil | |
| 349 | + | case "udp": | |
| 350 | + | return "udp", nil | |
| 351 | + | default: | |
| 352 | + | return "", fmt.Errorf("transport %q cannot route through a remote peer", t.ID) | |
| 353 | + | } | |
| 354 | + | } | |
| 355 | + | ||
| 219 | 356 | func withoutEnv(env []string, names ...string) []string { | |
| 220 | 357 | remove := make(map[string]bool, len(names)) | |
| 221 | 358 | for _, name := range names { | |
| @@ -320,7 +457,9 @@ func (p *cliProcess) Close() error { | |||
| 320 | 457 | close(p.done) | |
| 321 | 458 | } | |
| 322 | 459 | if p.cmd != nil && p.cmd.Process != nil { | |
| 323 | - | _ = p.cmd.Process.Signal(syscall.SIGTERM) | |
| 460 | + | // The client is a session/process-group leader; terminate its ssh | |
| 461 | + | // seam/_stdio descendants as well as the terminal client itself. | |
| 462 | + | _ = syscall.Kill(-p.cmd.Process.Pid, syscall.SIGTERM) | |
| 324 | 463 | } | |
| 325 | 464 | if p.pty != nil { | |
| 326 | 465 | _ = p.pty.Close() | |
| @@ -512,9 +651,12 @@ func validateManifest(m manifest) error { | |||
| 512 | 651 | if t.ID == "" { | |
| 513 | 652 | return errors.New("transport missing id") | |
| 514 | 653 | } | |
| 654 | + | if err := validateTransportFixture(t); err != nil { | |
| 655 | + | return err | |
| 656 | + | } | |
| 515 | 657 | trans[t.ID] = true | |
| 516 | 658 | } | |
| 517 | - | for _, v := range []string{"local", "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_1pct"} { | |
| 659 | + | for _, v := range []string{"local", "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_0pct", "udp_loss_1pct"} { | |
| 518 | 660 | if !trans[v] { | |
| 519 | 661 | return fmt.Errorf("missing canonical transport %s", v) | |
| 520 | 662 | } | |
| @@ -535,6 +677,9 @@ func validateManifest(m manifest) error { | |||
| 535 | 677 | if s.InapplicableReason != "" && len(s.Roles) != 0 { | |
| 536 | 678 | return fmt.Errorf("inapplicable scenario %s must not launch roles", s.ID) | |
| 537 | 679 | } | |
| 680 | + | if s.InapplicableReason == "" && !equalRoleSet(s.Roles, requiredRoles(s.Transport)) { | |
| 681 | + | return fmt.Errorf("scenario %s roles do not route transport %s", s.ID, s.Transport) | |
| 682 | + | } | |
| 538 | 683 | covered[s.Topology+"\x00"+s.Workload+"\x00"+s.Transport] = true | |
| 539 | 684 | } | |
| 540 | 685 | for topologyID := range tops { | |
| @@ -549,6 +694,50 @@ func validateManifest(m manifest) error { | |||
| 549 | 694 | } | |
| 550 | 695 | return nil | |
| 551 | 696 | } | |
| 697 | + | func requiredRoles(transportID string) []string { | |
| 698 | + | switch transportID { | |
| 699 | + | case "local": | |
| 700 | + | return []string{"daemon", "client"} | |
| 701 | + | case "ssh_stdio": | |
| 702 | + | return []string{"daemon", "client", "ssh_stdio_peer"} | |
| 703 | + | default: | |
| 704 | + | return []string{"daemon", "client", "udp_peer"} | |
| 705 | + | } | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | func equalRoleSet(got, want []string) bool { | |
| 709 | + | if len(got) != len(want) { | |
| 710 | + | return false | |
| 711 | + | } | |
| 712 | + | seen := make(map[string]bool, len(got)) | |
| 713 | + | for _, role := range got { | |
| 714 | + | seen[role] = true | |
| 715 | + | } | |
| 716 | + | for _, role := range want { | |
| 717 | + | if !seen[role] { | |
| 718 | + | return false | |
| 719 | + | } | |
| 720 | + | } | |
| 721 | + | return true | |
| 722 | + | } | |
| 723 | + | ||
| 724 | + | func validateTransportFixture(t transport) error { | |
| 725 | + | want := map[string]transport{ | |
| 726 | + | "local": {ID: "local", Kind: "local"}, | |
| 727 | + | "ssh_stdio": {ID: "ssh_stdio", Kind: "ssh_stdio"}, | |
| 728 | + | "udp_baseline": {ID: "udp_baseline", Kind: "udp"}, | |
| 729 | + | "udp_25ms": {ID: "udp_25ms", Kind: "udp", RTTMS: 25}, | |
| 730 | + | "udp_100ms": {ID: "udp_100ms", Kind: "udp", RTTMS: 100}, | |
| 731 | + | "udp_loss_0pct": {ID: "udp_loss_0pct", Kind: "udp", LossPercent: 0}, | |
| 732 | + | "udp_loss_1pct": {ID: "udp_loss_1pct", Kind: "udp", LossPercent: 1}, | |
| 733 | + | } | |
| 734 | + | w, ok := want[t.ID] | |
| 735 | + | if !ok || t.Kind != w.Kind || t.RTTMS != w.RTTMS || t.LossPercent != w.LossPercent { | |
| 736 | + | return fmt.Errorf("invalid canonical transport fixture %+v", t) | |
| 737 | + | } | |
| 738 | + | return nil | |
| 739 | + | } | |
| 740 | + | ||
| 552 | 741 | func set(v []string) map[string]bool { | |
| 553 | 742 | r := map[string]bool{} | |
| 554 | 743 | for _, x := range v { | |
| @@ -556,7 +745,7 @@ func set(v []string) map[string]bool { | |||
| 556 | 745 | } | |
| 557 | 746 | return r | |
| 558 | 747 | } | |
| 559 | - | func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writer) (res runResult, err error) { | |
| 748 | + | func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Writer) (res runResult, err error) { | |
| 560 | 749 | dir := filepath.Join(o.out, fmt.Sprintf("%s-run-%03d", safeName(s.ID), run)) | |
| 561 | 750 | if err = h.mkdir(dir); err != nil { | |
| 562 | 751 | return res, err | |
| @@ -586,28 +775,38 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||
| 586 | 775 | return res, err | |
| 587 | 776 | } | |
| 588 | 777 | if h.launcher == nil { | |
| 589 | - | h.launcher = cliLauncher{o.vevBin} | |
| 778 | + | h.launcher = &cliLauncher{bin: o.vevBin} | |
| 779 | + | } | |
| 780 | + | type launchedRole struct { | |
| 781 | + | mapping processMapping | |
| 782 | + | process launchedProcess | |
| 590 | 783 | } | |
| 591 | - | processes := make([]launchedProcess, 0, len(maps)) | |
| 784 | + | processes := make([]launchedRole, 0, len(maps)) | |
| 592 | 785 | defer func() { | |
| 593 | - | for _, p := range processes { | |
| 594 | - | if e := p.Close(); err == nil && e != nil { | |
| 786 | + | // Clients own SSH-stdio descendants; close them before the deferred UDP | |
| 787 | + | // peer and daemon so no role survives a failed or completed run. | |
| 788 | + | for i := len(processes) - 1; i >= 0; i-- { | |
| 789 | + | if e := processes[i].process.Close(); err == nil && e != nil { | |
| 595 | 790 | err = e | |
| 596 | 791 | } | |
| 597 | 792 | } | |
| 598 | 793 | }() | |
| 599 | - | for _, pm := range maps { | |
| 600 | - | p, e := h.launcher.Launch(pm, roleArgs(s, pm)) | |
| 794 | + | selectedTransport, e := manifestTransport(mat, s.Transport) | |
| 795 | + | if e != nil { | |
| 796 | + | return res, e | |
| 797 | + | } | |
| 798 | + | for _, pm := range launchOrder(maps) { | |
| 799 | + | p, e := h.launcher.Launch(pm, routeRoleArgs(s, pm, selectedTransport)) | |
| 601 | 800 | if e != nil { | |
| 602 | 801 | return res, e | |
| 603 | 802 | } | |
| 604 | - | processes = append(processes, p) | |
| 803 | + | processes = append(processes, launchedRole{mapping: pm, process: p}) | |
| 605 | 804 | } | |
| 606 | - | for i, p := range processes { | |
| 607 | - | if maps[i].Role != "client" { | |
| 805 | + | for _, p := range processes { | |
| 806 | + | if p.mapping.Role != "client" { | |
| 608 | 807 | continue | |
| 609 | 808 | } | |
| 610 | - | if err = p.Warmup(workloadInput(s, run, "warmup")); err != nil { | |
| 809 | + | if err = p.process.Warmup(workloadInput(s, run, "warmup")); err != nil { | |
| 611 | 810 | return res, err | |
| 612 | 811 | } | |
| 613 | 812 | } | |
| @@ -616,8 +815,8 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||
| 616 | 815 | h.clock.Sleep(o.warmup) | |
| 617 | 816 | marks := []harnessMark{} | |
| 618 | 817 | seq := uint64(0) | |
| 619 | - | for i, p := range processes { | |
| 620 | - | if maps[i].Role != "client" { | |
| 818 | + | for _, p := range processes { | |
| 819 | + | if p.mapping.Role != "client" { | |
| 621 | 820 | continue | |
| 622 | 821 | } | |
| 623 | 822 | seq++ | |
| @@ -630,7 +829,7 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||
| 630 | 829 | marks = append(marks, harnessMark{s.ID, run, sequence, "terminal_flushed", h.clock.Now(), true}) | |
| 631 | 830 | return nil | |
| 632 | 831 | } | |
| 633 | - | if err = p.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); err != nil { | |
| 832 | + | if err = p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); err != nil { | |
| 634 | 833 | return res, err | |
| 635 | 834 | } | |
| 636 | 835 | } | |
| @@ -654,22 +853,78 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||
| 654 | 853 | return runResult{Spans: spans, Scenario: s.ID, Run: run, Samples: len(samples), EndToEnd: samples, Processes: maps}, nil | |
| 655 | 854 | } | |
| 656 | 855 | ||
| 657 | - | func roleArgs(s scenario, m processMapping) []string { | |
| 856 | + | func roleArgs(s scenario, m processMapping) roleCommand { | |
| 857 | + | return routeRoleArgs(s, m, scenarioTransport(s)) | |
| 858 | + | } | |
| 859 | + | ||
| 860 | + | func routeRoleArgs(s scenario, m processMapping, selected transport) roleCommand { | |
| 658 | 861 | session := "perf-" + safeName(s.ID) + fmt.Sprintf("-%03d", m.Run) | |
| 862 | + | remote := []string{"attach", "harness@127.0.0.1:" + session} | |
| 659 | 863 | switch m.Role { | |
| 660 | 864 | case "daemon": | |
| 661 | - | return []string{"--daemon"} | |
| 865 | + | return roleCommand{Args: []string{"--daemon"}} | |
| 662 | 866 | case "client": | |
| 663 | - | // The client is intentionally driven through its documented interactive | |
| 664 | - | // CLI. Transport peers below are independently launched public roles. | |
| 665 | - | return []string{"new", session} | |
| 867 | + | switch s.Transport { | |
| 868 | + | case "local": | |
| 869 | + | return roleCommand{Args: []string{"new", session}} | |
| 870 | + | case "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_0pct", "udp_loss_1pct": | |
| 871 | + | return roleCommand{Args: remote} | |
| 872 | + | } | |
| 666 | 873 | case "ssh_stdio_peer": | |
| 667 | - | return []string{"_stdio", session} | |
| 874 | + | return roleCommand{Args: []string{"_stdio", session}, Transport: transport{ID: s.Transport, Kind: "ssh_stdio"}} | |
| 668 | 875 | case "udp_peer": | |
| 669 | - | return []string{"_udp-proxy", session} | |
| 876 | + | return roleCommand{Args: []string{"_udp-proxy", session}, Transport: selected} | |
| 877 | + | } | |
| 878 | + | return roleCommand{} | |
| 879 | + | } | |
| 880 | + | ||
| 881 | + | func manifestTransport(m manifest, id string) (transport, error) { | |
| 882 | + | for _, t := range m.Transports { | |
| 883 | + | if t.ID == id { | |
| 884 | + | return t, nil | |
| 885 | + | } | |
| 886 | + | } | |
| 887 | + | // Focused unit tests may call runOne with a minimal manifest; production | |
| 888 | + | // already validates every scenario reference before this point. | |
| 889 | + | if m.Schema == 0 { | |
| 890 | + | return scenarioTransport(scenario{Transport: id}), nil | |
| 891 | + | } | |
| 892 | + | return transport{}, fmt.Errorf("scenario references missing transport %q", id) | |
| 893 | + | } | |
| 894 | + | ||
| 895 | + | func scenarioTransport(s scenario) transport { | |
| 896 | + | switch s.Transport { | |
| 897 | + | case "udp_baseline": | |
| 898 | + | return transport{ID: s.Transport, Kind: "udp"} | |
| 899 | + | case "udp_25ms": | |
| 900 | + | return transport{ID: s.Transport, Kind: "udp", RTTMS: 25} | |
| 901 | + | case "udp_100ms": | |
| 902 | + | return transport{ID: s.Transport, Kind: "udp", RTTMS: 100} | |
| 903 | + | case "udp_loss_0pct": | |
| 904 | + | return transport{ID: s.Transport, Kind: "udp", LossPercent: 0} | |
| 905 | + | case "udp_loss_1pct": | |
| 906 | + | return transport{ID: s.Transport, Kind: "udp", LossPercent: 1} | |
| 670 | 907 | default: | |
| 671 | - | return nil | |
| 908 | + | return transport{ID: s.Transport} | |
| 909 | + | } | |
| 910 | + | } | |
| 911 | + | ||
| 912 | + | func launchOrder(mappings []processMapping) []processMapping { | |
| 913 | + | out := append([]processMapping(nil), mappings...) | |
| 914 | + | priority := func(role string) int { | |
| 915 | + | switch role { | |
| 916 | + | case "daemon": | |
| 917 | + | return 0 | |
| 918 | + | case "ssh_stdio_peer", "udp_peer": | |
| 919 | + | return 1 | |
| 920 | + | case "client": | |
| 921 | + | return 2 | |
| 922 | + | default: | |
| 923 | + | return 3 | |
| 924 | + | } | |
| 672 | 925 | } | |
| 926 | + | sort.SliceStable(out, func(i, j int) bool { return priority(out[i].Role) < priority(out[j].Role) }) | |
| 927 | + | return out | |
| 673 | 928 | } | |
| 674 | 929 | ||
| 675 | 930 | // workloadInput is shell input delivered through the client PTY. It avoids any | |
| @@ -14,6 +14,7 @@ import ( | |||||
| 14 | "os/exec" | 14 | "os/exec" | ||
| 15 | "path/filepath" | 15 | "path/filepath" | ||
| 16 | "sort" | 16 | "sort" | ||
| 17 | + | "strconv" | |||
| 17 | "strings" | 18 | "strings" | ||
| 18 | "sync" | 19 | "sync" | ||
| 19 | "syscall" | 20 | "syscall" | ||
| @@ -133,13 +134,57 @@ type launchedProcess interface { | |||||
| 133 | Measure([]byte, func() error, func() error) error | 134 | Measure([]byte, func() error, func() error) error | ||
| 134 | Close() error | 135 | Close() error | ||
| 135 | } | 136 | } | ||
| 137 | + | ||||
| 138 | + | // roleCommand contains only argv accepted by app.parseArgs. Transport routing | |||
| 139 | + | // metadata configures the harness's public-command seam; it is never passed as | |||
| 140 | + | // an unrecognised vev flag. | |||
| 141 | + | type roleCommand struct { | |||
| 142 | + | Args []string | |||
| 143 | + | Transport transport | |||
| 144 | + | } | |||
| 145 | + | ||||
| 136 | type launcher interface { | 146 | type launcher interface { | ||
| 137 | - | Launch(processMapping, []string) (launchedProcess, error) | 147 | + | Launch(processMapping, roleCommand) (launchedProcess, error) |
| 138 | } | 148 | } | ||
| 139 | 149 | ||||
| 140 | // cliLauncher is deliberately limited to executing the supplied public vev | 150 | // cliLauncher is deliberately limited to executing the supplied public vev | ||
| 141 | // binary. Scenario orchestration never reaches into an in-process daemon. | 151 | // binary. Scenario orchestration never reaches into an in-process daemon. | ||
| 142 | - | type cliLauncher struct{ bin string } | 152 | + | type cliLauncher struct { |
| 153 | + | bin string | |||
| 154 | + | mu sync.Mutex | |||
| 155 | + | peers map[string]peerRoute | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | type peerRoute struct { | |||
| 159 | + | mapping processMapping | |||
| 160 | + | command roleCommand | |||
| 161 | + | pidPath string | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | type preparedPeer struct{ route peerRoute } | |||
| 165 | + | ||||
| 166 | + | func (p preparedPeer) Warmup([]byte) error { return nil } | |||
| 167 | + | func (p preparedPeer) Measure([]byte, func() error, func() error) error { return nil } | |||
| 168 | + | func (p preparedPeer) Close() error { | |||
| 169 | + | if p.route.pidPath == "" { | |||
| 170 | + | return nil | |||
| 171 | + | } | |||
| 172 | + | b, err := os.ReadFile(p.route.pidPath) | |||
| 173 | + | if errors.Is(err, os.ErrNotExist) { | |||
| 174 | + | return nil | |||
| 175 | + | } | |||
| 176 | + | if err != nil { | |||
| 177 | + | return err | |||
| 178 | + | } | |||
| 179 | + | pid, err := strconv.Atoi(strings.TrimSpace(string(b))) | |||
| 180 | + | if err != nil || pid <= 0 { | |||
| 181 | + | return fmt.Errorf("invalid owned peer pid: %q", b) | |||
| 182 | + | } | |||
| 183 | + | if err := syscall.Kill(pid, syscall.SIGTERM); err != nil && !errors.Is(err, syscall.ESRCH) { | |||
| 184 | + | return err | |||
| 185 | + | } | |||
| 186 | + | return nil | |||
| 187 | + | } | |||
| 143 | 188 | ||||
| 144 | type terminalOutput interface { | 189 | type terminalOutput interface { | ||
| 145 | io.Writer | 190 | io.Writer | ||
| @@ -157,8 +202,8 @@ type cliProcess struct { | |||||
| 157 | waitErr chan error | 202 | waitErr chan error | ||
| 158 | } | 203 | } | ||
| 159 | 204 | ||||
| 160 | - | func (l cliLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | 205 | + | func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) { |
| 161 | - | if len(args) == 0 { | 206 | + | if len(role.Args) == 0 { |
| 162 | return nil, errors.New("public CLI role has no arguments") | 207 | return nil, errors.New("public CLI role has no arguments") | ||
| 163 | } | 208 | } | ||
| 164 | if info, err := os.Stat(l.bin); err != nil { | 209 | if info, err := os.Stat(l.bin); err != nil { | ||
| @@ -166,14 +211,30 @@ func (l cliLauncher) Launch(m processMapping, args []string) (launchedProcess, e | |||||
| 166 | } else if info.IsDir() { | 211 | } else if info.IsDir() { | ||
| 167 | return nil, fmt.Errorf("public CLI binary %q is a directory", l.bin) | 212 | return nil, fmt.Errorf("public CLI binary %q is a directory", l.bin) | ||
| 168 | } | 213 | } | ||
| 169 | - | cmd := exec.Command(l.bin, args...) | 214 | + | if m.Role == "ssh_stdio_peer" || m.Role == "udp_peer" { |
| 170 | - | cmd.Env = append(withoutEnv(os.Environ(), "VEV"), "VEV_PERF_TRACE="+m.TracePath, "VEV_PERF_PROCESS_ID="+m.ProcessID, | 215 | + | return l.preparePeer(m, role) |
| 171 | - | "XDG_RUNTIME_DIR="+filepath.Join(filepath.Dir(m.TracePath), "runtime"), | 216 | + | } |
| 172 | - | "XDG_STATE_HOME="+filepath.Join(filepath.Dir(m.TracePath), "state"), "TERM=xterm-256color") | 217 | + | cmd := exec.Command(l.bin, role.Args...) |
| 173 | - | if err := safedir.EnsurePrivate(filepath.Join(filepath.Dir(m.TracePath), "runtime")); err != nil { | 218 | + | runDir := filepath.Dir(m.TracePath) |
| 219 | + | cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_REMOTE_TRANSPORT"), "VEV_PERF_TRACE="+m.TracePath, "VEV_PERF_PROCESS_ID="+m.ProcessID, | |||
| 220 | + | "XDG_RUNTIME_DIR="+filepath.Join(runDir, "runtime"), | |||
| 221 | + | "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color") | |||
| 222 | + | if m.Role == "client" { | |||
| 223 | + | l.mu.Lock() | |||
| 224 | + | peer, routed := l.peers[runDir] | |||
| 225 | + | l.mu.Unlock() | |||
| 226 | + | if routed { | |||
| 227 | + | mode, err := remoteMode(peer.command.Transport) | |||
| 228 | + | if err != nil { | |||
| 229 | + | return nil, err | |||
| 230 | + | } | |||
| 231 | + | cmd.Env = append(cmd.Env, "PATH="+runDir+":"+os.Getenv("PATH"), "VEV_REMOTE_TRANSPORT="+mode) | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil { | |||
| 174 | return nil, err | 235 | return nil, err | ||
| 175 | } | 236 | } | ||
| 176 | - | if err := safedir.EnsurePrivate(filepath.Join(filepath.Dir(m.TracePath), "state")); err != nil { | 237 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil { |
| 177 | return nil, err | 238 | return nil, err | ||
| 178 | } | 239 | } | ||
| 179 | p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1)} | 240 | p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1)} | ||
| @@ -216,6 +277,82 @@ func (l cliLauncher) Launch(m processMapping, args []string) (launchedProcess, e | |||||
| 216 | return p, nil | 277 | return p, nil | ||
| 217 | } | 278 | } | ||
| 218 | 279 | ||||
| 280 | + | // preparePeer installs a per-run ssh command seam. The ordinary public client | |||
| 281 | + | // still executes its documented remote attach command; its ssh child invokes | |||
| 282 | + | // only the parsed public _stdio or _udp-proxy command. This makes the peer | |||
| 283 | + | // carrying client traffic the declared, exclusively traced role rather than a | |||
| 284 | + | // separately launched but unused process. | |||
| 285 | + | func (l *cliLauncher) preparePeer(m processMapping, role roleCommand) (launchedProcess, error) { | |||
| 286 | + | runDir := filepath.Dir(m.TracePath) | |||
| 287 | + | if err := safedir.EnsurePrivate(runDir); err != nil { | |||
| 288 | + | return nil, err | |||
| 289 | + | } | |||
| 290 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil { | |||
| 291 | + | return nil, err | |||
| 292 | + | } | |||
| 293 | + | if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil { | |||
| 294 | + | return nil, err | |||
| 295 | + | } | |||
| 296 | + | if len(role.Args) != 2 || (role.Args[0] != "_stdio" && role.Args[0] != "_udp-proxy") { | |||
| 297 | + | return nil, fmt.Errorf("unsupported public peer command %q", role.Args) | |||
| 298 | + | } | |||
| 299 | + | route := peerRoute{mapping: m, command: role} | |||
| 300 | + | if m.Role == "udp_peer" { | |||
| 301 | + | route.pidPath = filepath.Join(runDir, "udp-peer.pid") | |||
| 302 | + | } | |||
| 303 | + | shim := filepath.Join(runDir, "ssh") | |||
| 304 | + | ready := filepath.Join(runDir, "udp-peer.ready") | |||
| 305 | + | stderr := filepath.Join(runDir, "udp-peer.stderr") | |||
| 306 | + | // The bootstrap protocol requires the SSH command to exit after forwarding | |||
| 307 | + | // the readiness line. The long-lived proxy is owned by pidPath and receives | |||
| 308 | + | // the role's unique trace identity, never the bootstrap/client identity. | |||
| 309 | + | body := fmt.Sprintf(`#!/bin/sh | |||
| 310 | + | set -eu | |||
| 311 | + | case "$*" in | |||
| 312 | + | *"_stdio"*) | |||
| 313 | + | exec env VEV_PERF_TRACE=%[1]q VEV_PERF_PROCESS_ID=%[2]q XDG_RUNTIME_DIR=%[3]q XDG_STATE_HOME=%[4]q TERM=xterm-256color %[5]q _stdio %[6]q | |||
| 314 | + | ;; | |||
| 315 | + | *"_udp-bootstrap"*) | |||
| 316 | + | rm -f %[7]q %[8]q | |||
| 317 | + | env VEV_PERF_TRACE=%[1]q VEV_PERF_PROCESS_ID=%[2]q XDG_RUNTIME_DIR=%[3]q XDG_STATE_HOME=%[4]q TERM=xterm-256color VEV_PERF_UDP_RTT_MS=%[9]q VEV_PERF_UDP_LOSS_PERCENT=%[10]q %[5]q _udp-proxy %[6]q >%[7]q 2>%[8]q & | |||
| 318 | + | peer=$! | |||
| 319 | + | printf '%%s\\n' "$peer" > %[11]q | |||
| 320 | + | i=0 | |||
| 321 | + | while [ ! -s %[7]q ] && [ "$i" -lt 1000 ]; do sleep 0.01; i=$((i+1)); done | |||
| 322 | + | test -s %[7]q | |||
| 323 | + | head -n 1 %[7]q | |||
| 324 | + | exit 0 | |||
| 325 | + | ;; | |||
| 326 | + | *) echo 'vev harness ssh seam rejected non-vev command' >&2; exit 64 ;; | |||
| 327 | + | esac | |||
| 328 | + | `, m.TracePath, m.ProcessID, filepath.Join(runDir, "runtime"), filepath.Join(runDir, "state"), l.bin, role.Args[1], ready, stderr, strconv.Itoa(role.Transport.RTTMS), strconv.Itoa(role.Transport.LossPercent), route.pidPath) | |||
| 329 | + | if err := os.WriteFile(shim, []byte(body), 0o700); err != nil { | |||
| 330 | + | return nil, err | |||
| 331 | + | } | |||
| 332 | + | l.mu.Lock() | |||
| 333 | + | if l.peers == nil { | |||
| 334 | + | l.peers = make(map[string]peerRoute) | |||
| 335 | + | } | |||
| 336 | + | if _, exists := l.peers[runDir]; exists { | |||
| 337 | + | l.mu.Unlock() | |||
| 338 | + | return nil, fmt.Errorf("duplicate transport peer for %s", runDir) | |||
| 339 | + | } | |||
| 340 | + | l.peers[runDir] = route | |||
| 341 | + | l.mu.Unlock() | |||
| 342 | + | return preparedPeer{route: route}, nil | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | func remoteMode(t transport) (string, error) { | |||
| 346 | + | switch t.Kind { | |||
| 347 | + | case "ssh_stdio": | |||
| 348 | + | return "stdio", nil | |||
| 349 | + | case "udp": | |||
| 350 | + | return "udp", nil | |||
| 351 | + | default: | |||
| 352 | + | return "", fmt.Errorf("transport %q cannot route through a remote peer", t.ID) | |||
| 353 | + | } | |||
| 354 | + | } | |||
| 355 | + | ||||
| 219 | func withoutEnv(env []string, names ...string) []string { | 356 | func withoutEnv(env []string, names ...string) []string { | ||
| 220 | remove := make(map[string]bool, len(names)) | 357 | remove := make(map[string]bool, len(names)) | ||
| 221 | for _, name := range names { | 358 | for _, name := range names { | ||
| @@ -320,7 +457,9 @@ func (p *cliProcess) Close() error { | |||||
| 320 | close(p.done) | 457 | close(p.done) | ||
| 321 | } | 458 | } | ||
| 322 | if p.cmd != nil && p.cmd.Process != nil { | 459 | if p.cmd != nil && p.cmd.Process != nil { | ||
| 323 | - | _ = p.cmd.Process.Signal(syscall.SIGTERM) | 460 | + | // The client is a session/process-group leader; terminate its ssh |
| 461 | + | // seam/_stdio descendants as well as the terminal client itself. | |||
| 462 | + | _ = syscall.Kill(-p.cmd.Process.Pid, syscall.SIGTERM) | |||
| 324 | } | 463 | } | ||
| 325 | if p.pty != nil { | 464 | if p.pty != nil { | ||
| 326 | _ = p.pty.Close() | 465 | _ = p.pty.Close() | ||
| @@ -512,9 +651,12 @@ func validateManifest(m manifest) error { | |||||
| 512 | if t.ID == "" { | 651 | if t.ID == "" { | ||
| 513 | return errors.New("transport missing id") | 652 | return errors.New("transport missing id") | ||
| 514 | } | 653 | } | ||
| 654 | + | if err := validateTransportFixture(t); err != nil { | |||
| 655 | + | return err | |||
| 656 | + | } | |||
| 515 | trans[t.ID] = true | 657 | trans[t.ID] = true | ||
| 516 | } | 658 | } | ||
| 517 | - | for _, v := range []string{"local", "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_1pct"} { | 659 | + | for _, v := range []string{"local", "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_0pct", "udp_loss_1pct"} { |
| 518 | if !trans[v] { | 660 | if !trans[v] { | ||
| 519 | return fmt.Errorf("missing canonical transport %s", v) | 661 | return fmt.Errorf("missing canonical transport %s", v) | ||
| 520 | } | 662 | } | ||
| @@ -535,6 +677,9 @@ func validateManifest(m manifest) error { | |||||
| 535 | if s.InapplicableReason != "" && len(s.Roles) != 0 { | 677 | if s.InapplicableReason != "" && len(s.Roles) != 0 { | ||
| 536 | return fmt.Errorf("inapplicable scenario %s must not launch roles", s.ID) | 678 | return fmt.Errorf("inapplicable scenario %s must not launch roles", s.ID) | ||
| 537 | } | 679 | } | ||
| 680 | + | if s.InapplicableReason == "" && !equalRoleSet(s.Roles, requiredRoles(s.Transport)) { | |||
| 681 | + | return fmt.Errorf("scenario %s roles do not route transport %s", s.ID, s.Transport) | |||
| 682 | + | } | |||
| 538 | covered[s.Topology+"\x00"+s.Workload+"\x00"+s.Transport] = true | 683 | covered[s.Topology+"\x00"+s.Workload+"\x00"+s.Transport] = true | ||
| 539 | } | 684 | } | ||
| 540 | for topologyID := range tops { | 685 | for topologyID := range tops { | ||
| @@ -549,6 +694,50 @@ func validateManifest(m manifest) error { | |||||
| 549 | } | 694 | } | ||
| 550 | return nil | 695 | return nil | ||
| 551 | } | 696 | } | ||
| 697 | + | func requiredRoles(transportID string) []string { | |||
| 698 | + | switch transportID { | |||
| 699 | + | case "local": | |||
| 700 | + | return []string{"daemon", "client"} | |||
| 701 | + | case "ssh_stdio": | |||
| 702 | + | return []string{"daemon", "client", "ssh_stdio_peer"} | |||
| 703 | + | default: | |||
| 704 | + | return []string{"daemon", "client", "udp_peer"} | |||
| 705 | + | } | |||
| 706 | + | } | |||
| 707 | + | ||||
| 708 | + | func equalRoleSet(got, want []string) bool { | |||
| 709 | + | if len(got) != len(want) { | |||
| 710 | + | return false | |||
| 711 | + | } | |||
| 712 | + | seen := make(map[string]bool, len(got)) | |||
| 713 | + | for _, role := range got { | |||
| 714 | + | seen[role] = true | |||
| 715 | + | } | |||
| 716 | + | for _, role := range want { | |||
| 717 | + | if !seen[role] { | |||
| 718 | + | return false | |||
| 719 | + | } | |||
| 720 | + | } | |||
| 721 | + | return true | |||
| 722 | + | } | |||
| 723 | + | ||||
| 724 | + | func validateTransportFixture(t transport) error { | |||
| 725 | + | want := map[string]transport{ | |||
| 726 | + | "local": {ID: "local", Kind: "local"}, | |||
| 727 | + | "ssh_stdio": {ID: "ssh_stdio", Kind: "ssh_stdio"}, | |||
| 728 | + | "udp_baseline": {ID: "udp_baseline", Kind: "udp"}, | |||
| 729 | + | "udp_25ms": {ID: "udp_25ms", Kind: "udp", RTTMS: 25}, | |||
| 730 | + | "udp_100ms": {ID: "udp_100ms", Kind: "udp", RTTMS: 100}, | |||
| 731 | + | "udp_loss_0pct": {ID: "udp_loss_0pct", Kind: "udp", LossPercent: 0}, | |||
| 732 | + | "udp_loss_1pct": {ID: "udp_loss_1pct", Kind: "udp", LossPercent: 1}, | |||
| 733 | + | } | |||
| 734 | + | w, ok := want[t.ID] | |||
| 735 | + | if !ok || t.Kind != w.Kind || t.RTTMS != w.RTTMS || t.LossPercent != w.LossPercent { | |||
| 736 | + | return fmt.Errorf("invalid canonical transport fixture %+v", t) | |||
| 737 | + | } | |||
| 738 | + | return nil | |||
| 739 | + | } | |||
| 740 | + | ||||
| 552 | func set(v []string) map[string]bool { | 741 | func set(v []string) map[string]bool { | ||
| 553 | r := map[string]bool{} | 742 | r := map[string]bool{} | ||
| 554 | for _, x := range v { | 743 | for _, x := range v { | ||
| @@ -556,7 +745,7 @@ func set(v []string) map[string]bool { | |||||
| 556 | } | 745 | } | ||
| 557 | return r | 746 | return r | ||
| 558 | } | 747 | } | ||
| 559 | - | func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writer) (res runResult, err error) { | 748 | + | func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Writer) (res runResult, err error) { |
| 560 | dir := filepath.Join(o.out, fmt.Sprintf("%s-run-%03d", safeName(s.ID), run)) | 749 | dir := filepath.Join(o.out, fmt.Sprintf("%s-run-%03d", safeName(s.ID), run)) | ||
| 561 | if err = h.mkdir(dir); err != nil { | 750 | if err = h.mkdir(dir); err != nil { | ||
| 562 | return res, err | 751 | return res, err | ||
| @@ -586,28 +775,38 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||||
| 586 | return res, err | 775 | return res, err | ||
| 587 | } | 776 | } | ||
| 588 | if h.launcher == nil { | 777 | if h.launcher == nil { | ||
| 589 | - | h.launcher = cliLauncher{o.vevBin} | 778 | + | h.launcher = &cliLauncher{bin: o.vevBin} |
| 779 | + | } | |||
| 780 | + | type launchedRole struct { | |||
| 781 | + | mapping processMapping | |||
| 782 | + | process launchedProcess | |||
| 590 | } | 783 | } | ||
| 591 | - | processes := make([]launchedProcess, 0, len(maps)) | 784 | + | processes := make([]launchedRole, 0, len(maps)) |
| 592 | defer func() { | 785 | defer func() { | ||
| 593 | - | for _, p := range processes { | 786 | + | // Clients own SSH-stdio descendants; close them before the deferred UDP |
| 594 | - | if e := p.Close(); err == nil && e != nil { | 787 | + | // peer and daemon so no role survives a failed or completed run. |
| 788 | + | for i := len(processes) - 1; i >= 0; i-- { | |||
| 789 | + | if e := processes[i].process.Close(); err == nil && e != nil { | |||
| 595 | err = e | 790 | err = e | ||
| 596 | } | 791 | } | ||
| 597 | } | 792 | } | ||
| 598 | }() | 793 | }() | ||
| 599 | - | for _, pm := range maps { | 794 | + | selectedTransport, e := manifestTransport(mat, s.Transport) |
| 600 | - | p, e := h.launcher.Launch(pm, roleArgs(s, pm)) | 795 | + | if e != nil { |
| 796 | + | return res, e | |||
| 797 | + | } | |||
| 798 | + | for _, pm := range launchOrder(maps) { | |||
| 799 | + | p, e := h.launcher.Launch(pm, routeRoleArgs(s, pm, selectedTransport)) | |||
| 601 | if e != nil { | 800 | if e != nil { | ||
| 602 | return res, e | 801 | return res, e | ||
| 603 | } | 802 | } | ||
| 604 | - | processes = append(processes, p) | 803 | + | processes = append(processes, launchedRole{mapping: pm, process: p}) |
| 605 | } | 804 | } | ||
| 606 | - | for i, p := range processes { | 805 | + | for _, p := range processes { |
| 607 | - | if maps[i].Role != "client" { | 806 | + | if p.mapping.Role != "client" { |
| 608 | continue | 807 | continue | ||
| 609 | } | 808 | } | ||
| 610 | - | if err = p.Warmup(workloadInput(s, run, "warmup")); err != nil { | 809 | + | if err = p.process.Warmup(workloadInput(s, run, "warmup")); err != nil { |
| 611 | return res, err | 810 | return res, err | ||
| 612 | } | 811 | } | ||
| 613 | } | 812 | } | ||
| @@ -616,8 +815,8 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||||
| 616 | h.clock.Sleep(o.warmup) | 815 | h.clock.Sleep(o.warmup) | ||
| 617 | marks := []harnessMark{} | 816 | marks := []harnessMark{} | ||
| 618 | seq := uint64(0) | 817 | seq := uint64(0) | ||
| 619 | - | for i, p := range processes { | 818 | + | for _, p := range processes { |
| 620 | - | if maps[i].Role != "client" { | 819 | + | if p.mapping.Role != "client" { |
| 621 | continue | 820 | continue | ||
| 622 | } | 821 | } | ||
| 623 | seq++ | 822 | seq++ | ||
| @@ -630,7 +829,7 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||||
| 630 | marks = append(marks, harnessMark{s.ID, run, sequence, "terminal_flushed", h.clock.Now(), true}) | 829 | marks = append(marks, harnessMark{s.ID, run, sequence, "terminal_flushed", h.clock.Now(), true}) | ||
| 631 | return nil | 830 | return nil | ||
| 632 | } | 831 | } | ||
| 633 | - | if err = p.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); err != nil { | 832 | + | if err = p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); err != nil { |
| 634 | return res, err | 833 | return res, err | ||
| 635 | } | 834 | } | ||
| 636 | } | 835 | } | ||
| @@ -654,22 +853,78 @@ func (h *harness) runOne(o options, _ manifest, s scenario, run int, raw io.Writ | |||||
| 654 | return runResult{Spans: spans, Scenario: s.ID, Run: run, Samples: len(samples), EndToEnd: samples, Processes: maps}, nil | 853 | return runResult{Spans: spans, Scenario: s.ID, Run: run, Samples: len(samples), EndToEnd: samples, Processes: maps}, nil | ||
| 655 | } | 854 | } | ||
| 656 | 855 | ||||
| 657 | - | func roleArgs(s scenario, m processMapping) []string { | 856 | + | func roleArgs(s scenario, m processMapping) roleCommand { |
| 857 | + | return routeRoleArgs(s, m, scenarioTransport(s)) | |||
| 858 | + | } | |||
| 859 | + | ||||
| 860 | + | func routeRoleArgs(s scenario, m processMapping, selected transport) roleCommand { | |||
| 658 | session := "perf-" + safeName(s.ID) + fmt.Sprintf("-%03d", m.Run) | 861 | session := "perf-" + safeName(s.ID) + fmt.Sprintf("-%03d", m.Run) | ||
| 862 | + | remote := []string{"attach", "harness@127.0.0.1:" + session} | |||
| 659 | switch m.Role { | 863 | switch m.Role { | ||
| 660 | case "daemon": | 864 | case "daemon": | ||
| 661 | - | return []string{"--daemon"} | 865 | + | return roleCommand{Args: []string{"--daemon"}} |
| 662 | case "client": | 866 | case "client": | ||
| 663 | - | // The client is intentionally driven through its documented interactive | 867 | + | switch s.Transport { |
| 664 | - | // CLI. Transport peers below are independently launched public roles. | 868 | + | case "local": |
| 665 | - | return []string{"new", session} | 869 | + | return roleCommand{Args: []string{"new", session}} |
| 870 | + | case "ssh_stdio", "udp_baseline", "udp_25ms", "udp_100ms", "udp_loss_0pct", "udp_loss_1pct": | |||
| 871 | + | return roleCommand{Args: remote} | |||
| 872 | + | } | |||
| 666 | case "ssh_stdio_peer": | 873 | case "ssh_stdio_peer": | ||
| 667 | - | return []string{"_stdio", session} | 874 | + | return roleCommand{Args: []string{"_stdio", session}, Transport: transport{ID: s.Transport, Kind: "ssh_stdio"}} |
| 668 | case "udp_peer": | 875 | case "udp_peer": | ||
| 669 | - | return []string{"_udp-proxy", session} | 876 | + | return roleCommand{Args: []string{"_udp-proxy", session}, Transport: selected} |
| 877 | + | } | |||
| 878 | + | return roleCommand{} | |||
| 879 | + | } | |||
| 880 | + | ||||
| 881 | + | func manifestTransport(m manifest, id string) (transport, error) { | |||
| 882 | + | for _, t := range m.Transports { | |||
| 883 | + | if t.ID == id { | |||
| 884 | + | return t, nil | |||
| 885 | + | } | |||
| 886 | + | } | |||
| 887 | + | // Focused unit tests may call runOne with a minimal manifest; production | |||
| 888 | + | // already validates every scenario reference before this point. | |||
| 889 | + | if m.Schema == 0 { | |||
| 890 | + | return scenarioTransport(scenario{Transport: id}), nil | |||
| 891 | + | } | |||
| 892 | + | return transport{}, fmt.Errorf("scenario references missing transport %q", id) | |||
| 893 | + | } | |||
| 894 | + | ||||
| 895 | + | func scenarioTransport(s scenario) transport { | |||
| 896 | + | switch s.Transport { | |||
| 897 | + | case "udp_baseline": | |||
| 898 | + | return transport{ID: s.Transport, Kind: "udp"} | |||
| 899 | + | case "udp_25ms": | |||
| 900 | + | return transport{ID: s.Transport, Kind: "udp", RTTMS: 25} | |||
| 901 | + | case "udp_100ms": | |||
| 902 | + | return transport{ID: s.Transport, Kind: "udp", RTTMS: 100} | |||
| 903 | + | case "udp_loss_0pct": | |||
| 904 | + | return transport{ID: s.Transport, Kind: "udp", LossPercent: 0} | |||
| 905 | + | case "udp_loss_1pct": | |||
| 906 | + | return transport{ID: s.Transport, Kind: "udp", LossPercent: 1} | |||
| 670 | default: | 907 | default: | ||
| 671 | - | return nil | 908 | + | return transport{ID: s.Transport} |
| 909 | + | } | |||
| 910 | + | } | |||
| 911 | + | ||||
| 912 | + | func launchOrder(mappings []processMapping) []processMapping { | |||
| 913 | + | out := append([]processMapping(nil), mappings...) | |||
| 914 | + | priority := func(role string) int { | |||
| 915 | + | switch role { | |||
| 916 | + | case "daemon": | |||
| 917 | + | return 0 | |||
| 918 | + | case "ssh_stdio_peer", "udp_peer": | |||
| 919 | + | return 1 | |||
| 920 | + | case "client": | |||
| 921 | + | return 2 | |||
| 922 | + | default: | |||
| 923 | + | return 3 | |||
| 924 | + | } | |||
| 672 | } | 925 | } | ||
| 926 | + | sort.SliceStable(out, func(i, j int) bool { return priority(out[i].Role) < priority(out[j].Role) }) | |||
| 927 | + | return out | |||
| 673 | } | 928 | } | ||
| 674 | 929 | ||||
| 675 | // workloadInput is shell input delivered through the client PTY. It avoids any | 930 | // workloadInput is shell input delivered through the client PTY. It avoids any | ||
M cmd/vev-perf-harness/main_test.go +124 -10
| @@ -42,13 +42,15 @@ func (p *fakeProcess) Close() error { p.closed = true; return nil } | |||
| 42 | 42 | type fakeLauncher struct { | |
| 43 | 43 | mappings []processMapping | |
| 44 | 44 | args [][]string | |
| 45 | + | commands []roleCommand | |
| 45 | 46 | process []*fakeProcess | |
| 46 | 47 | manifestPresent bool | |
| 47 | 48 | } | |
| 48 | 49 | ||
| 49 | - | func (l *fakeLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | |
| 50 | + | func (l *fakeLauncher) Launch(m processMapping, command roleCommand) (launchedProcess, error) { | |
| 50 | 51 | l.mappings = append(l.mappings, m) | |
| 51 | - | l.args = append(l.args, append([]string(nil), args...)) | |
| 52 | + | l.args = append(l.args, append([]string(nil), command.Args...)) | |
| 53 | + | l.commands = append(l.commands, command) | |
| 52 | 54 | _, e := os.Stat(filepath.Join(filepath.Dir(m.TracePath), "manifest.json")) | |
| 53 | 55 | l.manifestPresent = e == nil | |
| 54 | 56 | p := &fakeProcess{} | |
| @@ -82,7 +84,7 @@ func TestHarnessManifestCoversCanonicalMatrix(t *testing.T) { | |||
| 82 | 84 | if err := validateManifest(m); err != nil { | |
| 83 | 85 | t.Fatal(err) | |
| 84 | 86 | } | |
| 85 | - | if len(m.Scenarios) != 4*9*6 { | |
| 87 | + | if len(m.Scenarios) != 4*9*7 { | |
| 86 | 88 | t.Fatalf("scenarios=%d", len(m.Scenarios)) | |
| 87 | 89 | } | |
| 88 | 90 | m.Scenarios = m.Scenarios[:len(m.Scenarios)-1] | |
| @@ -188,9 +190,9 @@ func TestHarnessUsesPublicRoleCommandsAndPTYWorkloads(t *testing.T) { | |||
| 188 | 190 | {"udp_peer", []string{"_udp-proxy", "perf-s-001"}}, | |
| 189 | 191 | } { | |
| 190 | 192 | t.Run(tc.role, func(t *testing.T) { | |
| 191 | - | got := roleArgs(scenario{ID: "s"}, processMapping{Role: tc.role, Run: 1}) | |
| 192 | - | if !equalStrings(got, tc.want) { | |
| 193 | - | t.Fatalf("args=%q want %q", got, tc.want) | |
| 193 | + | got := roleArgs(scenario{ID: "s", Transport: "local"}, processMapping{Role: tc.role, Run: 1}) | |
| 194 | + | if !equalStrings(got.Args, tc.want) { | |
| 195 | + | t.Fatalf("args=%q want %q", got.Args, tc.want) | |
| 194 | 196 | } | |
| 195 | 197 | }) | |
| 196 | 198 | } | |
| @@ -200,6 +202,118 @@ func TestHarnessUsesPublicRoleCommandsAndPTYWorkloads(t *testing.T) { | |||
| 200 | 202 | } | |
| 201 | 203 | } | |
| 202 | 204 | ||
| 205 | + | func TestHarnessRoutesEveryRemoteFixtureThroughItsDeclaredPeer(t *testing.T) { | |
| 206 | + | cases := []struct { | |
| 207 | + | name string | |
| 208 | + | transport transport | |
| 209 | + | peer string | |
| 210 | + | wantRTT int | |
| 211 | + | wantLoss int | |
| 212 | + | }{ | |
| 213 | + | {"ssh", transport{ID: "ssh_stdio", Kind: "ssh_stdio"}, "ssh_stdio_peer", 0, 0}, | |
| 214 | + | {"udp baseline", transport{ID: "udp_baseline", Kind: "udp"}, "udp_peer", 0, 0}, | |
| 215 | + | {"udp 25ms", transport{ID: "udp_25ms", Kind: "udp", RTTMS: 25}, "udp_peer", 25, 0}, | |
| 216 | + | {"udp 100ms", transport{ID: "udp_100ms", Kind: "udp", RTTMS: 100}, "udp_peer", 100, 0}, | |
| 217 | + | {"udp loss zero", transport{ID: "udp_loss_0pct", Kind: "udp", LossPercent: 0}, "udp_peer", 0, 0}, | |
| 218 | + | {"udp loss", transport{ID: "udp_loss_1pct", Kind: "udp", LossPercent: 1}, "udp_peer", 0, 1}, | |
| 219 | + | } | |
| 220 | + | for _, tc := range cases { | |
| 221 | + | t.Run(tc.name, func(t *testing.T) { | |
| 222 | + | s := scenario{ID: "route", Transport: tc.transport.ID} | |
| 223 | + | client := routeRoleArgs(s, processMapping{Role: "client", Run: 2}, tc.transport) | |
| 224 | + | peer := routeRoleArgs(s, processMapping{Role: tc.peer, Run: 2}, tc.transport) | |
| 225 | + | if !equalStrings(client.Args, []string{"attach", "harness@127.0.0.1:perf-route-002"}) { | |
| 226 | + | t.Fatalf("client did not use public remote attach: %q", client.Args) | |
| 227 | + | } | |
| 228 | + | if tc.peer == "ssh_stdio_peer" && !equalStrings(peer.Args, []string{"_stdio", "perf-route-002"}) { | |
| 229 | + | t.Fatalf("ssh peer command=%q", peer.Args) | |
| 230 | + | } | |
| 231 | + | if tc.peer == "udp_peer" && !equalStrings(peer.Args, []string{"_udp-proxy", "perf-route-002"}) { | |
| 232 | + | t.Fatalf("udp peer command=%q", peer.Args) | |
| 233 | + | } | |
| 234 | + | if peer.Transport.RTTMS != tc.wantRTT || peer.Transport.LossPercent != tc.wantLoss { | |
| 235 | + | t.Fatalf("peer lost manifest network settings: %+v", peer.Transport) | |
| 236 | + | } | |
| 237 | + | }) | |
| 238 | + | } | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func TestHarnessFakeRunnerRoutesClientToPeerAndCleansEveryRole(t *testing.T) { | |
| 242 | + | for _, tc := range []struct { | |
| 243 | + | name, transport, peer, peerCommand string | |
| 244 | + | }{ | |
| 245 | + | {"ssh", "ssh_stdio", "ssh_stdio_peer", "_stdio"}, | |
| 246 | + | {"udp baseline", "udp_baseline", "udp_peer", "_udp-proxy"}, | |
| 247 | + | {"udp 25ms", "udp_25ms", "udp_peer", "_udp-proxy"}, | |
| 248 | + | {"udp 100ms", "udp_100ms", "udp_peer", "_udp-proxy"}, | |
| 249 | + | {"udp loss zero", "udp_loss_0pct", "udp_peer", "_udp-proxy"}, | |
| 250 | + | {"udp loss", "udp_loss_1pct", "udp_peer", "_udp-proxy"}, | |
| 251 | + | } { | |
| 252 | + | t.Run(tc.name, func(t *testing.T) { | |
| 253 | + | dir := t.TempDir() | |
| 254 | + | raw, err := os.OpenFile(filepath.Join(dir, "raw.jsonl"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) | |
| 255 | + | if err != nil { | |
| 256 | + | t.Fatal(err) | |
| 257 | + | } | |
| 258 | + | defer raw.Close() | |
| 259 | + | l := &fakeLauncher{} | |
| 260 | + | h := defaultHarness() | |
| 261 | + | h.clock, h.launcher = &fakeClock{}, l | |
| 262 | + | s := scenario{ID: "routed", Transport: tc.transport, Roles: []string{"daemon", "client", tc.peer}} | |
| 263 | + | if _, err := h.runOne(options{out: dir, warmup: time.Second, duration: minimumDuration, repetitions: minimumRepetitions}, manifest{}, s, 1, raw); err != nil { | |
| 264 | + | t.Fatal(err) | |
| 265 | + | } | |
| 266 | + | if len(l.mappings) != 3 || l.mappings[1].Role != tc.peer || l.mappings[2].Role != "client" { | |
| 267 | + | t.Fatalf("dependency launch order=%+v", l.mappings) | |
| 268 | + | } | |
| 269 | + | if l.commands[1].Args[0] != tc.peerCommand || l.commands[2].Args[0] != "attach" { | |
| 270 | + | t.Fatalf("commands do not connect client through declared peer: %+v", l.commands) | |
| 271 | + | } | |
| 272 | + | for i, p := range l.process { | |
| 273 | + | if !p.closed { | |
| 274 | + | t.Fatalf("role %s was not cleaned up", l.mappings[i].Role) | |
| 275 | + | } | |
| 276 | + | } | |
| 277 | + | }) | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | func TestCLITransportSeamOwnsExclusivePeerTraceAndCleanup(t *testing.T) { | |
| 282 | + | dir := t.TempDir() | |
| 283 | + | if err := os.Chmod(dir, 0o700); err != nil { | |
| 284 | + | t.Fatal(err) | |
| 285 | + | } | |
| 286 | + | l := &cliLauncher{bin: "/bin/true"} | |
| 287 | + | m := processMapping{ProcessID: "udp-peer", TracePath: filepath.Join(dir, "udp.jsonl"), Role: "udp_peer"} | |
| 288 | + | if err := os.WriteFile(m.TracePath, nil, 0o600); err != nil { | |
| 289 | + | t.Fatal(err) | |
| 290 | + | } | |
| 291 | + | p, err := l.preparePeer(m, roleCommand{Args: []string{"_udp-proxy", "work"}, Transport: transport{ID: "udp_25ms", Kind: "udp", RTTMS: 25}}) | |
| 292 | + | if err != nil { | |
| 293 | + | t.Fatal(err) | |
| 294 | + | } | |
| 295 | + | shim, err := os.ReadFile(filepath.Join(dir, "ssh")) | |
| 296 | + | if err != nil { | |
| 297 | + | t.Fatal(err) | |
| 298 | + | } | |
| 299 | + | text := string(shim) | |
| 300 | + | for _, want := range []string{"_udp-proxy", m.TracePath, m.ProcessID, "VEV_PERF_UDP_RTT_MS=\"25\"", "udp-peer.pid"} { | |
| 301 | + | if !strings.Contains(text, want) { | |
| 302 | + | t.Errorf("seam does not retain %q:\n%s", want, text) | |
| 303 | + | } | |
| 304 | + | } | |
| 305 | + | // A nonexistent pid is already-cleaned-up; Close must treat it as such. | |
| 306 | + | if err := p.Close(); err != nil { | |
| 307 | + | t.Fatal(err) | |
| 308 | + | } | |
| 309 | + | l.mu.Lock() | |
| 310 | + | route := l.peers[dir] | |
| 311 | + | l.mu.Unlock() | |
| 312 | + | if route.mapping.TracePath != m.TracePath || route.mapping.ProcessID != m.ProcessID || route.command.Transport.RTTMS != 25 { | |
| 313 | + | t.Fatalf("peer route is not exclusively correlated: %+v", route) | |
| 314 | + | } | |
| 315 | + | } | |
| 316 | + | ||
| 203 | 317 | func TestHarnessRejectsInvalidBoundaryPairs(t *testing.T) { | |
| 204 | 318 | for _, marks := range [][]harnessMark{ | |
| 205 | 319 | {{Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}}, | |
| @@ -240,8 +354,8 @@ func TestHarnessCleansRunDirectoryOnProcessFailure(t *testing.T) { | |||
| 240 | 354 | ||
| 241 | 355 | type failingLauncher struct{ *fakeLauncher } | |
| 242 | 356 | ||
| 243 | - | func (l *failingLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | |
| 244 | - | p, err := l.fakeLauncher.Launch(m, args) | |
| 357 | + | func (l *failingLauncher) Launch(m processMapping, command roleCommand) (launchedProcess, error) { | |
| 358 | + | p, err := l.fakeLauncher.Launch(m, command) | |
| 245 | 359 | if err != nil { | |
| 246 | 360 | return nil, err | |
| 247 | 361 | } | |
| @@ -280,8 +394,8 @@ func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) { | |||
| 280 | 394 | ||
| 281 | 395 | type traceLauncher struct{ fakeLauncher } | |
| 282 | 396 | ||
| 283 | - | func (l *traceLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | |
| 284 | - | p, err := l.fakeLauncher.Launch(m, args) | |
| 397 | + | func (l *traceLauncher) Launch(m processMapping, command roleCommand) (launchedProcess, error) { | |
| 398 | + | p, err := l.fakeLauncher.Launch(m, command) | |
| 285 | 399 | if err != nil { | |
| 286 | 400 | return nil, err | |
| 287 | 401 | } | |
| @@ -42,13 +42,15 @@ func (p *fakeProcess) Close() error { p.closed = true; return nil } | |||||
| 42 | type fakeLauncher struct { | 42 | type fakeLauncher struct { | ||
| 43 | mappings []processMapping | 43 | mappings []processMapping | ||
| 44 | args [][]string | 44 | args [][]string | ||
| 45 | + | commands []roleCommand | |||
| 45 | process []*fakeProcess | 46 | process []*fakeProcess | ||
| 46 | manifestPresent bool | 47 | manifestPresent bool | ||
| 47 | } | 48 | } | ||
| 48 | 49 | ||||
| 49 | - | func (l *fakeLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | 50 | + | func (l *fakeLauncher) Launch(m processMapping, command roleCommand) (launchedProcess, error) { |
| 50 | l.mappings = append(l.mappings, m) | 51 | l.mappings = append(l.mappings, m) | ||
| 51 | - | l.args = append(l.args, append([]string(nil), args...)) | 52 | + | l.args = append(l.args, append([]string(nil), command.Args...)) |
| 53 | + | l.commands = append(l.commands, command) | |||
| 52 | _, e := os.Stat(filepath.Join(filepath.Dir(m.TracePath), "manifest.json")) | 54 | _, e := os.Stat(filepath.Join(filepath.Dir(m.TracePath), "manifest.json")) | ||
| 53 | l.manifestPresent = e == nil | 55 | l.manifestPresent = e == nil | ||
| 54 | p := &fakeProcess{} | 56 | p := &fakeProcess{} | ||
| @@ -82,7 +84,7 @@ func TestHarnessManifestCoversCanonicalMatrix(t *testing.T) { | |||||
| 82 | if err := validateManifest(m); err != nil { | 84 | if err := validateManifest(m); err != nil { | ||
| 83 | t.Fatal(err) | 85 | t.Fatal(err) | ||
| 84 | } | 86 | } | ||
| 85 | - | if len(m.Scenarios) != 4*9*6 { | 87 | + | if len(m.Scenarios) != 4*9*7 { |
| 86 | t.Fatalf("scenarios=%d", len(m.Scenarios)) | 88 | t.Fatalf("scenarios=%d", len(m.Scenarios)) | ||
| 87 | } | 89 | } | ||
| 88 | m.Scenarios = m.Scenarios[:len(m.Scenarios)-1] | 90 | m.Scenarios = m.Scenarios[:len(m.Scenarios)-1] | ||
| @@ -188,9 +190,9 @@ func TestHarnessUsesPublicRoleCommandsAndPTYWorkloads(t *testing.T) { | |||||
| 188 | {"udp_peer", []string{"_udp-proxy", "perf-s-001"}}, | 190 | {"udp_peer", []string{"_udp-proxy", "perf-s-001"}}, | ||
| 189 | } { | 191 | } { | ||
| 190 | t.Run(tc.role, func(t *testing.T) { | 192 | t.Run(tc.role, func(t *testing.T) { | ||
| 191 | - | got := roleArgs(scenario{ID: "s"}, processMapping{Role: tc.role, Run: 1}) | 193 | + | got := roleArgs(scenario{ID: "s", Transport: "local"}, processMapping{Role: tc.role, Run: 1}) |
| 192 | - | if !equalStrings(got, tc.want) { | 194 | + | if !equalStrings(got.Args, tc.want) { |
| 193 | - | t.Fatalf("args=%q want %q", got, tc.want) | 195 | + | t.Fatalf("args=%q want %q", got.Args, tc.want) |
| 194 | } | 196 | } | ||
| 195 | }) | 197 | }) | ||
| 196 | } | 198 | } | ||
| @@ -200,6 +202,118 @@ func TestHarnessUsesPublicRoleCommandsAndPTYWorkloads(t *testing.T) { | |||||
| 200 | } | 202 | } | ||
| 201 | } | 203 | } | ||
| 202 | 204 | ||||
| 205 | + | func TestHarnessRoutesEveryRemoteFixtureThroughItsDeclaredPeer(t *testing.T) { | |||
| 206 | + | cases := []struct { | |||
| 207 | + | name string | |||
| 208 | + | transport transport | |||
| 209 | + | peer string | |||
| 210 | + | wantRTT int | |||
| 211 | + | wantLoss int | |||
| 212 | + | }{ | |||
| 213 | + | {"ssh", transport{ID: "ssh_stdio", Kind: "ssh_stdio"}, "ssh_stdio_peer", 0, 0}, | |||
| 214 | + | {"udp baseline", transport{ID: "udp_baseline", Kind: "udp"}, "udp_peer", 0, 0}, | |||
| 215 | + | {"udp 25ms", transport{ID: "udp_25ms", Kind: "udp", RTTMS: 25}, "udp_peer", 25, 0}, | |||
| 216 | + | {"udp 100ms", transport{ID: "udp_100ms", Kind: "udp", RTTMS: 100}, "udp_peer", 100, 0}, | |||
| 217 | + | {"udp loss zero", transport{ID: "udp_loss_0pct", Kind: "udp", LossPercent: 0}, "udp_peer", 0, 0}, | |||
| 218 | + | {"udp loss", transport{ID: "udp_loss_1pct", Kind: "udp", LossPercent: 1}, "udp_peer", 0, 1}, | |||
| 219 | + | } | |||
| 220 | + | for _, tc := range cases { | |||
| 221 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 222 | + | s := scenario{ID: "route", Transport: tc.transport.ID} | |||
| 223 | + | client := routeRoleArgs(s, processMapping{Role: "client", Run: 2}, tc.transport) | |||
| 224 | + | peer := routeRoleArgs(s, processMapping{Role: tc.peer, Run: 2}, tc.transport) | |||
| 225 | + | if !equalStrings(client.Args, []string{"attach", "harness@127.0.0.1:perf-route-002"}) { | |||
| 226 | + | t.Fatalf("client did not use public remote attach: %q", client.Args) | |||
| 227 | + | } | |||
| 228 | + | if tc.peer == "ssh_stdio_peer" && !equalStrings(peer.Args, []string{"_stdio", "perf-route-002"}) { | |||
| 229 | + | t.Fatalf("ssh peer command=%q", peer.Args) | |||
| 230 | + | } | |||
| 231 | + | if tc.peer == "udp_peer" && !equalStrings(peer.Args, []string{"_udp-proxy", "perf-route-002"}) { | |||
| 232 | + | t.Fatalf("udp peer command=%q", peer.Args) | |||
| 233 | + | } | |||
| 234 | + | if peer.Transport.RTTMS != tc.wantRTT || peer.Transport.LossPercent != tc.wantLoss { | |||
| 235 | + | t.Fatalf("peer lost manifest network settings: %+v", peer.Transport) | |||
| 236 | + | } | |||
| 237 | + | }) | |||
| 238 | + | } | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func TestHarnessFakeRunnerRoutesClientToPeerAndCleansEveryRole(t *testing.T) { | |||
| 242 | + | for _, tc := range []struct { | |||
| 243 | + | name, transport, peer, peerCommand string | |||
| 244 | + | }{ | |||
| 245 | + | {"ssh", "ssh_stdio", "ssh_stdio_peer", "_stdio"}, | |||
| 246 | + | {"udp baseline", "udp_baseline", "udp_peer", "_udp-proxy"}, | |||
| 247 | + | {"udp 25ms", "udp_25ms", "udp_peer", "_udp-proxy"}, | |||
| 248 | + | {"udp 100ms", "udp_100ms", "udp_peer", "_udp-proxy"}, | |||
| 249 | + | {"udp loss zero", "udp_loss_0pct", "udp_peer", "_udp-proxy"}, | |||
| 250 | + | {"udp loss", "udp_loss_1pct", "udp_peer", "_udp-proxy"}, | |||
| 251 | + | } { | |||
| 252 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 253 | + | dir := t.TempDir() | |||
| 254 | + | raw, err := os.OpenFile(filepath.Join(dir, "raw.jsonl"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) | |||
| 255 | + | if err != nil { | |||
| 256 | + | t.Fatal(err) | |||
| 257 | + | } | |||
| 258 | + | defer raw.Close() | |||
| 259 | + | l := &fakeLauncher{} | |||
| 260 | + | h := defaultHarness() | |||
| 261 | + | h.clock, h.launcher = &fakeClock{}, l | |||
| 262 | + | s := scenario{ID: "routed", Transport: tc.transport, Roles: []string{"daemon", "client", tc.peer}} | |||
| 263 | + | if _, err := h.runOne(options{out: dir, warmup: time.Second, duration: minimumDuration, repetitions: minimumRepetitions}, manifest{}, s, 1, raw); err != nil { | |||
| 264 | + | t.Fatal(err) | |||
| 265 | + | } | |||
| 266 | + | if len(l.mappings) != 3 || l.mappings[1].Role != tc.peer || l.mappings[2].Role != "client" { | |||
| 267 | + | t.Fatalf("dependency launch order=%+v", l.mappings) | |||
| 268 | + | } | |||
| 269 | + | if l.commands[1].Args[0] != tc.peerCommand || l.commands[2].Args[0] != "attach" { | |||
| 270 | + | t.Fatalf("commands do not connect client through declared peer: %+v", l.commands) | |||
| 271 | + | } | |||
| 272 | + | for i, p := range l.process { | |||
| 273 | + | if !p.closed { | |||
| 274 | + | t.Fatalf("role %s was not cleaned up", l.mappings[i].Role) | |||
| 275 | + | } | |||
| 276 | + | } | |||
| 277 | + | }) | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | func TestCLITransportSeamOwnsExclusivePeerTraceAndCleanup(t *testing.T) { | |||
| 282 | + | dir := t.TempDir() | |||
| 283 | + | if err := os.Chmod(dir, 0o700); err != nil { | |||
| 284 | + | t.Fatal(err) | |||
| 285 | + | } | |||
| 286 | + | l := &cliLauncher{bin: "/bin/true"} | |||
| 287 | + | m := processMapping{ProcessID: "udp-peer", TracePath: filepath.Join(dir, "udp.jsonl"), Role: "udp_peer"} | |||
| 288 | + | if err := os.WriteFile(m.TracePath, nil, 0o600); err != nil { | |||
| 289 | + | t.Fatal(err) | |||
| 290 | + | } | |||
| 291 | + | p, err := l.preparePeer(m, roleCommand{Args: []string{"_udp-proxy", "work"}, Transport: transport{ID: "udp_25ms", Kind: "udp", RTTMS: 25}}) | |||
| 292 | + | if err != nil { | |||
| 293 | + | t.Fatal(err) | |||
| 294 | + | } | |||
| 295 | + | shim, err := os.ReadFile(filepath.Join(dir, "ssh")) | |||
| 296 | + | if err != nil { | |||
| 297 | + | t.Fatal(err) | |||
| 298 | + | } | |||
| 299 | + | text := string(shim) | |||
| 300 | + | for _, want := range []string{"_udp-proxy", m.TracePath, m.ProcessID, "VEV_PERF_UDP_RTT_MS=\"25\"", "udp-peer.pid"} { | |||
| 301 | + | if !strings.Contains(text, want) { | |||
| 302 | + | t.Errorf("seam does not retain %q:\n%s", want, text) | |||
| 303 | + | } | |||
| 304 | + | } | |||
| 305 | + | // A nonexistent pid is already-cleaned-up; Close must treat it as such. | |||
| 306 | + | if err := p.Close(); err != nil { | |||
| 307 | + | t.Fatal(err) | |||
| 308 | + | } | |||
| 309 | + | l.mu.Lock() | |||
| 310 | + | route := l.peers[dir] | |||
| 311 | + | l.mu.Unlock() | |||
| 312 | + | if route.mapping.TracePath != m.TracePath || route.mapping.ProcessID != m.ProcessID || route.command.Transport.RTTMS != 25 { | |||
| 313 | + | t.Fatalf("peer route is not exclusively correlated: %+v", route) | |||
| 314 | + | } | |||
| 315 | + | } | |||
| 316 | + | ||||
| 203 | func TestHarnessRejectsInvalidBoundaryPairs(t *testing.T) { | 317 | func TestHarnessRejectsInvalidBoundaryPairs(t *testing.T) { | ||
| 204 | for _, marks := range [][]harnessMark{ | 318 | for _, marks := range [][]harnessMark{ | ||
| 205 | {{Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}}, | 319 | {{Sequence: 1, Kind: "terminal_flushed", Tick: 1, Valid: true}}, | ||
| @@ -240,8 +354,8 @@ func TestHarnessCleansRunDirectoryOnProcessFailure(t *testing.T) { | |||||
| 240 | 354 | ||||
| 241 | type failingLauncher struct{ *fakeLauncher } | 355 | type failingLauncher struct{ *fakeLauncher } | ||
| 242 | 356 | ||||
| 243 | - | func (l *failingLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | 357 | + | func (l *failingLauncher) Launch(m processMapping, command roleCommand) (launchedProcess, error) { |
| 244 | - | p, err := l.fakeLauncher.Launch(m, args) | 358 | + | p, err := l.fakeLauncher.Launch(m, command) |
| 245 | if err != nil { | 359 | if err != nil { | ||
| 246 | return nil, err | 360 | return nil, err | ||
| 247 | } | 361 | } | ||
| @@ -280,8 +394,8 @@ func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) { | |||||
| 280 | 394 | ||||
| 281 | type traceLauncher struct{ fakeLauncher } | 395 | type traceLauncher struct{ fakeLauncher } | ||
| 282 | 396 | ||||
| 283 | - | func (l *traceLauncher) Launch(m processMapping, args []string) (launchedProcess, error) { | 397 | + | func (l *traceLauncher) Launch(m processMapping, command roleCommand) (launchedProcess, error) { |
| 284 | - | p, err := l.fakeLauncher.Launch(m, args) | 398 | + | p, err := l.fakeLauncher.Launch(m, command) |
| 285 | if err != nil { | 399 | if err != nil { | ||
| 286 | return nil, err | 400 | return nil, err | ||
| 287 | } | 401 | } | ||
M testdata/perf/README.md +2 -2
| @@ -2,8 +2,8 @@ | |||
| 2 | 2 | ||
| 3 | 3 | `manifest.json` is the complete, measurement-only public-CLI matrix: four | |
| 4 | 4 | 120x40 topologies (`1x4`, `4x1`, `4x4`, `8x1`), 10,000 history rows per pane, | |
| 5 | - | nine workloads, and six transports (local, SSH stdio, UDP baseline, UDP 25 ms, | |
| 6 | - | UDP 100 ms, and UDP with 1% loss). It contains 216 scenario combinations; | |
| 5 | + | nine workloads, and seven transports (local, SSH stdio, UDP baseline, UDP 25 ms, | |
| 6 | + | UDP 100 ms, and UDP with 0% or 1% loss). It contains 252 scenario combinations; | |
| 7 | 7 | there are no omitted combinations. A genuinely unavailable combination must | |
| 8 | 8 | remain listed with `inapplicable_reason` and no launched roles. | |
| 9 | 9 | ||
| @@ -2,8 +2,8 @@ | |||||
| 2 | 2 | ||||
| 3 | `manifest.json` is the complete, measurement-only public-CLI matrix: four | 3 | `manifest.json` is the complete, measurement-only public-CLI matrix: four | ||
| 4 | 120x40 topologies (`1x4`, `4x1`, `4x4`, `8x1`), 10,000 history rows per pane, | 4 | 120x40 topologies (`1x4`, `4x1`, `4x4`, `8x1`), 10,000 history rows per pane, | ||
| 5 | - | nine workloads, and six transports (local, SSH stdio, UDP baseline, UDP 25 ms, | 5 | + | nine workloads, and seven transports (local, SSH stdio, UDP baseline, UDP 25 ms, |
| 6 | - | UDP 100 ms, and UDP with 1% loss). It contains 216 scenario combinations; | 6 | + | UDP 100 ms, and UDP with 0% or 1% loss). It contains 252 scenario combinations; |
| 7 | there are no omitted combinations. A genuinely unavailable combination must | 7 | there are no omitted combinations. A genuinely unavailable combination must | ||
| 8 | remain listed with `inapplicable_reason` and no launched roles. | 8 | remain listed with `inapplicable_reason` and no launched roles. | ||
| 9 | 9 | ||||
M testdata/perf/manifest.json +401 -0
| @@ -46,6 +46,11 @@ | |||
| 46 | 46 | "id": "udp_baseline", | |
| 47 | 47 | "kind": "udp" | |
| 48 | 48 | }, | |
| 49 | + | { | |
| 50 | + | "id": "udp_loss_0pct", | |
| 51 | + | "kind": "udp", | |
| 52 | + | "loss_percent": 0 | |
| 53 | + | }, | |
| 49 | 54 | { | |
| 50 | 55 | "id": "udp_25ms", | |
| 51 | 56 | "kind": "udp", | |
| @@ -2402,6 +2407,402 @@ | |||
| 2402 | 2407 | "client", | |
| 2403 | 2408 | "udp_peer" | |
| 2404 | 2409 | ] | |
| 2410 | + | }, | |
| 2411 | + | { | |
| 2412 | + | "id": "1x4-idle-udp_loss_0pct", | |
| 2413 | + | "topology": "1x4", | |
| 2414 | + | "workload": "idle", | |
| 2415 | + | "transport": "udp_loss_0pct", | |
| 2416 | + | "roles": [ | |
| 2417 | + | "daemon", | |
| 2418 | + | "client", | |
| 2419 | + | "udp_peer" | |
| 2420 | + | ] | |
| 2421 | + | }, | |
| 2422 | + | { | |
| 2423 | + | "id": "1x4-active_output-udp_loss_0pct", | |
| 2424 | + | "topology": "1x4", | |
| 2425 | + | "workload": "active_output", | |
| 2426 | + | "transport": "udp_loss_0pct", | |
| 2427 | + | "roles": [ | |
| 2428 | + | "daemon", | |
| 2429 | + | "client", | |
| 2430 | + | "udp_peer" | |
| 2431 | + | ] | |
| 2432 | + | }, | |
| 2433 | + | { | |
| 2434 | + | "id": "1x4-all_output-udp_loss_0pct", | |
| 2435 | + | "topology": "1x4", | |
| 2436 | + | "workload": "all_output", | |
| 2437 | + | "transport": "udp_loss_0pct", | |
| 2438 | + | "roles": [ | |
| 2439 | + | "daemon", | |
| 2440 | + | "client", | |
| 2441 | + | "udp_peer" | |
| 2442 | + | ] | |
| 2443 | + | }, | |
| 2444 | + | { | |
| 2445 | + | "id": "1x4-inactive_output-udp_loss_0pct", | |
| 2446 | + | "topology": "1x4", | |
| 2447 | + | "workload": "inactive_output", | |
| 2448 | + | "transport": "udp_loss_0pct", | |
| 2449 | + | "roles": [ | |
| 2450 | + | "daemon", | |
| 2451 | + | "client", | |
| 2452 | + | "udp_peer" | |
| 2453 | + | ] | |
| 2454 | + | }, | |
| 2455 | + | { | |
| 2456 | + | "id": "1x4-interactive_flood-udp_loss_0pct", | |
| 2457 | + | "topology": "1x4", | |
| 2458 | + | "workload": "interactive_flood", | |
| 2459 | + | "transport": "udp_loss_0pct", | |
| 2460 | + | "roles": [ | |
| 2461 | + | "daemon", | |
| 2462 | + | "client", | |
| 2463 | + | "udp_peer" | |
| 2464 | + | ] | |
| 2465 | + | }, | |
| 2466 | + | { | |
| 2467 | + | "id": "1x4-copy_search-udp_loss_0pct", | |
| 2468 | + | "topology": "1x4", | |
| 2469 | + | "workload": "copy_search", | |
| 2470 | + | "transport": "udp_loss_0pct", | |
| 2471 | + | "roles": [ | |
| 2472 | + | "daemon", | |
| 2473 | + | "client", | |
| 2474 | + | "udp_peer" | |
| 2475 | + | ] | |
| 2476 | + | }, | |
| 2477 | + | { | |
| 2478 | + | "id": "1x4-resize_sweep-udp_loss_0pct", | |
| 2479 | + | "topology": "1x4", | |
| 2480 | + | "workload": "resize_sweep", | |
| 2481 | + | "transport": "udp_loss_0pct", | |
| 2482 | + | "roles": [ | |
| 2483 | + | "daemon", | |
| 2484 | + | "client", | |
| 2485 | + | "udp_peer" | |
| 2486 | + | ] | |
| 2487 | + | }, | |
| 2488 | + | { | |
| 2489 | + | "id": "1x4-snapshot_output_resize-udp_loss_0pct", | |
| 2490 | + | "topology": "1x4", | |
| 2491 | + | "workload": "snapshot_output_resize", | |
| 2492 | + | "transport": "udp_loss_0pct", | |
| 2493 | + | "roles": [ | |
| 2494 | + | "daemon", | |
| 2495 | + | "client", | |
| 2496 | + | "udp_peer" | |
| 2497 | + | ] | |
| 2498 | + | }, | |
| 2499 | + | { | |
| 2500 | + | "id": "1x4-attach_restore_tab_switch-udp_loss_0pct", | |
| 2501 | + | "topology": "1x4", | |
| 2502 | + | "workload": "attach_restore_tab_switch", | |
| 2503 | + | "transport": "udp_loss_0pct", | |
| 2504 | + | "roles": [ | |
| 2505 | + | "daemon", | |
| 2506 | + | "client", | |
| 2507 | + | "udp_peer" | |
| 2508 | + | ] | |
| 2509 | + | }, | |
| 2510 | + | { | |
| 2511 | + | "id": "4x1-idle-udp_loss_0pct", | |
| 2512 | + | "topology": "4x1", | |
| 2513 | + | "workload": "idle", | |
| 2514 | + | "transport": "udp_loss_0pct", | |
| 2515 | + | "roles": [ | |
| 2516 | + | "daemon", | |
| 2517 | + | "client", | |
| 2518 | + | "udp_peer" | |
| 2519 | + | ] | |
| 2520 | + | }, | |
| 2521 | + | { | |
| 2522 | + | "id": "4x1-active_output-udp_loss_0pct", | |
| 2523 | + | "topology": "4x1", | |
| 2524 | + | "workload": "active_output", | |
| 2525 | + | "transport": "udp_loss_0pct", | |
| 2526 | + | "roles": [ | |
| 2527 | + | "daemon", | |
| 2528 | + | "client", | |
| 2529 | + | "udp_peer" | |
| 2530 | + | ] | |
| 2531 | + | }, | |
| 2532 | + | { | |
| 2533 | + | "id": "4x1-all_output-udp_loss_0pct", | |
| 2534 | + | "topology": "4x1", | |
| 2535 | + | "workload": "all_output", | |
| 2536 | + | "transport": "udp_loss_0pct", | |
| 2537 | + | "roles": [ | |
| 2538 | + | "daemon", | |
| 2539 | + | "client", | |
| 2540 | + | "udp_peer" | |
| 2541 | + | ] | |
| 2542 | + | }, | |
| 2543 | + | { | |
| 2544 | + | "id": "4x1-inactive_output-udp_loss_0pct", | |
| 2545 | + | "topology": "4x1", | |
| 2546 | + | "workload": "inactive_output", | |
| 2547 | + | "transport": "udp_loss_0pct", | |
| 2548 | + | "roles": [ | |
| 2549 | + | "daemon", | |
| 2550 | + | "client", | |
| 2551 | + | "udp_peer" | |
| 2552 | + | ] | |
| 2553 | + | }, | |
| 2554 | + | { | |
| 2555 | + | "id": "4x1-interactive_flood-udp_loss_0pct", | |
| 2556 | + | "topology": "4x1", | |
| 2557 | + | "workload": "interactive_flood", | |
| 2558 | + | "transport": "udp_loss_0pct", | |
| 2559 | + | "roles": [ | |
| 2560 | + | "daemon", | |
| 2561 | + | "client", | |
| 2562 | + | "udp_peer" | |
| 2563 | + | ] | |
| 2564 | + | }, | |
| 2565 | + | { | |
| 2566 | + | "id": "4x1-copy_search-udp_loss_0pct", | |
| 2567 | + | "topology": "4x1", | |
| 2568 | + | "workload": "copy_search", | |
| 2569 | + | "transport": "udp_loss_0pct", | |
| 2570 | + | "roles": [ | |
| 2571 | + | "daemon", | |
| 2572 | + | "client", | |
| 2573 | + | "udp_peer" | |
| 2574 | + | ] | |
| 2575 | + | }, | |
| 2576 | + | { | |
| 2577 | + | "id": "4x1-resize_sweep-udp_loss_0pct", | |
| 2578 | + | "topology": "4x1", | |
| 2579 | + | "workload": "resize_sweep", | |
| 2580 | + | "transport": "udp_loss_0pct", | |
| 2581 | + | "roles": [ | |
| 2582 | + | "daemon", | |
| 2583 | + | "client", | |
| 2584 | + | "udp_peer" | |
| 2585 | + | ] | |
| 2586 | + | }, | |
| 2587 | + | { | |
| 2588 | + | "id": "4x1-snapshot_output_resize-udp_loss_0pct", | |
| 2589 | + | "topology": "4x1", | |
| 2590 | + | "workload": "snapshot_output_resize", | |
| 2591 | + | "transport": "udp_loss_0pct", | |
| 2592 | + | "roles": [ | |
| 2593 | + | "daemon", | |
| 2594 | + | "client", | |
| 2595 | + | "udp_peer" | |
| 2596 | + | ] | |
| 2597 | + | }, | |
| 2598 | + | { | |
| 2599 | + | "id": "4x1-attach_restore_tab_switch-udp_loss_0pct", | |
| 2600 | + | "topology": "4x1", | |
| 2601 | + | "workload": "attach_restore_tab_switch", | |
| 2602 | + | "transport": "udp_loss_0pct", | |
| 2603 | + | "roles": [ | |
| 2604 | + | "daemon", | |
| 2605 | + | "client", | |
| 2606 | + | "udp_peer" | |
| 2607 | + | ] | |
| 2608 | + | }, | |
| 2609 | + | { | |
| 2610 | + | "id": "4x4-idle-udp_loss_0pct", | |
| 2611 | + | "topology": "4x4", | |
| 2612 | + | "workload": "idle", | |
| 2613 | + | "transport": "udp_loss_0pct", | |
| 2614 | + | "roles": [ | |
| 2615 | + | "daemon", | |
| 2616 | + | "client", | |
| 2617 | + | "udp_peer" | |
| 2618 | + | ] | |
| 2619 | + | }, | |
| 2620 | + | { | |
| 2621 | + | "id": "4x4-active_output-udp_loss_0pct", | |
| 2622 | + | "topology": "4x4", | |
| 2623 | + | "workload": "active_output", | |
| 2624 | + | "transport": "udp_loss_0pct", | |
| 2625 | + | "roles": [ | |
| 2626 | + | "daemon", | |
| 2627 | + | "client", | |
| 2628 | + | "udp_peer" | |
| 2629 | + | ] | |
| 2630 | + | }, | |
| 2631 | + | { | |
| 2632 | + | "id": "4x4-all_output-udp_loss_0pct", | |
| 2633 | + | "topology": "4x4", | |
| 2634 | + | "workload": "all_output", | |
| 2635 | + | "transport": "udp_loss_0pct", | |
| 2636 | + | "roles": [ | |
| 2637 | + | "daemon", | |
| 2638 | + | "client", | |
| 2639 | + | "udp_peer" | |
| 2640 | + | ] | |
| 2641 | + | }, | |
| 2642 | + | { | |
| 2643 | + | "id": "4x4-inactive_output-udp_loss_0pct", | |
| 2644 | + | "topology": "4x4", | |
| 2645 | + | "workload": "inactive_output", | |
| 2646 | + | "transport": "udp_loss_0pct", | |
| 2647 | + | "roles": [ | |
| 2648 | + | "daemon", | |
| 2649 | + | "client", | |
| 2650 | + | "udp_peer" | |
| 2651 | + | ] | |
| 2652 | + | }, | |
| 2653 | + | { | |
| 2654 | + | "id": "4x4-interactive_flood-udp_loss_0pct", | |
| 2655 | + | "topology": "4x4", | |
| 2656 | + | "workload": "interactive_flood", | |
| 2657 | + | "transport": "udp_loss_0pct", | |
| 2658 | + | "roles": [ | |
| 2659 | + | "daemon", | |
| 2660 | + | "client", | |
| 2661 | + | "udp_peer" | |
| 2662 | + | ] | |
| 2663 | + | }, | |
| 2664 | + | { | |
| 2665 | + | "id": "4x4-copy_search-udp_loss_0pct", | |
| 2666 | + | "topology": "4x4", | |
| 2667 | + | "workload": "copy_search", | |
| 2668 | + | "transport": "udp_loss_0pct", | |
| 2669 | + | "roles": [ | |
| 2670 | + | "daemon", | |
| 2671 | + | "client", | |
| 2672 | + | "udp_peer" | |
| 2673 | + | ] | |
| 2674 | + | }, | |
| 2675 | + | { | |
| 2676 | + | "id": "4x4-resize_sweep-udp_loss_0pct", | |
| 2677 | + | "topology": "4x4", | |
| 2678 | + | "workload": "resize_sweep", | |
| 2679 | + | "transport": "udp_loss_0pct", | |
| 2680 | + | "roles": [ | |
| 2681 | + | "daemon", | |
| 2682 | + | "client", | |
| 2683 | + | "udp_peer" | |
| 2684 | + | ] | |
| 2685 | + | }, | |
| 2686 | + | { | |
| 2687 | + | "id": "4x4-snapshot_output_resize-udp_loss_0pct", | |
| 2688 | + | "topology": "4x4", | |
| 2689 | + | "workload": "snapshot_output_resize", | |
| 2690 | + | "transport": "udp_loss_0pct", | |
| 2691 | + | "roles": [ | |
| 2692 | + | "daemon", | |
| 2693 | + | "client", | |
| 2694 | + | "udp_peer" | |
| 2695 | + | ] | |
| 2696 | + | }, | |
| 2697 | + | { | |
| 2698 | + | "id": "4x4-attach_restore_tab_switch-udp_loss_0pct", | |
| 2699 | + | "topology": "4x4", | |
| 2700 | + | "workload": "attach_restore_tab_switch", | |
| 2701 | + | "transport": "udp_loss_0pct", | |
| 2702 | + | "roles": [ | |
| 2703 | + | "daemon", | |
| 2704 | + | "client", | |
| 2705 | + | "udp_peer" | |
| 2706 | + | ] | |
| 2707 | + | }, | |
| 2708 | + | { | |
| 2709 | + | "id": "8x1-idle-udp_loss_0pct", | |
| 2710 | + | "topology": "8x1", | |
| 2711 | + | "workload": "idle", | |
| 2712 | + | "transport": "udp_loss_0pct", | |
| 2713 | + | "roles": [ | |
| 2714 | + | "daemon", | |
| 2715 | + | "client", | |
| 2716 | + | "udp_peer" | |
| 2717 | + | ] | |
| 2718 | + | }, | |
| 2719 | + | { | |
| 2720 | + | "id": "8x1-active_output-udp_loss_0pct", | |
| 2721 | + | "topology": "8x1", | |
| 2722 | + | "workload": "active_output", | |
| 2723 | + | "transport": "udp_loss_0pct", | |
| 2724 | + | "roles": [ | |
| 2725 | + | "daemon", | |
| 2726 | + | "client", | |
| 2727 | + | "udp_peer" | |
| 2728 | + | ] | |
| 2729 | + | }, | |
| 2730 | + | { | |
| 2731 | + | "id": "8x1-all_output-udp_loss_0pct", | |
| 2732 | + | "topology": "8x1", | |
| 2733 | + | "workload": "all_output", | |
| 2734 | + | "transport": "udp_loss_0pct", | |
| 2735 | + | "roles": [ | |
| 2736 | + | "daemon", | |
| 2737 | + | "client", | |
| 2738 | + | "udp_peer" | |
| 2739 | + | ] | |
| 2740 | + | }, | |
| 2741 | + | { | |
| 2742 | + | "id": "8x1-inactive_output-udp_loss_0pct", | |
| 2743 | + | "topology": "8x1", | |
| 2744 | + | "workload": "inactive_output", | |
| 2745 | + | "transport": "udp_loss_0pct", | |
| 2746 | + | "roles": [ | |
| 2747 | + | "daemon", | |
| 2748 | + | "client", | |
| 2749 | + | "udp_peer" | |
| 2750 | + | ] | |
| 2751 | + | }, | |
| 2752 | + | { | |
| 2753 | + | "id": "8x1-interactive_flood-udp_loss_0pct", | |
| 2754 | + | "topology": "8x1", | |
| 2755 | + | "workload": "interactive_flood", | |
| 2756 | + | "transport": "udp_loss_0pct", | |
| 2757 | + | "roles": [ | |
| 2758 | + | "daemon", | |
| 2759 | + | "client", | |
| 2760 | + | "udp_peer" | |
| 2761 | + | ] | |
| 2762 | + | }, | |
| 2763 | + | { | |
| 2764 | + | "id": "8x1-copy_search-udp_loss_0pct", | |
| 2765 | + | "topology": "8x1", | |
| 2766 | + | "workload": "copy_search", | |
| 2767 | + | "transport": "udp_loss_0pct", | |
| 2768 | + | "roles": [ | |
| 2769 | + | "daemon", | |
| 2770 | + | "client", | |
| 2771 | + | "udp_peer" | |
| 2772 | + | ] | |
| 2773 | + | }, | |
| 2774 | + | { | |
| 2775 | + | "id": "8x1-resize_sweep-udp_loss_0pct", | |
| 2776 | + | "topology": "8x1", | |
| 2777 | + | "workload": "resize_sweep", | |
| 2778 | + | "transport": "udp_loss_0pct", | |
| 2779 | + | "roles": [ | |
| 2780 | + | "daemon", | |
| 2781 | + | "client", | |
| 2782 | + | "udp_peer" | |
| 2783 | + | ] | |
| 2784 | + | }, | |
| 2785 | + | { | |
| 2786 | + | "id": "8x1-snapshot_output_resize-udp_loss_0pct", | |
| 2787 | + | "topology": "8x1", | |
| 2788 | + | "workload": "snapshot_output_resize", | |
| 2789 | + | "transport": "udp_loss_0pct", | |
| 2790 | + | "roles": [ | |
| 2791 | + | "daemon", | |
| 2792 | + | "client", | |
| 2793 | + | "udp_peer" | |
| 2794 | + | ] | |
| 2795 | + | }, | |
| 2796 | + | { | |
| 2797 | + | "id": "8x1-attach_restore_tab_switch-udp_loss_0pct", | |
| 2798 | + | "topology": "8x1", | |
| 2799 | + | "workload": "attach_restore_tab_switch", | |
| 2800 | + | "transport": "udp_loss_0pct", | |
| 2801 | + | "roles": [ | |
| 2802 | + | "daemon", | |
| 2803 | + | "client", | |
| 2804 | + | "udp_peer" | |
| 2805 | + | ] | |
| 2405 | 2806 | } | |
| 2406 | 2807 | ] | |
| 2407 | 2808 | } | |
| @@ -46,6 +46,11 @@ | |||||
| 46 | "id": "udp_baseline", | 46 | "id": "udp_baseline", | ||
| 47 | "kind": "udp" | 47 | "kind": "udp" | ||
| 48 | }, | 48 | }, | ||
| 49 | + | { | |||
| 50 | + | "id": "udp_loss_0pct", | |||
| 51 | + | "kind": "udp", | |||
| 52 | + | "loss_percent": 0 | |||
| 53 | + | }, | |||
| 49 | { | 54 | { | ||
| 50 | "id": "udp_25ms", | 55 | "id": "udp_25ms", | ||
| 51 | "kind": "udp", | 56 | "kind": "udp", | ||
| @@ -2402,6 +2407,402 @@ | |||||
| 2402 | "client", | 2407 | "client", | ||
| 2403 | "udp_peer" | 2408 | "udp_peer" | ||
| 2404 | ] | 2409 | ] | ||
| 2410 | + | }, | |||
| 2411 | + | { | |||
| 2412 | + | "id": "1x4-idle-udp_loss_0pct", | |||
| 2413 | + | "topology": "1x4", | |||
| 2414 | + | "workload": "idle", | |||
| 2415 | + | "transport": "udp_loss_0pct", | |||
| 2416 | + | "roles": [ | |||
| 2417 | + | "daemon", | |||
| 2418 | + | "client", | |||
| 2419 | + | "udp_peer" | |||
| 2420 | + | ] | |||
| 2421 | + | }, | |||
| 2422 | + | { | |||
| 2423 | + | "id": "1x4-active_output-udp_loss_0pct", | |||
| 2424 | + | "topology": "1x4", | |||
| 2425 | + | "workload": "active_output", | |||
| 2426 | + | "transport": "udp_loss_0pct", | |||
| 2427 | + | "roles": [ | |||
| 2428 | + | "daemon", | |||
| 2429 | + | "client", | |||
| 2430 | + | "udp_peer" | |||
| 2431 | + | ] | |||
| 2432 | + | }, | |||
| 2433 | + | { | |||
| 2434 | + | "id": "1x4-all_output-udp_loss_0pct", | |||
| 2435 | + | "topology": "1x4", | |||
| 2436 | + | "workload": "all_output", | |||
| 2437 | + | "transport": "udp_loss_0pct", | |||
| 2438 | + | "roles": [ | |||
| 2439 | + | "daemon", | |||
| 2440 | + | "client", | |||
| 2441 | + | "udp_peer" | |||
| 2442 | + | ] | |||
| 2443 | + | }, | |||
| 2444 | + | { | |||
| 2445 | + | "id": "1x4-inactive_output-udp_loss_0pct", | |||
| 2446 | + | "topology": "1x4", | |||
| 2447 | + | "workload": "inactive_output", | |||
| 2448 | + | "transport": "udp_loss_0pct", | |||
| 2449 | + | "roles": [ | |||
| 2450 | + | "daemon", | |||
| 2451 | + | "client", | |||
| 2452 | + | "udp_peer" | |||
| 2453 | + | ] | |||
| 2454 | + | }, | |||
| 2455 | + | { | |||
| 2456 | + | "id": "1x4-interactive_flood-udp_loss_0pct", | |||
| 2457 | + | "topology": "1x4", | |||
| 2458 | + | "workload": "interactive_flood", | |||
| 2459 | + | "transport": "udp_loss_0pct", | |||
| 2460 | + | "roles": [ | |||
| 2461 | + | "daemon", | |||
| 2462 | + | "client", | |||
| 2463 | + | "udp_peer" | |||
| 2464 | + | ] | |||
| 2465 | + | }, | |||
| 2466 | + | { | |||
| 2467 | + | "id": "1x4-copy_search-udp_loss_0pct", | |||
| 2468 | + | "topology": "1x4", | |||
| 2469 | + | "workload": "copy_search", | |||
| 2470 | + | "transport": "udp_loss_0pct", | |||
| 2471 | + | "roles": [ | |||
| 2472 | + | "daemon", | |||
| 2473 | + | "client", | |||
| 2474 | + | "udp_peer" | |||
| 2475 | + | ] | |||
| 2476 | + | }, | |||
| 2477 | + | { | |||
| 2478 | + | "id": "1x4-resize_sweep-udp_loss_0pct", | |||
| 2479 | + | "topology": "1x4", | |||
| 2480 | + | "workload": "resize_sweep", | |||
| 2481 | + | "transport": "udp_loss_0pct", | |||
| 2482 | + | "roles": [ | |||
| 2483 | + | "daemon", | |||
| 2484 | + | "client", | |||
| 2485 | + | "udp_peer" | |||
| 2486 | + | ] | |||
| 2487 | + | }, | |||
| 2488 | + | { | |||
| 2489 | + | "id": "1x4-snapshot_output_resize-udp_loss_0pct", | |||
| 2490 | + | "topology": "1x4", | |||
| 2491 | + | "workload": "snapshot_output_resize", | |||
| 2492 | + | "transport": "udp_loss_0pct", | |||
| 2493 | + | "roles": [ | |||
| 2494 | + | "daemon", | |||
| 2495 | + | "client", | |||
| 2496 | + | "udp_peer" | |||
| 2497 | + | ] | |||
| 2498 | + | }, | |||
| 2499 | + | { | |||
| 2500 | + | "id": "1x4-attach_restore_tab_switch-udp_loss_0pct", | |||
| 2501 | + | "topology": "1x4", | |||
| 2502 | + | "workload": "attach_restore_tab_switch", | |||
| 2503 | + | "transport": "udp_loss_0pct", | |||
| 2504 | + | "roles": [ | |||
| 2505 | + | "daemon", | |||
| 2506 | + | "client", | |||
| 2507 | + | "udp_peer" | |||
| 2508 | + | ] | |||
| 2509 | + | }, | |||
| 2510 | + | { | |||
| 2511 | + | "id": "4x1-idle-udp_loss_0pct", | |||
| 2512 | + | "topology": "4x1", | |||
| 2513 | + | "workload": "idle", | |||
| 2514 | + | "transport": "udp_loss_0pct", | |||
| 2515 | + | "roles": [ | |||
| 2516 | + | "daemon", | |||
| 2517 | + | "client", | |||
| 2518 | + | "udp_peer" | |||
| 2519 | + | ] | |||
| 2520 | + | }, | |||
| 2521 | + | { | |||
| 2522 | + | "id": "4x1-active_output-udp_loss_0pct", | |||
| 2523 | + | "topology": "4x1", | |||
| 2524 | + | "workload": "active_output", | |||
| 2525 | + | "transport": "udp_loss_0pct", | |||
| 2526 | + | "roles": [ | |||
| 2527 | + | "daemon", | |||
| 2528 | + | "client", | |||
| 2529 | + | "udp_peer" | |||
| 2530 | + | ] | |||
| 2531 | + | }, | |||
| 2532 | + | { | |||
| 2533 | + | "id": "4x1-all_output-udp_loss_0pct", | |||
| 2534 | + | "topology": "4x1", | |||
| 2535 | + | "workload": "all_output", | |||
| 2536 | + | "transport": "udp_loss_0pct", | |||
| 2537 | + | "roles": [ | |||
| 2538 | + | "daemon", | |||
| 2539 | + | "client", | |||
| 2540 | + | "udp_peer" | |||
| 2541 | + | ] | |||
| 2542 | + | }, | |||
| 2543 | + | { | |||
| 2544 | + | "id": "4x1-inactive_output-udp_loss_0pct", | |||
| 2545 | + | "topology": "4x1", | |||
| 2546 | + | "workload": "inactive_output", | |||
| 2547 | + | "transport": "udp_loss_0pct", | |||
| 2548 | + | "roles": [ | |||
| 2549 | + | "daemon", | |||
| 2550 | + | "client", | |||
| 2551 | + | "udp_peer" | |||
| 2552 | + | ] | |||
| 2553 | + | }, | |||
| 2554 | + | { | |||
| 2555 | + | "id": "4x1-interactive_flood-udp_loss_0pct", | |||
| 2556 | + | "topology": "4x1", | |||
| 2557 | + | "workload": "interactive_flood", | |||
| 2558 | + | "transport": "udp_loss_0pct", | |||
| 2559 | + | "roles": [ | |||
| 2560 | + | "daemon", | |||
| 2561 | + | "client", | |||
| 2562 | + | "udp_peer" | |||
| 2563 | + | ] | |||
| 2564 | + | }, | |||
| 2565 | + | { | |||
| 2566 | + | "id": "4x1-copy_search-udp_loss_0pct", | |||
| 2567 | + | "topology": "4x1", | |||
| 2568 | + | "workload": "copy_search", | |||
| 2569 | + | "transport": "udp_loss_0pct", | |||
| 2570 | + | "roles": [ | |||
| 2571 | + | "daemon", | |||
| 2572 | + | "client", | |||
| 2573 | + | "udp_peer" | |||
| 2574 | + | ] | |||
| 2575 | + | }, | |||
| 2576 | + | { | |||
| 2577 | + | "id": "4x1-resize_sweep-udp_loss_0pct", | |||
| 2578 | + | "topology": "4x1", | |||
| 2579 | + | "workload": "resize_sweep", | |||
| 2580 | + | "transport": "udp_loss_0pct", | |||
| 2581 | + | "roles": [ | |||
| 2582 | + | "daemon", | |||
| 2583 | + | "client", | |||
| 2584 | + | "udp_peer" | |||
| 2585 | + | ] | |||
| 2586 | + | }, | |||
| 2587 | + | { | |||
| 2588 | + | "id": "4x1-snapshot_output_resize-udp_loss_0pct", | |||
| 2589 | + | "topology": "4x1", | |||
| 2590 | + | "workload": "snapshot_output_resize", | |||
| 2591 | + | "transport": "udp_loss_0pct", | |||
| 2592 | + | "roles": [ | |||
| 2593 | + | "daemon", | |||
| 2594 | + | "client", | |||
| 2595 | + | "udp_peer" | |||
| 2596 | + | ] | |||
| 2597 | + | }, | |||
| 2598 | + | { | |||
| 2599 | + | "id": "4x1-attach_restore_tab_switch-udp_loss_0pct", | |||
| 2600 | + | "topology": "4x1", | |||
| 2601 | + | "workload": "attach_restore_tab_switch", | |||
| 2602 | + | "transport": "udp_loss_0pct", | |||
| 2603 | + | "roles": [ | |||
| 2604 | + | "daemon", | |||
| 2605 | + | "client", | |||
| 2606 | + | "udp_peer" | |||
| 2607 | + | ] | |||
| 2608 | + | }, | |||
| 2609 | + | { | |||
| 2610 | + | "id": "4x4-idle-udp_loss_0pct", | |||
| 2611 | + | "topology": "4x4", | |||
| 2612 | + | "workload": "idle", | |||
| 2613 | + | "transport": "udp_loss_0pct", | |||
| 2614 | + | "roles": [ | |||
| 2615 | + | "daemon", | |||
| 2616 | + | "client", | |||
| 2617 | + | "udp_peer" | |||
| 2618 | + | ] | |||
| 2619 | + | }, | |||
| 2620 | + | { | |||
| 2621 | + | "id": "4x4-active_output-udp_loss_0pct", | |||
| 2622 | + | "topology": "4x4", | |||
| 2623 | + | "workload": "active_output", | |||
| 2624 | + | "transport": "udp_loss_0pct", | |||
| 2625 | + | "roles": [ | |||
| 2626 | + | "daemon", | |||
| 2627 | + | "client", | |||
| 2628 | + | "udp_peer" | |||
| 2629 | + | ] | |||
| 2630 | + | }, | |||
| 2631 | + | { | |||
| 2632 | + | "id": "4x4-all_output-udp_loss_0pct", | |||
| 2633 | + | "topology": "4x4", | |||
| 2634 | + | "workload": "all_output", | |||
| 2635 | + | "transport": "udp_loss_0pct", | |||
| 2636 | + | "roles": [ | |||
| 2637 | + | "daemon", | |||
| 2638 | + | "client", | |||
| 2639 | + | "udp_peer" | |||
| 2640 | + | ] | |||
| 2641 | + | }, | |||
| 2642 | + | { | |||
| 2643 | + | "id": "4x4-inactive_output-udp_loss_0pct", | |||
| 2644 | + | "topology": "4x4", | |||
| 2645 | + | "workload": "inactive_output", | |||
| 2646 | + | "transport": "udp_loss_0pct", | |||
| 2647 | + | "roles": [ | |||
| 2648 | + | "daemon", | |||
| 2649 | + | "client", | |||
| 2650 | + | "udp_peer" | |||
| 2651 | + | ] | |||
| 2652 | + | }, | |||
| 2653 | + | { | |||
| 2654 | + | "id": "4x4-interactive_flood-udp_loss_0pct", | |||
| 2655 | + | "topology": "4x4", | |||
| 2656 | + | "workload": "interactive_flood", | |||
| 2657 | + | "transport": "udp_loss_0pct", | |||
| 2658 | + | "roles": [ | |||
| 2659 | + | "daemon", | |||
| 2660 | + | "client", | |||
| 2661 | + | "udp_peer" | |||
| 2662 | + | ] | |||
| 2663 | + | }, | |||
| 2664 | + | { | |||
| 2665 | + | "id": "4x4-copy_search-udp_loss_0pct", | |||
| 2666 | + | "topology": "4x4", | |||
| 2667 | + | "workload": "copy_search", | |||
| 2668 | + | "transport": "udp_loss_0pct", | |||
| 2669 | + | "roles": [ | |||
| 2670 | + | "daemon", | |||
| 2671 | + | "client", | |||
| 2672 | + | "udp_peer" | |||
| 2673 | + | ] | |||
| 2674 | + | }, | |||
| 2675 | + | { | |||
| 2676 | + | "id": "4x4-resize_sweep-udp_loss_0pct", | |||
| 2677 | + | "topology": "4x4", | |||
| 2678 | + | "workload": "resize_sweep", | |||
| 2679 | + | "transport": "udp_loss_0pct", | |||
| 2680 | + | "roles": [ | |||
| 2681 | + | "daemon", | |||
| 2682 | + | "client", | |||
| 2683 | + | "udp_peer" | |||
| 2684 | + | ] | |||
| 2685 | + | }, | |||
| 2686 | + | { | |||
| 2687 | + | "id": "4x4-snapshot_output_resize-udp_loss_0pct", | |||
| 2688 | + | "topology": "4x4", | |||
| 2689 | + | "workload": "snapshot_output_resize", | |||
| 2690 | + | "transport": "udp_loss_0pct", | |||
| 2691 | + | "roles": [ | |||
| 2692 | + | "daemon", | |||
| 2693 | + | "client", | |||
| 2694 | + | "udp_peer" | |||
| 2695 | + | ] | |||
| 2696 | + | }, | |||
| 2697 | + | { | |||
| 2698 | + | "id": "4x4-attach_restore_tab_switch-udp_loss_0pct", | |||
| 2699 | + | "topology": "4x4", | |||
| 2700 | + | "workload": "attach_restore_tab_switch", | |||
| 2701 | + | "transport": "udp_loss_0pct", | |||
| 2702 | + | "roles": [ | |||
| 2703 | + | "daemon", | |||
| 2704 | + | "client", | |||
| 2705 | + | "udp_peer" | |||
| 2706 | + | ] | |||
| 2707 | + | }, | |||
| 2708 | + | { | |||
| 2709 | + | "id": "8x1-idle-udp_loss_0pct", | |||
| 2710 | + | "topology": "8x1", | |||
| 2711 | + | "workload": "idle", | |||
| 2712 | + | "transport": "udp_loss_0pct", | |||
| 2713 | + | "roles": [ | |||
| 2714 | + | "daemon", | |||
| 2715 | + | "client", | |||
| 2716 | + | "udp_peer" | |||
| 2717 | + | ] | |||
| 2718 | + | }, | |||
| 2719 | + | { | |||
| 2720 | + | "id": "8x1-active_output-udp_loss_0pct", | |||
| 2721 | + | "topology": "8x1", | |||
| 2722 | + | "workload": "active_output", | |||
| 2723 | + | "transport": "udp_loss_0pct", | |||
| 2724 | + | "roles": [ | |||
| 2725 | + | "daemon", | |||
| 2726 | + | "client", | |||
| 2727 | + | "udp_peer" | |||
| 2728 | + | ] | |||
| 2729 | + | }, | |||
| 2730 | + | { | |||
| 2731 | + | "id": "8x1-all_output-udp_loss_0pct", | |||
| 2732 | + | "topology": "8x1", | |||
| 2733 | + | "workload": "all_output", | |||
| 2734 | + | "transport": "udp_loss_0pct", | |||
| 2735 | + | "roles": [ | |||
| 2736 | + | "daemon", | |||
| 2737 | + | "client", | |||
| 2738 | + | "udp_peer" | |||
| 2739 | + | ] | |||
| 2740 | + | }, | |||
| 2741 | + | { | |||
| 2742 | + | "id": "8x1-inactive_output-udp_loss_0pct", | |||
| 2743 | + | "topology": "8x1", | |||
| 2744 | + | "workload": "inactive_output", | |||
| 2745 | + | "transport": "udp_loss_0pct", | |||
| 2746 | + | "roles": [ | |||
| 2747 | + | "daemon", | |||
| 2748 | + | "client", | |||
| 2749 | + | "udp_peer" | |||
| 2750 | + | ] | |||
| 2751 | + | }, | |||
| 2752 | + | { | |||
| 2753 | + | "id": "8x1-interactive_flood-udp_loss_0pct", | |||
| 2754 | + | "topology": "8x1", | |||
| 2755 | + | "workload": "interactive_flood", | |||
| 2756 | + | "transport": "udp_loss_0pct", | |||
| 2757 | + | "roles": [ | |||
| 2758 | + | "daemon", | |||
| 2759 | + | "client", | |||
| 2760 | + | "udp_peer" | |||
| 2761 | + | ] | |||
| 2762 | + | }, | |||
| 2763 | + | { | |||
| 2764 | + | "id": "8x1-copy_search-udp_loss_0pct", | |||
| 2765 | + | "topology": "8x1", | |||
| 2766 | + | "workload": "copy_search", | |||
| 2767 | + | "transport": "udp_loss_0pct", | |||
| 2768 | + | "roles": [ | |||
| 2769 | + | "daemon", | |||
| 2770 | + | "client", | |||
| 2771 | + | "udp_peer" | |||
| 2772 | + | ] | |||
| 2773 | + | }, | |||
| 2774 | + | { | |||
| 2775 | + | "id": "8x1-resize_sweep-udp_loss_0pct", | |||
| 2776 | + | "topology": "8x1", | |||
| 2777 | + | "workload": "resize_sweep", | |||
| 2778 | + | "transport": "udp_loss_0pct", | |||
| 2779 | + | "roles": [ | |||
| 2780 | + | "daemon", | |||
| 2781 | + | "client", | |||
| 2782 | + | "udp_peer" | |||
| 2783 | + | ] | |||
| 2784 | + | }, | |||
| 2785 | + | { | |||
| 2786 | + | "id": "8x1-snapshot_output_resize-udp_loss_0pct", | |||
| 2787 | + | "topology": "8x1", | |||
| 2788 | + | "workload": "snapshot_output_resize", | |||
| 2789 | + | "transport": "udp_loss_0pct", | |||
| 2790 | + | "roles": [ | |||
| 2791 | + | "daemon", | |||
| 2792 | + | "client", | |||
| 2793 | + | "udp_peer" | |||
| 2794 | + | ] | |||
| 2795 | + | }, | |||
| 2796 | + | { | |||
| 2797 | + | "id": "8x1-attach_restore_tab_switch-udp_loss_0pct", | |||
| 2798 | + | "topology": "8x1", | |||
| 2799 | + | "workload": "attach_restore_tab_switch", | |||
| 2800 | + | "transport": "udp_loss_0pct", | |||
| 2801 | + | "roles": [ | |||
| 2802 | + | "daemon", | |||
| 2803 | + | "client", | |||
| 2804 | + | "udp_peer" | |||
| 2805 | + | ] | |||
| 2405 | } | 2806 | } | ||
| 2406 | ] | 2807 | ] | ||
| 2407 | } | 2808 | } | ||