aboutsummaryrefslogtreecommitdiffstats
path: root/research
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-01 07:05:51 +0100
committereunhhu <[email protected]>2026-10-01 07:05:51 +0100
commit792b34a127de71abc73f4071e8a7363864eef170 (patch)
tree09c70c9ca6b70d564eaac58fccf957ad25ae1fa6 /research
parent0ad46603d88eccaefd250870109186f823e1845a (diff)
downloadpure-792b34a127de71abc73f4071e8a7363864eef170.tar.gz
pure-792b34a127de71abc73f4071e8a7363864eef170.zip
Measure sending between two apps of a host: local outlet or link (112)
Two apps of one host, src sending one message for each of 1,000 items in a burst event, 2,000 bursts a run, five rounds with the ways in turns: the local outlet stores 1,141,642 sends a second at 1.17 us of the host's CPU each, a loopback pure:// link to the host's own port 578,879 at 2.93 us (1.97 times the sends, 0.40 times the CPU). 017's second stage asked for at least the link's rate at half its CPU or less: it goes in. Every run checks dst's sum: each send came once.
Diffstat (limited to 'research')
-rw-r--r--research/experiments/027-apps-sending/2026-10-01-load.txt2
-rw-r--r--research/experiments/027-apps-sending/2026-10-01-sends-linux-aarch64.jsonl10
-rw-r--r--research/experiments/027-apps-sending/dst.aup9
-rwxr-xr-xresearch/experiments/027-apps-sending/runs.sh12
-rw-r--r--research/experiments/027-apps-sending/sends.py117
-rw-r--r--research/experiments/027-apps-sending/src.aup16
6 files changed, 166 insertions, 0 deletions
diff --git a/research/experiments/027-apps-sending/2026-10-01-load.txt b/research/experiments/027-apps-sending/2026-10-01-load.txt
new file mode 100644
index 0000000..877d5fc
--- /dev/null
+++ b/research/experiments/027-apps-sending/2026-10-01-load.txt
@@ -0,0 +1,2 @@
+ 07:04:41 up 4:17, 3 users, load average: 1.33, 4.07, 4.66
+ 07:05:09 up 4:17, 3 users, load average: 1.58, 3.87, 4.57
diff --git a/research/experiments/027-apps-sending/2026-10-01-sends-linux-aarch64.jsonl b/research/experiments/027-apps-sending/2026-10-01-sends-linux-aarch64.jsonl
new file mode 100644
index 0000000..194d5c3
--- /dev/null
+++ b/research/experiments/027-apps-sending/2026-10-01-sends-linux-aarch64.jsonl
@@ -0,0 +1,10 @@
+{"round": 1, "way": "local", "sends": 2000000, "seconds": 1.778, "sends_per_second": 1124661, "cpu_us_per_send": 1.17, "handed": 1999, "link_packets": 0}
+{"round": 1, "way": "link", "sends": 2000000, "seconds": 3.477, "sends_per_second": 575155, "cpu_us_per_send": 2.92, "handed": 0, "link_packets": 63888}
+{"round": 2, "way": "link", "sends": 2000000, "seconds": 3.383, "sends_per_second": 591140, "cpu_us_per_send": 2.87, "handed": 0, "link_packets": 63094}
+{"round": 2, "way": "local", "sends": 2000000, "seconds": 1.74, "sends_per_second": 1149587, "cpu_us_per_send": 1.17, "handed": 2000, "link_packets": 0}
+{"round": 3, "way": "local", "sends": 2000000, "seconds": 1.756, "sends_per_second": 1138945, "cpu_us_per_send": 1.18, "handed": 2000, "link_packets": 0}
+{"round": 3, "way": "link", "sends": 2000000, "seconds": 3.455, "sends_per_second": 578879, "cpu_us_per_send": 2.94, "handed": 0, "link_packets": 66506}
+{"round": 4, "way": "link", "sends": 2000000, "seconds": 3.636, "sends_per_second": 550106, "cpu_us_per_send": 3.12, "handed": 0, "link_packets": 72477}
+{"round": 4, "way": "local", "sends": 2000000, "seconds": 1.752, "sends_per_second": 1141642, "cpu_us_per_send": 1.17, "handed": 2000, "link_packets": 0}
+{"round": 5, "way": "local", "sends": 2000000, "seconds": 1.721, "sends_per_second": 1161814, "cpu_us_per_send": 1.16, "handed": 2000, "link_packets": 0}
+{"round": 5, "way": "link", "sends": 2000000, "seconds": 3.325, "sends_per_second": 601578, "cpu_us_per_send": 2.93, "handed": 0, "link_packets": 69839}
diff --git a/research/experiments/027-apps-sending/dst.aup b/research/experiments/027-apps-sending/dst.aup
new file mode 100644
index 0000000..c1c8acb
--- /dev/null
+++ b/research/experiments/027-apps-sending/dst.aup
@@ -0,0 +1,9 @@
+state n = 0
+state total = 0
+input take = message { k: u32 }
+on take {
+ n = n + 1
+ total = total + take.k
+}
+view count = n
+view sum = total
diff --git a/research/experiments/027-apps-sending/runs.sh b/research/experiments/027-apps-sending/runs.sh
new file mode 100755
index 0000000..fe4dfda
--- /dev/null
+++ b/research/experiments/027-apps-sending/runs.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+# The runs of docs/design/017's second stage, from the repository's root:
+# two apps of one host, sending through the local outlet or over a
+# loopback link, five rounds of 2,000 bursts (2,000,000 sends), the ways in
+# turns, once the last minute's load is below 1.5.
+set -e
+out=research/experiments/027-apps-sending
+until awk '{ exit !($1 < 1.5) }' /proc/loadavg; do sleep 10; done
+echo "$(uptime)" > $out/2026-10-01-load.txt
+make -s build/pure-serve
+python3 $out/sends.py build/pure-serve 5 2000 > $out/2026-10-01-sends-linux-aarch64.jsonl
+echo "$(uptime)" >> $out/2026-10-01-load.txt
diff --git a/research/experiments/027-apps-sending/sends.py b/research/experiments/027-apps-sending/sends.py
new file mode 100644
index 0000000..454dc2b
--- /dev/null
+++ b/research/experiments/027-apps-sending/sends.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+"""Two apps of one host (docs/design/017, stage 2), src sending to dst
+either through the local outlet (send=out=dst/take) or over a loopback
+pure:// link to the host's own --link-port (sealed with its key). src
+holds 1,000 items; each burst event walks them and sends one message
+each, so sending (not HTTP) is what the host does. A run: BURSTS bursts
+posted one after another, then the time until dst has stored every send,
+and the host's CPU over it (utime + stime). dst's sum says each send came
+once. In RAM, the host on cores 0-2.
+
+sends.py SERVE ROUNDS [BURSTS] prints a JSON line per run
+"""
+import http.client
+import json
+import os
+import select
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+ROOT = HERE.parents[2]
+AUP = ROOT.parent / 'aup' / 'build' / 'aup'
+ITEMS = 1000
+
+
+def started(args):
+ process = subprocess.Popen(['taskset', '-c', '0-2', *map(str, args), '--port', '0'], stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ ready, _, _ = select.select([process.stdout], [], [], 10)
+ assert ready, process.stderr.read()
+ return process, json.loads(process.stdout.readline())
+
+
+def stopped(process):
+ process.terminate()
+ _, error = process.communicate(timeout=60)
+ assert process.returncode == 0, error[-500:]
+ return json.loads(error.decode().strip().splitlines()[-1])
+
+
+def cpu_seconds(pid):
+ fields = Path(f'/proc/{pid}/stat').read_text().rsplit(')', 1)[1].split()
+ return (int(fields[11]) + int(fields[12])) / os.sysconf('SC_CLK_TCK')
+
+
+def run(way, serve, work, modules, key, public, bursts):
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
+ probe.bind(('127.0.0.1', 0))
+ link_port = probe.getsockname()[1]
+ to = 'dst/take' if way == 'local' else f'pure://127.0.0.1:{link_port}/dst/take seal=out={public}'
+ (work / 'apps.conf').write_text(f"src {modules['src']} {work / 'src'} send=out={to}\n"
+ f"dst {modules['dst']} {work / 'dst'}\n")
+ args = [serve, '--apps', work / 'apps.conf', '--threads', 1, '--workers', 2]
+ if way == 'link':
+ args += ['--key', key, '--link-port', link_port, '--sender', f'src.out={public}']
+ process, info = started(args)
+ client = http.client.HTTPConnection('127.0.0.1', info['port'], timeout=60)
+
+ def ask(method, path):
+ client.request(method, path, b'' if method == 'POST' else None)
+ answer = client.getresponse()
+ body = answer.read()
+ assert answer.status == 200, (path, answer.status)
+ return body
+
+ try:
+ for k in range(1, ITEMS + 1):
+ ask('POST', f'/src/inputs/fill?k={k}')
+ began, cpu = time.perf_counter(), cpu_seconds(process.pid)
+ for _ in range(bursts):
+ ask('POST', '/src/inputs/burst?step=1')
+ sent = bursts * ITEMS
+ while int(ask('GET', '/dst/views/count')) < sent:
+ time.sleep(0.005)
+ took, used = time.perf_counter() - began, cpu_seconds(process.pid) - cpu
+ total = int(ask('GET', '/dst/views/sum'))
+ assert int(ask('GET', '/dst/views/count')) == sent and total == bursts * (ITEMS * (ITEMS + 1) // 2 + ITEMS), total
+ finally:
+ client.close()
+ stats = stopped(process)
+ return {'way': way, 'sends': sent, 'seconds': round(took, 3), 'sends_per_second': round(sent / took),
+ 'cpu_us_per_send': round(used * 1e6 / sent, 2), 'handed': stats.get('handed', 0),
+ 'link_packets': stats['link_packets']}
+
+
+def main():
+ serve, rounds = Path(sys.argv[1]).resolve(), int(sys.argv[2])
+ bursts = int(sys.argv[3]) if len(sys.argv) > 3 else 60
+ work = Path(tempfile.mkdtemp(prefix='pure-sends-', dir='/dev/shm'))
+ try:
+ modules = {}
+ for name in ('src', 'dst'):
+ modules[name] = work / f'{name}.pmod'
+ subprocess.run([str(AUP), 'build', str(HERE / f'{name}.aup'), '-o', str(modules[name])], check=True)
+ key = work / 'host.key'
+ (work / 'one.conf').write_text(f"x {modules['dst']} {work / 'keys'}\n")
+ process, info = started([serve, '--apps', work / 'one.conf', '--key', key])
+ public = info['public_key']
+ stopped(process)
+ for round_ in range(1, rounds + 1):
+ for way in (['local', 'link'] if round_ % 2 else ['link', 'local']):
+ runs = work / f'run-{round_}-{way}'
+ runs.mkdir()
+ row = run(way, serve, runs, modules, key, public, bursts)
+ print(json.dumps({'round': round_, **row}), flush=True)
+ shutil.rmtree(runs)
+ finally:
+ shutil.rmtree(work)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/research/experiments/027-apps-sending/src.aup b/research/experiments/027-apps-sending/src.aup
new file mode 100644
index 0000000..6ec7c4c
--- /dev/null
+++ b/research/experiments/027-apps-sending/src.aup
@@ -0,0 +1,16 @@
+record Item {
+ k = 0
+}
+state items: list of Item
+input fill = message { k: u32 }
+input burst = message { step: u32 }
+output out = message { k: u32 }
+on fill {
+ items.append(Item { k: fill.k })
+}
+on burst {
+ for i in items {
+ send out { k: i.k + burst.step }
+ }
+}
+view count = count(i in items where i.k > 0)