99
1010`summarize` reads finished experiments and prints, per query and config, the
1111measured error against Prometheus (ARE over the 100 largest keys for key
12- queries, the metric sketch-bench's CMS estimate uses; relative value error for
13- quantile queries; ARE over all keys as a second column) next to the predicted
14- error, plus query latencies.
12+ queries, the metric sketch-bench's CMS estimate uses, with ARE over all keys as
13+ a second column; rank error against the replayed trace values for the p99
14+ query) next to the predicted error, plus query latencies.
1515
1616Usage (from asap-tools/experiments):
1717 python recommended_sketch_configs/recommended_sketch_configs.py generate \
2222
2323import argparse
2424import csv
25+ import gzip
2526import json
2627import os
2728import re
128129# remote_monitor.py keyword of the containerized query engine.
129130QUERY_ENGINE_MONITOR_KEYWORD = "sketchdb-queryengine-rust"
130131
132+ # The only quantile query is cpu_p99. A run replays Google at 1/10 speed, so by
133+ # its queries the exporter has exported the rows starting by 615 s (the
134+ # 5-minute window starting at 600 s).
135+ P99 = 0.99
136+ P99_REPLAY_CUTOFF_US = 615_000_000
137+
131138
132139def parse_duration_s (text : str ) -> int :
133140 """'5m' -> 300."""
@@ -327,15 +334,42 @@ def are_top_keys(exact: Dict, estimate: Dict, num_keys: int = 100) -> float:
327334 return float (np .mean (errors )) if errors else float ("nan" )
328335
329336
337+ def replayed_google_cpu_values (trace : str , cutoff_us : int ) -> np .ndarray :
338+ """Sorted mean CPU usage of the task_usage rows the exporter replayed.
339+
340+ Rows with start_time <= cutoff_us and aggregation_type 0 (the series the
341+ p99 query reads).
342+ """
343+ values = []
344+ with gzip .open (trace , "rt" ) as f :
345+ for line in f :
346+ c = line .rstrip ("\n " ).split ("," )
347+ if int (c [0 ]) <= cutoff_us and c [18 ] in ("" , "0" ) and c [5 ] != "" :
348+ values .append (float (c [5 ]))
349+ return np .sort (values )
350+
351+
352+ def rank_errors (sorted_values : np .ndarray , estimates : List [float ], q : float ):
353+ """|F(estimate) - q| per estimate, F the empirical CDF of sorted_values."""
354+ ranks = np .searchsorted (sorted_values , estimates , side = "right" )
355+ return np .abs (ranks / len (sorted_values ) - q )
356+
357+
330358def monitor_output_path (experiment_dir : str ) -> str :
331359 """Written by remote_monitor.py when the run finishes."""
332360 return os .path .join (
333361 experiment_dir , "sketchdb" , "remote_monitor_output" , "monitor_output.json"
334362 )
335363
336364
337- def summarize_experiment (experiment_dir : str ) -> dict :
338- """Measured error and latency of one finished experiment."""
365+ def summarize_experiment (
366+ experiment_dir : str , quantile_values : Optional [np .ndarray ] = None
367+ ) -> dict :
368+ """Measured error and latency of one finished experiment.
369+
370+ With quantile_values (sorted replayed values), the measured error is the
371+ p99 rank error instead of the ARE, and there is no all-keys column.
372+ """
339373 sys .path .insert (0 , EXPERIMENTS_DIR )
340374 from post_experiment .lib .results_loader import load_results
341375
@@ -347,13 +381,16 @@ def summarize_experiment(experiment_dir: str) -> dict:
347381 errors = []
348382 errors_all_keys = []
349383 for exact_rep , estimate_rep in zip (exact , estimate ):
350- if exact_rep .result and estimate_rep .result :
351- errors .append (are_top_keys (exact_rep .result , estimate_rep .result ))
352- errors_all_keys .append (
353- are_top_keys (
354- exact_rep .result , estimate_rep .result , len (exact_rep .result )
355- )
356- )
384+ if not (exact_rep .result and estimate_rep .result ):
385+ continue
386+ if quantile_values is not None :
387+ (value ,) = estimate_rep .result .values ()
388+ errors .append (float (rank_errors (quantile_values , [value ], P99 )[0 ]))
389+ continue
390+ errors .append (are_top_keys (exact_rep .result , estimate_rep .result ))
391+ errors_all_keys .append (
392+ are_top_keys (exact_rep .result , estimate_rep .result , len (exact_rep .result ))
393+ )
357394 latencies = {
358395 server : [r .latency for r in results [server ][0 ].query_results if r .latency ]
359396 for server in ("prometheus" , "sketchdb" )
@@ -367,7 +404,7 @@ def summarize_experiment(experiment_dir: str) -> dict:
367404 return {
368405 "measured_error" : float (np .nanmedian (errors )) if errors else float ("nan" ),
369406 "measured_error_all_keys" : (
370- float (np .nanmedian (errors_all_keys )) if errors else float ( "nan" )
407+ float (np .nanmedian (errors_all_keys )) if errors_all_keys else ""
371408 ),
372409 "answered" : f"{ len (errors )} /{ len (exact )} " ,
373410 "asap_latency_ms" : 1000 * float (np .median (latencies ["sketchdb" ])),
@@ -376,9 +413,12 @@ def summarize_experiment(experiment_dir: str) -> dict:
376413 }
377414
378415
379- def summarize (recommendations : List [dict ], experiments_dir : str ) -> List [dict ]:
416+ def summarize (
417+ recommendations : List [dict ], experiments_dir : str , google_trace : str
418+ ) -> List [dict ]:
380419 """One row per finished recommended or default experiment."""
381420 rows = []
421+ quantile_values = None
382422 for row in recommendations :
383423 if row ["family" ] not in PLANNER_FAMILIES :
384424 continue
@@ -392,13 +432,21 @@ def summarize(recommendations: List[dict], experiments_dir: str) -> List[dict]:
392432 if not os .path .exists (monitor_output_path (experiment_dir )):
393433 print (f"skipped unfinished { name } " , file = sys .stderr )
394434 continue
395- summary = summarize_experiment (experiment_dir )
435+ if row ["family" ] == "kll" and quantile_values is None :
436+ quantile_values = replayed_google_cpu_values (
437+ google_trace , P99_REPLAY_CUTOFF_US
438+ )
439+ summary = summarize_experiment (
440+ experiment_dir , quantile_values if row ["family" ] == "kll" else None
441+ )
442+ recommended = variant == "recommended"
396443 rows .append (
397444 {
398445 "experiment" : name ,
399446 "family" : row ["family" ],
400- "config" : row ["config" ] if variant == "recommended" else "default" ,
401- "predicted_error" : float (row ["est_error" ]),
447+ "config" : row ["config" ] if recommended else "default" ,
448+ # Only the recommended config has a prediction.
449+ "predicted_error" : float (row ["est_error" ]) if recommended else "" ,
402450 "target" : float (row ["target" ]),
403451 ** summary ,
404452 "meets_target" : summary ["measured_error" ] <= float (row ["target" ]),
@@ -418,6 +466,11 @@ def main() -> None:
418466 summ .add_argument ("--recommendations" , required = True )
419467 summ .add_argument ("--experiments-dir" , required = True )
420468 summ .add_argument ("--output-csv" )
469+ summ .add_argument (
470+ "--google-trace" ,
471+ default = "/data/cluster_traces/google/part-00000-of-00500.csv.gz" ,
472+ help = "task_usage part the p99 runs replayed, for their rank error" ,
473+ )
421474 args = parser .parse_args ()
422475
423476 recommendations = load_recommendations (args .recommendations )
@@ -432,7 +485,7 @@ def main() -> None:
432485 print (f"skipped { note } " )
433486 return
434487
435- rows = summarize (recommendations , args .experiments_dir )
488+ rows = summarize (recommendations , args .experiments_dir , args . google_trace )
436489 if not rows :
437490 print ("No finished experiments found" )
438491 return
0 commit comments