diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/udp_scaling.py | 64 |
1 files changed, 44 insertions, 20 deletions
diff --git a/tests/udp_scaling.py b/tests/udp_scaling.py index b5048cd..7691fcb 100644 --- a/tests/udp_scaling.py +++ b/tests/udp_scaling.py | |||
| @@ -14,10 +14,16 @@ loop, which is what a default build does. | |||
| 14 | LOAD_THREADS and WINDOW decide how much load is offered. Both matter. Too | 14 | LOAD_THREADS and WINDOW decide how much load is offered. Both matter. Too |
| 15 | little and the tracker idles while the figure measures round trips; too much | 15 | little and the tracker idles while the figure measures round trips; too much |
| 16 | and the kernel drops datagrams, every lost reply costs the generator a | 16 | and the kernel drops datagrams, every lost reply costs the generator a |
| 17 | timeout, and the figure collapses. The generator prints how many were lost: | 17 | timeout, and the figure collapses. |
| 18 | tune until that is near zero and the throughput has stopped rising. | 18 | |
| 19 | Three columns say which side the number came from. "cores" is the tracker's | ||
| 20 | cpu, "load" the generator's. Whichever is near its machine's core count is | ||
| 21 | the one being measured. "lost" counts datagrams that went unanswered: once | ||
| 22 | it leaves zero the throughput figure is timeouts, not service, and the run | ||
| 23 | needs a smaller WINDOW. | ||
| 19 | """ | 24 | """ |
| 20 | import os | 25 | import os |
| 26 | import resource | ||
| 21 | import signal | 27 | import signal |
| 22 | import socket | 28 | import socket |
| 23 | import subprocess | 29 | import subprocess |
| @@ -73,6 +79,13 @@ def cpu_seconds(pid): | |||
| 73 | return total | 79 | return total |
| 74 | 80 | ||
| 75 | 81 | ||
| 82 | def reaped_cpu_seconds(): | ||
| 83 | """Cpu of every child already waited for. The generator is the only one | ||
| 84 | that starts and finishes inside a measured run.""" | ||
| 85 | spent = resource.getrusage(resource.RUSAGE_CHILDREN) | ||
| 86 | return spent.ru_utime + spent.ru_stime | ||
| 87 | |||
| 88 | |||
| 76 | def stop(tracker): | 89 | def stop(tracker): |
| 77 | tracker.send_signal(signal.SIGINT) | 90 | tracker.send_signal(signal.SIGINT) |
| 78 | try: | 91 | try: |
| @@ -82,17 +95,28 @@ def stop(tracker): | |||
| 82 | tracker.wait() | 95 | tracker.wait() |
| 83 | 96 | ||
| 84 | 97 | ||
| 98 | def parse(line): | ||
| 99 | """replies/s and the count of announces that were never answered""" | ||
| 100 | words = line.split() | ||
| 101 | if not words or not words[0].replace(".", "").isdigit(): | ||
| 102 | return None, None | ||
| 103 | lost = 0 | ||
| 104 | if "lost," in words: | ||
| 105 | lost = int(words[words.index("lost,") - 1]) | ||
| 106 | return float(words[0]), lost | ||
| 107 | |||
| 108 | |||
| 85 | def main(): | 109 | def main(): |
| 86 | if not os.path.exists(LOADER): | 110 | if not os.path.exists(LOADER): |
| 87 | raise SystemExit("build the generator first: cc -O2 -pthread -o %s tests/udp_load.c" % LOADER) | 111 | raise SystemExit("build the generator first: cc -O2 -pthread -o %s tests/udp_load.c" % LOADER) |
| 88 | 112 | ||
| 89 | print(" tracker %s, %d load threads x %d in flight, %d torrents, %.0f s x %d runs" | 113 | print(" tracker %s, %d load threads x %d in flight, %d torrents, %.0f s x %d runs" |
| 90 | % (TRACKER, LOAD_THREADS, WINDOW, TORRENTS, SECONDS, REPEATS)) | 114 | % (TRACKER, LOAD_THREADS, WINDOW, TORRENTS, SECONDS, REPEATS)) |
| 91 | print(" %-8s %12s %12s %12s %7s %s" | 115 | print(" %-8s %12s %7s %7s %7s %10s %s" |
| 92 | % ("workers", "median/s", "lowest", "highest", "cores", "vs 0 workers")) | 116 | % ("workers", "median/s", "spread", "cores", "load", "lost", "vs 0 workers")) |
| 93 | baseline = None | 117 | baseline = None |
| 94 | for index, workers in enumerate(WORKER_COUNTS): | 118 | for index, workers in enumerate(WORKER_COUNTS): |
| 95 | rates, cores = [], [] | 119 | results = [] |
| 96 | for repeat in range(REPEATS): | 120 | for repeat in range(REPEATS): |
| 97 | port = PORT_BASE + index * 16 + repeat | 121 | port = PORT_BASE + index * 16 + repeat |
| 98 | tracker, log = start(port, workers) | 122 | tracker, log = start(port, workers) |
| @@ -101,34 +125,34 @@ def main(): | |||
| 101 | # place, so the measured run is renewals rather than insertions | 125 | # place, so the measured run is renewals rather than insertions |
| 102 | subprocess.run([LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), "2", str(TORRENTS), str(WINDOW)], | 126 | subprocess.run([LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), "2", str(TORRENTS), str(WINDOW)], |
| 103 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | 127 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 104 | spent, wall = cpu_seconds(tracker.pid), time.time() | 128 | spent, offered, wall = cpu_seconds(tracker.pid), reaped_cpu_seconds(), time.time() |
| 105 | result = subprocess.run( | 129 | result = subprocess.run( |
| 106 | [LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), str(SECONDS), str(TORRENTS), str(WINDOW)], | 130 | [LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), str(SECONDS), str(TORRENTS), str(WINDOW)], |
| 107 | capture_output=True, text=True, | 131 | capture_output=True, text=True, |
| 108 | ) | 132 | ) |
| 109 | used = cpu_seconds(tracker.pid) | 133 | used, drove, elapsed = cpu_seconds(tracker.pid), reaped_cpu_seconds(), max(time.time() - wall, 1e-9) |
| 110 | finally: | 134 | finally: |
| 111 | stop(tracker) | 135 | stop(tracker) |
| 112 | log.close() | 136 | log.close() |
| 113 | head = result.stdout.strip().split() | 137 | rate, lost = parse(result.stdout.strip()) |
| 114 | if head and head[0].replace(".", "").isdigit(): | 138 | if rate is None: |
| 115 | rates.append(float(head[0])) | 139 | continue |
| 116 | if used is not None and spent is not None: | 140 | cores = (used - spent) / elapsed if (used is not None and spent is not None) else None |
| 117 | cores.append((used - spent) / max(time.time() - wall, 1e-9)) | 141 | results.append((rate, cores, (drove - offered) / elapsed, lost)) |
| 118 | 142 | ||
| 119 | if not rates: | 143 | if not results: |
| 120 | print(" %-8s no result" % workers) | 144 | print(" %-8s no result" % workers) |
| 121 | continue | 145 | continue |
| 122 | rates.sort() | 146 | results.sort() |
| 123 | median = rates[len(rates) // 2] | 147 | rate, cores, load, lost = results[len(results) // 2] |
| 124 | if baseline is None: | 148 | if baseline is None: |
| 125 | baseline = median | 149 | baseline = rate |
| 126 | # A spread wider than the differences between rows means the rows are | 150 | # A spread wider than the differences between rows means the rows are |
| 127 | # not telling you anything | 151 | # not telling you anything |
| 128 | busy = ("%7.2f" % (sorted(cores)[len(cores) // 2])) if cores else " ?" | 152 | spread = 100.0 * (results[-1][0] - results[0][0]) / rate |
| 129 | print(" %-8s %12.0f %12.0f %12.0f %s %.2fx (spread %.0f%%)" | 153 | print(" %-8s %12.0f %6.0f%% %7s %7.2f %10d %.2fx" |
| 130 | % (workers, median, rates[0], rates[-1], busy, median / baseline, | 154 | % (workers, rate, spread, ("%.2f" % cores) if cores else "?", load, lost, rate / baseline)) |
| 131 | 100.0 * (rates[-1] - rates[0]) / median)) | 155 | sys.stdout.flush() |
| 132 | 156 | ||
| 133 | 157 | ||
| 134 | if __name__ == "__main__": | 158 | if __name__ == "__main__": |
