okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
b0282b1
main
1"""Compare exported daemons through the real gated-provider E2E harness. 2 3Run in nix develop: 4python test/manual/active_output_benchmark.py --baseline /tmp/baseline/albedo-daemon \ 5--candidate /tmp/candidate/albedo-daemon --output /tmp/active-output-results.json 6 7Twenty attachments follow one discarded warmup per case, alternating builds. 8HTTP capture and complete reference hydration are timed separately. Throughput 9is first-to-last responsive text delivery, not provider request/boot overhead. 10RSS is sampled every 5 ms; Linux /proc is required for memory measurements. 11""" 12 13import argparse 14import json 15import os 16from pathlib import Path 17import platform 18import statistics 19import sys 20import threading 21import time 22import unittest 23 24sys .path .insert (0 ,str (Path (__file__ ).resolve ().parents [1 ]/ "e2e" )) 25import harness 26from active_output_test import ActiveOutputProbe ,PausedOutput ,active_text 27 28 29def summary (values ): 30ordered = sorted (values ) 31return { 32"samples" :len (values ), 33"median_ms" :statistics .median (values ), 34"p95_ms" :ordered [int ((len (ordered )- 1 )* 0.95 )], 35"raw_ms" :values , 36 } 37 38 39class MemorySampler : 40def __init__ (self ,pid ): 41self .pid ,self .peak_kib = pid ,0 42self .stop = threading .Event () 43self .thread = threading .Thread (target = self .sample ,daemon = True ) 44 45def sample (self ): 46while not self .stop .is_set (): 47status = Path (f"/proc/ { self . pid } /status" ).read_text () 48rss = next ( 49int (line .split ()[1 ]) 50for line in status .splitlines () 51if line .startswith ("VmRSS:" ) 52 ) 53self .peak_kib = max (self .peak_kib ,rss ) 54self .stop .wait (0.005 ) 55 56 57def capture (app ,paused ): 58started = time .perf_counter () 59batch = app .stream_page (paused .session ) 60captured = time .perf_counter () 61descriptors = batch ["snapshot" ].get ("active_output" , []) 62restored = {item ["kind" ]:active_text (app ,item )for item in descriptors } 63complete = time .perf_counter () 64available = restored .get ("text" )== paused .prefix and ( 65not paused .thinking or restored .get ("thinking" )== paused .thinking 66 ) 67return ((captured - started )* 1000 , (complete - started )* 1000 ,available ) 68 69 70def delete_session (app ,session ): 71resource = f"/sessions/ { session } ?view=configuration" 72with app .api (resource )as response : 73revision = response .getheader ("ETag" ) 74response .read () 75app .api (resource ,method = "DELETE" ,headers = {"If-Match" :revision }).close () 76 77 78def main (): 79parser = argparse .ArgumentParser (description = __doc__ ) 80parser .add_argument ("--baseline" ,required = True ) 81parser .add_argument ("--candidate" ,required = True ) 82parser .add_argument ("--output" ,type = Path ,required = True ) 83parser .add_argument ("--samples" ,type = int ,default = 20 ) 84parser .add_argument ( 85"--trials" , 86type = int , 87default = 5 , 88help = "responsive throughput trials after warmup" , 89 ) 90args = parser .parse_args () 91if args .samples < 1 or args .trials < 1 : 92parser .error ("samples and trials must be positive" ) 93test = unittest .TestCase () 94daemons ,apps ,samplers = {}, {}, {} 95reports = [] 96try : 97for label in ("baseline" ,"candidate" ): 98os .environ ["ALBEDO_TEST_DAEMON" ]= str (Path (getattr (args ,label )).resolve ()) 99harness .snapshot_daemon () 100daemon = harness .Daemon ("exclusive" ) 101daemons [label ]= daemon 102with harness .on_daemon (daemon ): 103apps [label ]= harness .Albedo ( 104harness .Provider (lambda _ :harness .Reply ("text" ,value = "unused" )), 105prepare = lambda app :app .daemon .env .update (ALBEDO_INSPECT = "1" ), 106 ) 107apps [label ].__enter__ () 108sampler = MemorySampler (daemon ._pid ) 109samplers [label ]= sampler 110sampler .thread .start () 111 112probes = {label :ActiveOutputProbe (app )for label ,app in apps .items ()} 113other_sessions = {label :app .session ()for label ,app in apps .items ()} 114cases = [ 115 ("small" , ["x" * 256 ]* 64 ,"" ), 116 ("text-and-thinking" , ["x" * 256 ]* 64 ,"r" * 16384 ), 117 ("raw-boundary" , ["x" * 256 ]* 256 ,"" ), 118 ("escaped-boundary" , ['"\\\n' * 128 ]* 100 ,"" ), 119 ("unicode" , ["α🙂" * 64 ]* 256 ,"β" * 8192 ), 120 ("one-MiB" , ["x" * 256 ]* 4096 ,"" ), 121 ] 122for name ,chunks ,thinking in cases : 123case = { 124"name" :name , 125"chunks" :len (chunks ), 126"text_bytes" :len ("" .join (chunks ).encode ()), 127"thinking_bytes" :len (thinking .encode ()), 128"builds" : {}, 129 } 130paused = {} 131for label ,app in apps .items (): 132paused [label ]= PausedOutput (test ,app ,app .provider ,chunks ,thinking ) 133capture (app ,paused [label ]) 134case ["builds" ][label ]= { 135"capture_ms" : [], 136"hydrated_ms" : [], 137"restored" : [], 138"unrelated_session_ms" : [], 139"responsive_delivery_ms" : [], 140"actor_after_prefix" :probes [label ].call ( 141"stats" ,paused [label ].session 142 ), 143 } 144for index in range (args .samples ): 145for label in ( 146 ("baseline" ,"candidate" ) 147if index % 2 == 0 148else ("candidate" ,"baseline" ) 149 ): 150capture_ms ,hydrated_ms ,restored = capture ( 151apps [label ],paused [label ] 152 ) 153measurements = case ["builds" ][label ] 154measurements ["capture_ms" ].append (capture_ms ) 155measurements ["hydrated_ms" ].append (hydrated_ms ) 156measurements ["restored" ].append (restored ) 157started = time .perf_counter () 158with apps [label ].api ( 159f"/sessions/ { other_sessions [ label ] } ?tail=0" 160 )as response : 161json .load (response ) 162measurements ["unrelated_session_ms" ].append ( 163 (time .perf_counter ()- started )* 1000 164 ) 165for label ,measurements in case ["builds" ].items (): 166measurements ["capture" ]= summary (measurements .pop ("capture_ms" )) 167measurements ["hydrated" ]= summary (measurements .pop ("hydrated_ms" )) 168measurements ["unrelated_session" ]= summary ( 169measurements .pop ("unrelated_session_ms" ) 170 ) 171measurements ["all_attachments_restored" ]= all ( 172measurements .pop ("restored" ) 173 ) 174spill = apps [label ].home / "active-output" 175measurements ["spill_bytes_before_commit" ]= sum ( 176path .stat ().st_size for path in spill .glob ("*.data" ) 177 ) 178paused [label ].finish (test ) 179measurements ["spill_bytes_after_commit" ]= sum ( 180path .stat ().st_size for path in spill .glob ("*.data" ) 181 ) 182delete_session (apps [label ],paused [label ].session ) 183measurements ["spill_bytes_after_delete" ]= sum ( 184path .stat ().st_size for path in spill .glob ("*.data" ) 185 ) 186for trial in range (args .trials ): 187for label in ( 188 ("baseline" ,"candidate" ) 189if trial % 2 == 0 190else ("candidate" ,"baseline" ) 191 ): 192app = apps [label ] 193output = PausedOutput (test ,app ,app .provider ,chunks ,thinking ) 194assert ( 195output .prefix_at is not None 196and output .first_text_at is not None 197 ) 198case ["builds" ][label ]["responsive_delivery_ms" ].append ( 199 (output .prefix_at - output .first_text_at )* 1000 200 ) 201output .finish (test ) 202delete_session (app ,output .session ) 203for measurements in case ["builds" ].values (): 204measurements ["responsive_delivery" ]= summary ( 205measurements .pop ("responsive_delivery_ms" ) 206 ) 207reports .append (case ) 208json .dumps ( 210 { 211"case" :name , 212"builds" : { 213label : { 214"capture_median_ms" :values ["capture" ]["median_ms" ], 215"hydrated_median_ms" :values ["hydrated" ]["median_ms" ], 216"delivery_median_ms" :values ["responsive_delivery" ][ 217"median_ms" 218 ], 219"restored" :values ["all_attachments_restored" ], 220"spill_after_delete" :values [ 221"spill_bytes_after_delete" 222 ], 223 } 224for label ,values in case ["builds" ].items () 225 }, 226 } 227 ), 228flush = True , 229 ) 230args .output .write_text ( 231json .dumps ( 232 { 233"launchers" : {label :getattr (args ,label )for label in apps }, 234"method" : { 235"platform" :platform .platform (), 236"python" :platform .python_version (), 237"vm_flags" :harness .TEST_VM_FLAGS , 238"attach_samples" :args .samples , 239"throughput_trials" :args .trials , 240"rss_sample_ms" :5 , 241"actor_memory" :"after GC, outside timed HTTP and throughput trials" , 242"hydrated_latency" :"HTTP snapshot plus all active reference pages; baseline has no prefix" , 243 }, 244"rss_peak_kib" : { 245label :sampler .peak_kib for label ,sampler in samplers .items () 246 }, 247"cases" :reports , 248 }, 249indent = 2 , 250 ) 251+ "\n" 252 ) 253finally : 254test .doCleanups () 255for sampler in samplers .values (): 256sampler .stop .set () 257sampler .thread .join () 258for app in apps .values (): 259app .provider .close () 260app .__exit__ () 261harness .shutdown () 262 263 264if __name__ == "__main__" : 265main ()