okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
b570aa7
main
1"""Compare the daemon's CPU and memory across builds on one scripted load. 2 3The load is `--sessions` sessions run at once, each a model turn that makes 4`--calls` python tool calls (every cell asks the kernel for the commands 5catalog, a host-route call) and then streams a long final answer. The numbers 6come from the daemon's own VM over inspection, so build the daemon you want to 7measure and point at it: 8 9ALBEDO_TEST_DAEMON=$(test/snapshot-daemon.sh /tmp/bench/old) \ 10python3 test/manual/json_roundtrip_bench.py 11 12Or compare builds in one run, interleaved ABBA so a busy machine slows both 13alike (give each build a label): 14 15python3 test/manual/json_roundtrip_bench.py \\ 16--daemon old=/tmp/bench/old/albedo-daemon --daemon new=/tmp/bench/new/albedo-daemon 17 18reductions and gc_words barely move with contention; cpu_ms and wall_s do, so 19read those only on a quiet machine. 20cpu_ms is the VM's CPU time, reductions its work, gc_words the heap the 21collector reclaimed (garbage made), peak_mb the daemon's highest resident set 22sampled every 50 ms. 23""" 24 25import argparse 26import os 27import re 28import statistics 29import subprocess 30import sys 31import threading 32import time 33from pathlib import Path 34 35sys .path .insert (0 ,str (Path (__file__ ).resolve ().parents [1 ]/ "e2e" )) 36 37import harness # noqa: E402 38from harness import Albedo ,Provider ,python ,text # noqa: E402 39from inspect_support import Inspection # noqa: E402 40 41CALL = "len(await commands.catalog())" 42ANSWER = "a streamed word " * 200 43 44 45def cell (kilobytes ): 46"""The cell's source: the host call, after `kilobytes` KiB of padding that 47the kernel's command carries (the daemon once decoded all of it per cell).""" 48pad = f'PAD = " { "x" * 1024 * kilobytes } "\n' if kilobytes else "" 49return pad + CALL 50 51 52def script (calls ,code ): 53def reply (request ): 54used = sum ( 551 for message in request ["messages" ]if message .get ("role" )== "tool" 56 ) 57return python (code )if used < calls else text (ANSWER ) 58 59return reply 60 61 62def numbers (output ): 63return [int (value )for value in re .findall (r"\d+" ,output )] 64 65 66class Meter : 67def __init__ (self ,inspection ,pid ): 68self .inspection = inspection 69self .pid = pid 70 71def read (self ): 72return numbers ( 73self .inspection .evaluate ( 74"Read = fun() -> {Cpu, _} = erlang:statistics(runtime), " 75"{Red, _} = erlang:statistics(reductions), " 76"{_, Words, _} = erlang:statistics(garbage_collection), " 77"[Cpu, Red, Words] end, " 78'io:format("~w", [Call(erlang, apply, [Read, []])])' 79 ) 80 ) 81 82def rss_mb (self ): 83out = subprocess .run ( 84 ["ps" ,"-o" ,"rss=" ,"-p" ,str (self .pid )],capture_output = True ,text = True 85 ).stdout 86return int (out )/ 1024 if out .strip ()else 0.0 87 88 89def load (app ,sessions ,meter ): 90"""Run every session at once: the wall time and the highest RSS seen.""" 91 92def run (session ): 93app .prompt (session ,"go" ).close () 94app .idle (session ,timeout = 180 ) 95 96peak = [meter .rss_mb ()] 97done = threading .Event () 98 99def watch (): 100while not done .wait (0.05 ): 101peak [0 ]= max (peak [0 ],meter .rss_mb ()) 102 103threads = [threading .Thread (target = run ,args = (s ,))for s in sessions ] 104watcher = threading .Thread (target = watch ) 105started = time .monotonic () 106watcher .start () 107for thread in threads : 108thread .start () 109for thread in threads : 110thread .join () 111wall = time .monotonic ()- started 112done .set () 113watcher .join () 114return wall ,peak [0 ] 115 116 117def measure (launcher ,args ): 118"""Boot one daemon from `launcher` and run the load: one row per run.""" 119harness ._executable = launcher 120daemon = harness .Daemon ("bench" ) 121daemon .env ["ALBEDO_INSPECT" ]= "1" 122provider = Provider (script (args .calls ,cell (args .code_kb )),chunk_size = 20 ) 123rows = [] 124with harness .on_daemon (daemon ): 125app = Albedo (provider ) 126app .__enter__ () 127try : 128meter = Meter (Inspection (app ),daemon ._pid ) 129for index in range (args .repeat + 1 ): 130sessions = [app .session ()for _ in range (args .sessions )] 131before = meter .read () 132wall ,peak = load (app ,sessions ,meter ) 133after = meter .read () 134if index :# the first run warms the daemon 135rows .append ( 136 { 137"wall_s" :wall , 138"cpu_ms" :after [0 ]- before [0 ], 139"reductions" :after [1 ]- before [1 ], 140"gc_words" :after [2 ]- before [2 ], 141"peak_mb" :peak , 142 } 143 ) 144finally : 145app .__exit__ (None ,None ,None ) 146provider .close () 147daemon .discard () 148return rows 149 150 151def main (): 152parser = argparse .ArgumentParser (description = __doc__ .split ("\n" )[0 ]) 153parser .add_argument ("--sessions" ,type = int ,default = 8 ) 154parser .add_argument ("--calls" ,type = int ,default = 25 ) 155parser .add_argument ("--repeat" ,type = int ,default = 4 ,help = "runs per boot" ) 156parser .add_argument ("--code-kb" ,type = int ,default = 0 ,help = "padding in each cell" ) 157parser .add_argument ( 158"--daemon" , 159action = "append" , 160metavar = "LABEL=LAUNCHER" , 161help = "a build to measure; give two or more to compare them" , 162 ) 163parser .add_argument ( 164"--rounds" ,type = int ,default = 2 ,help = "boots of each build, ABBA order" 165 ) 166args = parser .parse_args () 167builds = [tuple (item .split ("=" ,1 ))for item in args .daemon or []]or [ 168 ("daemon" ,os .environ ["ALBEDO_TEST_DAEMON" ]) 169 ] 170labels = [label for label ,_ in builds ] 171order = [] 172for round_ in range (args .rounds ): 173order += labels if round_ % 2 == 0 else labels [::- 1 ] 174rows = {label : []for label in labels } 175launchers = dict (builds ) 176for label in order : 177taken = measure (launchers [label ],args ) 178rows [label ]+= taken 179f" { label :>10 } boot: " + format_row (statistics_of (taken ,statistics .median )) 181 ) 182for label ,taken in rows .items (): 184f" { label :>10 } median: " 186+ format_row (statistics_of (taken ,statistics .median )) 187 ) 188f" { label :>10 } min..max: " + spread (taken )) 189if len (builds )== 2 : 190 (first ,one ), (second ,two )= rows .items () 191for key in ("reductions" ,"gc_words" ,"cpu_ms" ,"peak_mb" ,"wall_s" ): 193a = statistics .median (r [key ]for r in one ) 194b = statistics .median (r [key ]for r in two ) 195f" { key :>10 } : { second } vs { first } { 100 * ( b - a ) / a :+.1f } %" ) 196 197 198def statistics_of (rows ,fold ): 199return {key :fold (row [key ]for row in rows )for key in rows [0 ]} 200 201 202def spread (rows ): 203return " " .join ( 204f" { key } { min ( r [ key ] for r in rows ):.4g } .. { max ( r [ key ] for r in rows ):.4g } " 205for key in ("reductions" ,"gc_words" ,"cpu_ms" ,"peak_mb" ) 206 ) 207 208 209def format_row (row ): 210return ( 211f"wall { row [ 'wall_s' ]:.2f } s cpu { row [ 'cpu_ms' ]:.0f } ms " 212f"reductions { row [ 'reductions' ] / 1e6 :.1f } M gc_words { row [ 'gc_words' ] / 1e6 :.1f } M " 213f"peak { row [ 'peak_mb' ]:.1f } MB" 214 ) 215 216 217if __name__ == "__main__" : 218main ()