Files
wehub-resource-sync 85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

1105 lines
43 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Lightweight benchmark report for the Prometheus + Grafana stack shipped with Agent Lightning."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import math
from dataclasses import dataclass
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, cast
from urllib import error, parse, request
class PrometheusQueryError(RuntimeError):
"""Raised when Prometheus returns an error payload."""
class PrometheusClient:
"""Tiny helper around the Prometheus HTTP API."""
def __init__(
self,
base_url: str,
timeout: float = 10.0,
default_time: Optional[dt.datetime] = None,
):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.default_time = default_time
def query_vector(self, expr: str, eval_time: Optional[dt.datetime] = None) -> List[Mapping[str, object]]:
params: Dict[str, str] = {"query": expr}
query_time = eval_time or self.default_time
if query_time is not None:
params["time"] = query_time.isoformat()
payload = self._get("/api/v1/query", params)
status = payload.get("status")
if not isinstance(status, str) or status != "success":
error_msg = payload.get("error", "unknown error")
raise PrometheusQueryError(str(error_msg))
data_obj = payload.get("data", {})
if isinstance(data_obj, dict):
data = cast(Dict[str, Any], data_obj)
else:
data = {}
result_type_obj = data.get("resultType")
result_type = result_type_obj if isinstance(result_type_obj, str) else None
raw_result_obj = data.get("result", [])
raw_result: List[object]
if isinstance(raw_result_obj, list):
raw_result = cast(List[object], raw_result_obj)
else:
raw_result = []
if result_type == "scalar":
if len(raw_result) >= 2:
ts = raw_result[0]
value = raw_result[1]
return [{"metric": {}, "value": [ts, value]}]
return []
vector_result: List[Mapping[str, object]] = [
cast(Mapping[str, object], item) for item in raw_result if isinstance(item, Mapping)
]
if result_type == "matrix":
collapsed: List[Dict[str, object]] = []
for series in vector_result:
values_obj = series.get("values")
if isinstance(values_obj, list) and values_obj and isinstance(values_obj[-1], Sequence):
last = cast(Sequence[object], values_obj[-1])
else:
continue
metric_obj = series.get("metric")
if isinstance(metric_obj, Mapping):
metric: Dict[str, object] = dict(cast(Mapping[str, object], metric_obj))
else:
metric = {}
collapsed.append({"metric": metric, "value": list(last)})
return cast(List[Mapping[str, object]], collapsed)
if result_type == "vector":
return vector_result
return []
def query_scalar(self, expr: str, eval_time: Optional[dt.datetime] = None) -> Optional[float]:
samples = self.query_vector(expr, eval_time=eval_time)
if not samples:
return None
return _sample_value(samples[0])
def _get(self, path: str, data: Optional[Mapping[str, str]] = None) -> Dict[str, Any]:
encoded: Optional[bytes] = None
if data is not None:
encoded = parse.urlencode(data).encode()
req = request.Request(f"{self.base_url}{path}", data=encoded)
try:
with request.urlopen(req, timeout=self.timeout) as resp:
loaded = json.loads(resp.read().decode())
if isinstance(loaded, dict):
return cast(Dict[str, Any], loaded)
return {}
except error.URLError as exc: # pragma: no cover - network/infra issues
raise PrometheusQueryError(str(exc)) from exc
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Summarize benchmark metrics from Prometheus.")
parser.add_argument("--prom-url", default="http://localhost:9090", help="Base URL for the Prometheus API.")
parser.add_argument(
"--store-url",
default="http://localhost:4747/v1/agl",
help="Base URL for the Lightning Store API (without the /statistics suffix).",
)
parser.add_argument("--timeout", type=float, default=10.0, help="HTTP timeout in seconds.")
parser.add_argument("--start", type=str, help="ISO timestamp (e.g. 2024-05-01T12:00:00Z).")
parser.add_argument("--end", type=str, help="ISO timestamp (default: now).")
parser.add_argument(
"--duration",
type=str,
default="5m",
help="Fallback duration (e.g. 5m, 1h) used when --start is omitted.",
)
return parser.parse_args(argv)
def parse_timestamp(value: Optional[str], default: Optional[dt.datetime] = None) -> Optional[dt.datetime]:
if value is None:
return default
try:
if value.endswith("Z"):
value = value[:-1] + "+00:00"
return dt.datetime.fromisoformat(value).astimezone(dt.timezone.utc)
except ValueError as exc: # pragma: no cover - invalid CLI input
raise SystemExit(f"Invalid timestamp '{value}': {exc}") from exc
def parse_duration(text: str) -> dt.timedelta:
units = {"s": 1, "m": 60, "h": 3600}
if text.isdigit():
return dt.timedelta(seconds=int(text))
suffix = text[-1]
if suffix not in units:
raise SystemExit(f"Unsupported duration '{text}'. Use Ns/Nm/Nh.")
try:
value = int(text[:-1])
except ValueError as exc: # pragma: no cover - invalid CLI input
raise SystemExit(f"Invalid duration '{text}': {exc}") from exc
return dt.timedelta(seconds=value * units[suffix])
def format_window(seconds: float) -> str:
seconds = max(int(seconds), 1)
return f"{seconds}s"
def clamp_window_seconds(duration_seconds: float) -> int:
return max(int(duration_seconds), 1)
def compute_peak_window(duration_seconds: float) -> str:
peak_seconds = max(min(int(duration_seconds), 60), 1)
return f"{peak_seconds}s"
def compute_subquery_step(duration_seconds: float) -> str:
step_seconds = max(int(duration_seconds / 60), 1)
step_seconds = min(step_seconds, 15)
return f"{step_seconds}s"
def _sample_value(sample: Mapping[str, object]) -> Optional[float]:
value_obj = sample.get("value")
if not isinstance(value_obj, Sequence):
return None
value_seq = cast(Sequence[object], value_obj)
if len(value_seq) < 2:
return None
candidate = value_seq[1]
if isinstance(candidate, (int, float)):
return float(candidate)
if isinstance(candidate, str):
try:
return float(candidate)
except ValueError:
return None
return None
def vector_to_map(
samples: Optional[Sequence[Mapping[str, object]]],
labels: Sequence[str],
) -> Dict[Any, float]:
mapping: Dict[Any, float] = {}
if not samples:
return mapping
for sample in samples:
metric_obj = sample.get("metric", {})
if isinstance(metric_obj, Mapping):
metric: Dict[str, object] = dict(cast(Mapping[str, object], metric_obj))
else:
metric = {}
if len(labels) == 1:
key: Any = str(metric.get(labels[0], ""))
else:
key = tuple(str(metric.get(label, "")) for label in labels)
value = _sample_value(sample)
if value is not None:
mapping[key] = value
return mapping
def _normalize_label_value(value: Any) -> str:
if value is None:
return "-"
text = str(value)
return text if text else "-"
def vector_to_labeled_map(
samples: Optional[Sequence[Mapping[str, object]]],
labels: Sequence[str],
) -> Dict[Tuple[str, ...], float]:
mapping: Dict[Tuple[str, ...], float] = {}
if not samples:
return mapping
for sample in samples:
metric_obj = sample.get("metric", {})
if isinstance(metric_obj, Mapping):
metric = dict(cast(Mapping[str, object], metric_obj))
else:
metric = {}
if labels:
key = tuple(_normalize_label_value(metric.get(label)) for label in labels)
else:
key = tuple[str, ...]()
value = _sample_value(sample)
if value is not None:
mapping[key] = value
return mapping
def sum_by_clause(labels: Sequence[str]) -> str:
if labels:
joined = ", ".join(labels)
return f"sum by ({joined})"
return "sum"
def histogram_sum_by_clause(labels: Sequence[str]) -> str:
le_prefixed = ("le", *labels)
joined = ", ".join(le_prefixed)
return f"sum by ({joined})"
def histogram_sum_metric_name(bucket_metric: str) -> str:
if bucket_metric.endswith("_bucket"):
return f"{bucket_metric[: -len('_bucket')]}_sum"
return f"{bucket_metric}_sum"
def histogram_count_metric_name(bucket_metric: str) -> str:
if bucket_metric.endswith("_bucket"):
return f"{bucket_metric[: -len('_bucket')]}_count"
return f"{bucket_metric}_count"
def divide_or_none(numerator: Optional[float], denominator: Optional[float]) -> Optional[float]:
if numerator is None or denominator is None:
return None
if denominator == 0:
return None
return numerator / denominator
def compute_average_time_map(
time_totals: Mapping[Tuple[str, ...], float],
count_totals: Mapping[Tuple[str, ...], float],
) -> Dict[Tuple[str, ...], float]:
averages: Dict[Tuple[str, ...], float] = {}
keys = set(time_totals.keys()).union(count_totals.keys())
for key in keys:
avg = divide_or_none(time_totals.get(key), count_totals.get(key))
if avg is not None:
averages[key] = avg
return averages
def safe_vector(client: PrometheusClient, expr: str) -> Optional[List[Mapping[str, object]]]:
try:
return client.query_vector(expr)
except PrometheusQueryError as exc:
print(f"[warn] Prometheus query failed: {exc} (expr={expr})")
return None
def safe_scalar(client: PrometheusClient, expr: str) -> Optional[float]:
try:
return client.query_scalar(expr)
except PrometheusQueryError as exc:
print(f"[warn] Prometheus query failed: {exc} (expr={expr})")
return None
def fetch_store_statistics(store_url: str, timeout: float) -> Optional[Dict[str, Any]]:
store_url = store_url.rstrip("/")
stats_url = f"{store_url}/statistics"
req = request.Request(stats_url)
try:
with request.urlopen(req, timeout=timeout) as resp:
loaded = json.loads(resp.read().decode())
if isinstance(loaded, Mapping):
return dict(cast(Mapping[str, Any], loaded))
return None
except error.URLError as exc:
print(f"[warn] Failed to fetch store statistics: {exc} (url={stats_url})")
return None
except json.JSONDecodeError as exc:
print(f"[warn] Failed to decode store statistics: {exc} (url={stats_url})")
return None
except TimeoutError as exc:
print(f"[warn] Timeout fetching store statistics: {exc} (url={stats_url})")
return None
@dataclass
class CollectionThroughput:
name: str
count: Optional[float]
per_sec: Optional[float]
@dataclass
class MetricRow:
label_values: Tuple[str, ...]
avg_rate: Optional[float]
max_rate: Optional[float]
min_rate: Optional[float]
p50: Optional[float]
p95: Optional[float]
p99: Optional[float]
max_latency: Optional[float]
time_per_sec: Optional[float]
time_per_request: Optional[float]
avg_rate_delta: Optional[float]
p50_delta: Optional[float]
p95_delta: Optional[float]
time_delta: Optional[float]
time_per_request_delta: Optional[float]
@dataclass(frozen=True)
class MetricGroupSpec:
title: str
histogram_bucket_metric: str
label_names: Tuple[str, ...]
label_headers: Tuple[str, ...]
selector: str = ""
sum_metric: Optional[str] = None
count_metric: Optional[str] = None
def metric_row_sort_key(row: MetricRow) -> Tuple[str, ...]:
return row.label_values
STORE_TOTAL_FIELDS = {
"rollouts": "total_rollouts",
"spans": "total_spans",
"attempts": "total_attempts",
"resources": "total_resources",
"workers": "total_workers",
}
STORE_TOTAL_COLLECTIONS = tuple(STORE_TOTAL_FIELDS.keys())
def _coerce_int(value: Any) -> Optional[int]:
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return value
if isinstance(value, float):
if math.isnan(value):
return None
return int(value)
if isinstance(value, str):
try:
return int(value)
except ValueError:
try:
return int(float(value))
except ValueError:
return None
return None
def extract_store_totals(stats: Optional[Mapping[str, Any]]) -> Dict[str, Optional[int]]:
totals: Dict[str, Optional[int]] = {}
if not stats:
return totals
for display_name, field_name in STORE_TOTAL_FIELDS.items():
if field_name in stats:
totals[display_name] = _coerce_int(stats.get(field_name))
else:
totals[display_name] = None
return totals
def gather_collection_throughput(
client: PrometheusClient, collections: Sequence[str], duration_seconds: float
) -> List[CollectionThroughput]:
rows: List[CollectionThroughput] = []
window = format_window(duration_seconds)
for collection in collections:
# Successful insert operations reflect the number of new records.
expr = (
"sum("
f'increase(mongo_operation_total{{collection="{collection}", operation="insert", status="ok"}}[{window}])'
")"
)
count = safe_scalar(client, expr)
if count is not None and count < 0:
count = 0.0
per_sec = (count / duration_seconds) if (count is not None and duration_seconds > 0) else None
rows.append(CollectionThroughput(collection, count, per_sec))
return rows
def gather_metric_group(
client: PrometheusClient,
spec: MetricGroupSpec,
*,
window: str,
window_seconds: int,
peak_window: str,
subquery_step: str,
half_window: Optional[str],
half_window_seconds: Optional[int],
) -> List[MetricRow]:
label_names = spec.label_names
sum_clause = sum_by_clause(label_names)
hist_clause = histogram_sum_by_clause(label_names)
bucket_metric = f"{spec.histogram_bucket_metric}{spec.selector}" if spec.selector else spec.histogram_bucket_metric
base_sum_metric = spec.sum_metric or histogram_sum_metric_name(spec.histogram_bucket_metric)
sum_metric = f"{base_sum_metric}{spec.selector}" if spec.selector else base_sum_metric
base_count_metric = spec.count_metric or histogram_count_metric_name(spec.histogram_bucket_metric)
count_metric = f"{base_count_metric}{spec.selector}" if spec.selector else base_count_metric
count_total_expr = f"{sum_clause}(increase({count_metric}[{window}]))"
count_total_map = vector_to_labeled_map(safe_vector(client, count_total_expr), label_names)
avg_map = {key: value / window_seconds for key, value in count_total_map.items()} if window_seconds > 0 else {}
peak_expr = f"{sum_clause}(irate({count_metric}[{peak_window}]))"
max_expr = f"max_over_time(({peak_expr})[{window}:{subquery_step}])"
min_expr = f"min_over_time(({peak_expr})[{window}:{subquery_step}])"
max_map = vector_to_labeled_map(safe_vector(client, max_expr), label_names)
min_map = vector_to_labeled_map(safe_vector(client, min_expr), label_names)
p50_map = vector_to_labeled_map(
safe_vector(
client,
f"histogram_quantile(0.50, {hist_clause}(increase({bucket_metric}[{window}])))",
),
label_names,
)
p95_map = vector_to_labeled_map(
safe_vector(
client,
f"histogram_quantile(0.95, {hist_clause}(increase({bucket_metric}[{window}])))",
),
label_names,
)
p99_map = vector_to_labeled_map(
safe_vector(
client,
f"histogram_quantile(0.99, {hist_clause}(increase({bucket_metric}[{window}])))",
),
label_names,
)
max_latency_map = vector_to_labeled_map(
safe_vector(
client,
f"histogram_quantile(1.00, {hist_clause}(increase({bucket_metric}[{window}])))",
),
label_names,
)
time_total_expr = f"{sum_clause}(increase({sum_metric}[{window}]))"
time_total_map = vector_to_labeled_map(safe_vector(client, time_total_expr), label_names)
time_rate_map = {key: value / window_seconds for key, value in time_total_map.items()} if window_seconds > 0 else {}
avg_time_map = compute_average_time_map(time_total_map, count_total_map)
if half_window and half_window_seconds and half_window_seconds > 0:
count_late_expr = f"{sum_clause}(increase({count_metric}[{half_window}]))"
count_early_expr = f"{sum_clause}(increase({count_metric}[{half_window}] offset {half_window}))"
count_late_total_map = vector_to_labeled_map(safe_vector(client, count_late_expr), label_names)
count_early_total_map = vector_to_labeled_map(safe_vector(client, count_early_expr), label_names)
avg_late_map = {key: value / half_window_seconds for key, value in count_late_total_map.items()}
avg_early_map = {key: value / half_window_seconds for key, value in count_early_total_map.items()}
p50_late_expr = f"histogram_quantile(0.50, {hist_clause}(increase({bucket_metric}[{half_window}])))"
p50_early_expr = (
f"histogram_quantile(0.50, {hist_clause}(increase({bucket_metric}[{half_window}] offset {half_window})))"
)
p50_late_map = vector_to_labeled_map(safe_vector(client, p50_late_expr), label_names)
p50_early_map = vector_to_labeled_map(safe_vector(client, p50_early_expr), label_names)
p95_late_expr = f"histogram_quantile(0.95, {hist_clause}(increase({bucket_metric}[{half_window}])))"
p95_early_expr = (
f"histogram_quantile(0.95, {hist_clause}(increase({bucket_metric}[{half_window}] offset {half_window})))"
)
p95_late_map = vector_to_labeled_map(safe_vector(client, p95_late_expr), label_names)
p95_early_map = vector_to_labeled_map(safe_vector(client, p95_early_expr), label_names)
time_late_expr = f"{sum_clause}(increase({sum_metric}[{half_window}]))"
time_early_expr = f"{sum_clause}(increase({sum_metric}[{half_window}] offset {half_window}))"
time_late_total_map = vector_to_labeled_map(safe_vector(client, time_late_expr), label_names)
time_early_total_map = vector_to_labeled_map(safe_vector(client, time_early_expr), label_names)
time_late_map = {key: value / half_window_seconds for key, value in time_late_total_map.items()}
time_early_map = {key: value / half_window_seconds for key, value in time_early_total_map.items()}
avg_time_late_map = compute_average_time_map(time_late_total_map, count_late_total_map)
avg_time_early_map = compute_average_time_map(time_early_total_map, count_early_total_map)
else:
count_late_total_map: Dict[Tuple[str, ...], float] = {}
count_early_total_map: Dict[Tuple[str, ...], float] = {}
avg_late_map: Dict[Tuple[str, ...], float] = {}
avg_early_map: Dict[Tuple[str, ...], float] = {}
p50_late_map: Dict[Tuple[str, ...], float] = {}
p50_early_map: Dict[Tuple[str, ...], float] = {}
p95_late_map: Dict[Tuple[str, ...], float] = {}
p95_early_map: Dict[Tuple[str, ...], float] = {}
time_late_map: Dict[Tuple[str, ...], float] = {}
time_early_map: Dict[Tuple[str, ...], float] = {}
time_late_total_map = {}
time_early_total_map = {}
avg_time_late_map = {}
avg_time_early_map = {}
all_keys: Set[Tuple[str, ...]] = set()
all_keys.update(count_total_map.keys())
all_keys.update(avg_map.keys())
all_keys.update(max_map.keys())
all_keys.update(min_map.keys())
all_keys.update(p50_map.keys())
all_keys.update(p95_map.keys())
all_keys.update(p99_map.keys())
all_keys.update(max_latency_map.keys())
all_keys.update(time_rate_map.keys())
all_keys.update(avg_time_map.keys())
all_keys.update(count_late_total_map.keys())
all_keys.update(count_early_total_map.keys())
all_keys.update(avg_late_map.keys())
all_keys.update(avg_early_map.keys())
all_keys.update(p50_late_map.keys())
all_keys.update(p50_early_map.keys())
all_keys.update(p95_late_map.keys())
all_keys.update(p95_early_map.keys())
all_keys.update(time_late_map.keys())
all_keys.update(time_early_map.keys())
all_keys.update(avg_time_late_map.keys())
all_keys.update(avg_time_early_map.keys())
if not all_keys:
return []
def build_delta(
late_map: Mapping[Tuple[str, ...], float],
early_map: Mapping[Tuple[str, ...], float],
key: Tuple[str, ...],
) -> Optional[float]:
late = late_map.get(key)
early = early_map.get(key)
if late is None or early is None:
return None
return late - early
rows: List[MetricRow] = []
for key in sorted(all_keys):
rows.append(
MetricRow(
label_values=key,
avg_rate=avg_map.get(key),
max_rate=max_map.get(key),
min_rate=min_map.get(key),
p50=p50_map.get(key),
p95=p95_map.get(key),
p99=p99_map.get(key),
max_latency=max_latency_map.get(key),
time_per_sec=time_rate_map.get(key),
time_per_request=avg_time_map.get(key),
avg_rate_delta=build_delta(avg_late_map, avg_early_map, key),
p50_delta=build_delta(p50_late_map, p50_early_map, key),
p95_delta=build_delta(p95_late_map, p95_early_map, key),
time_delta=build_delta(time_late_map, time_early_map, key),
time_per_request_delta=build_delta(avg_time_late_map, avg_time_early_map, key),
)
)
return rows
def gather_diagnostics(client: PrometheusClient, window: str) -> Dict[str, Any]:
diagnostics: Dict[str, Any] = {}
diagnostics["mongo_ops"] = vector_to_map(
safe_vector(
client,
f"sum by (operation)(rate(mongo_operation_total{{operation!='ensure_collection'}}[{window}]))",
),
("operation",),
)
diagnostics["mongo_latency_p50"] = vector_to_map(
safe_vector(
client,
f"histogram_quantile(0.50, sum by (le, operation)(rate(mongo_operation_duration_seconds_bucket{{operation!='ensure_collection'}}[{window}])))",
),
("operation",),
)
diagnostics["mongo_latency_p95"] = vector_to_map(
safe_vector(
client,
f"histogram_quantile(0.95, sum by (le, operation)(rate(mongo_operation_duration_seconds_bucket{{operation!='ensure_collection'}}[{window}])))",
),
("operation",),
)
diagnostics["mongo_latency_p99"] = vector_to_map(
safe_vector(
client,
f"histogram_quantile(0.99, sum by (le, operation)(rate(mongo_operation_duration_seconds_bucket{{operation!='ensure_collection'}}[{window}])))",
),
("operation",),
)
opcounters_samples = safe_vector(client, f"sum by (legacy_op_type)(rate(mongodb_ss_opcounters[{window}]))")
mongo_opcounters: Dict[str, float] = {}
if opcounters_samples:
for sample in opcounters_samples:
metric_obj = sample.get("metric", {})
if isinstance(metric_obj, Mapping):
metric: Dict[str, object] = dict(cast(Mapping[str, object], metric_obj))
else:
metric = {}
label_value = metric.get("legacy_op_type") or metric.get("type")
label = str(label_value) if label_value is not None else ""
value = _sample_value(sample)
if value is not None:
mongo_opcounters[str(label or "-")] = value
diagnostics["mongo_opcounters"] = mongo_opcounters
diagnostics["mongo_connections"] = safe_scalar(client, "avg(mongodb_ss_connections{conn_type='current'})")
diagnostics["memory_lock_rate"] = vector_to_map(
safe_vector(client, f"sum by (collection)(rate(memory_collection_lock_rate_total[{window}]))"),
("collection",),
)
diagnostics["memory_lock_p50"] = vector_to_map(
safe_vector(
client,
f"histogram_quantile(0.50, sum by (le, collection)(rate(memory_collection_lock_latency_seconds_bucket[{window}])))",
),
("collection",),
)
diagnostics["memory_lock_p95"] = vector_to_map(
safe_vector(
client,
f"histogram_quantile(0.95, sum by (le, collection)(rate(memory_collection_lock_latency_seconds_bucket[{window}])))",
),
("collection",),
)
diagnostics["memory_lock_p99"] = vector_to_map(
safe_vector(
client,
f"histogram_quantile(0.99, sum by (le, collection)(rate(memory_collection_lock_latency_seconds_bucket[{window}])))",
),
("collection",),
)
diagnostics["cpu_usage"] = safe_scalar(client, f"1 - avg(rate(node_cpu_seconds_total{{mode='idle'}}[{window}]))")
diagnostics["memory_total"] = safe_scalar(client, "avg(node_memory_MemTotal_bytes)")
diagnostics["memory_available"] = safe_scalar(client, "avg(node_memory_MemAvailable_bytes)")
diagnostics["network_rx"] = safe_scalar(
client,
f"sum(rate(node_network_receive_bytes_total{{device!~'lo|docker.*'}}[{window}]))",
)
diagnostics["network_tx"] = safe_scalar(
client,
f"sum(rate(node_network_transmit_bytes_total{{device!~'lo|docker.*'}}[{window}]))",
)
diagnostics["disk_read_ops"] = safe_scalar(client, f"sum(rate(node_disk_reads_completed_total[{window}]))")
diagnostics["disk_write_ops"] = safe_scalar(client, f"sum(rate(node_disk_writes_completed_total[{window}]))")
diagnostics["disk_read_bytes"] = safe_scalar(client, f"sum(rate(node_disk_read_bytes_total[{window}]))")
diagnostics["disk_write_bytes"] = safe_scalar(client, f"sum(rate(node_disk_written_bytes_total[{window}]))")
return diagnostics
def render_table(headers: Sequence[str], rows: Sequence[Sequence[str]]) -> List[str]:
if not rows:
return [f"(no data for {headers})"]
widths = [len(h) for h in headers]
rendered: List[List[str]] = []
for row in rows:
rendered_row = [str(cell) for cell in row]
for idx, cell in enumerate(rendered_row):
widths[idx] = max(widths[idx], len(cell))
rendered.append(rendered_row)
lines = [
" | ".join(headers[idx].ljust(widths[idx]) for idx in range(len(headers))),
"-+-".join("-" * widths[idx] for idx in range(len(headers))),
]
for row in rendered:
lines.append(" | ".join(row[idx].ljust(widths[idx]) for idx in range(len(headers))))
return lines
def fmt_rate(value: Optional[float]) -> str:
if value is None or math.isnan(value):
return "-"
return f"{value:.2f}/s"
def fmt_latency(value: Optional[float]) -> str:
if value is None or math.isnan(value):
return "-"
if abs(value) < 10:
return f"{value * 1e3:.2f} ms"
return f"{value:.2f} s"
def fmt_bytes(value: Optional[float]) -> str:
if value is None or math.isnan(value):
return "-"
units = ["B", "KB", "MB", "GB", "TB", "PB"]
idx = 0
current = value
while current >= 1024 and idx < len(units) - 1:
current /= 1024
idx += 1
return f"{current:.2f} {units[idx]}"
def fmt_percentage(value: Optional[float]) -> str:
if value is None or math.isnan(value):
return "-"
return f"{value * 100:4.1f}%"
def section(title: str, body: Iterable[str]) -> List[str]:
lines = [f"## {title}"]
lines.extend(body)
lines.append("")
return lines
def render_metric_group_table(
spec: MetricGroupSpec,
rows: Sequence[MetricRow],
extra_columns: Optional[Sequence[Tuple[str, Callable[[MetricRow], str]]]] = None,
) -> List[str]:
headers = list(spec.label_headers)
headers.extend(
[
"Avg Rate/s",
"Max Rate/s",
"Min Rate/s",
"P50",
"P95",
"P99",
"Max Latency",
"Time/s",
"Avg Time/req",
"Avg Rate Δ",
"P50 Δ",
"P95 Δ",
"Time Δ",
"Avg Time/req Δ",
]
)
column_renderers: Sequence[Tuple[str, Callable[[MetricRow], str]]] = extra_columns or ()
for header, _ in column_renderers:
headers.append(header)
if not rows:
return render_table(headers, [])
sorted_rows = sorted(rows, key=metric_row_sort_key)
rendered_rows: List[List[str]] = []
for row in sorted_rows:
label_cells = list(row.label_values) if spec.label_headers else []
metrics = [
fmt_rate(row.avg_rate),
fmt_rate(row.max_rate),
fmt_rate(row.min_rate),
fmt_latency(row.p50),
fmt_latency(row.p95),
fmt_latency(row.p99),
fmt_latency(row.max_latency),
fmt_latency(row.time_per_sec),
fmt_latency(row.time_per_request),
fmt_rate(row.avg_rate_delta),
fmt_latency(row.p50_delta),
fmt_latency(row.p95_delta),
fmt_latency(row.time_delta),
fmt_latency(row.time_per_request_delta),
]
extra_cells = [renderer(row) for _, renderer in column_renderers]
rendered_rows.append(label_cells + metrics + extra_cells)
return render_table(headers, rendered_rows)
def make_time_share_column(
*,
label_index: int,
column_title: str,
time_per_sec_map: Mapping[str, Optional[float]],
) -> Tuple[str, Callable[[MetricRow], str]]:
def render_cell(row: MetricRow) -> str:
if not row.label_values or len(row.label_values) <= label_index:
return "-"
label_value = row.label_values[label_index]
store_time = time_per_sec_map.get(label_value)
collection_time = row.time_per_sec
if (
store_time is None
or collection_time is None
or math.isnan(store_time)
or math.isnan(collection_time)
or store_time <= 0
):
return "-"
return fmt_percentage(collection_time / store_time)
return (column_title, render_cell)
def main(argv: Optional[Sequence[str]] = None) -> None:
args = parse_args(argv)
end = parse_timestamp(args.end, default=dt.datetime.now(dt.timezone.utc))
if end is None:
raise SystemExit("End timestamp could not be determined.")
start = parse_timestamp(args.start)
if start is None:
duration = parse_duration(args.duration)
start = end - duration
assert start is not None
duration_seconds = max((end - start).total_seconds(), 1.0)
window_seconds = clamp_window_seconds(duration_seconds)
window = format_window(duration_seconds)
peak_window = compute_peak_window(duration_seconds)
subquery_step = compute_subquery_step(duration_seconds)
half_window_seconds = window_seconds // 2 if window_seconds // 2 >= 1 else None
half_window = format_window(half_window_seconds) if half_window_seconds else None
client = PrometheusClient(args.prom_url, timeout=args.timeout, default_time=end)
store_stats = fetch_store_statistics(args.store_url, timeout=args.timeout)
store_totals = extract_store_totals(store_stats)
lines: List[str] = [
f"Agent Lightning benchmark report",
f"Range: {start.isoformat()}{end.isoformat()} ({duration_seconds:.0f}s window)",
f"Prometheus: {args.prom_url}",
f"Store: {args.store_url}",
"",
]
# Throughput
throughput_rows = gather_collection_throughput(
client, collections=STORE_TOTAL_COLLECTIONS, duration_seconds=duration_seconds
)
throughput_table: List[List[str]] = []
for item in throughput_rows:
store_total = store_totals.get(item.name)
if store_total is not None:
count_value: Optional[int] = store_total
elif item.count is not None:
count_value = int(item.count)
else:
count_value = None
if count_value is None:
count_str = "-"
else:
count_str = f"{count_value:,}"
if count_value is not None and duration_seconds > 0:
per_sec_value = float(count_value) / duration_seconds
else:
per_sec_value = item.per_sec
throughput_table.append([item.name, count_str, fmt_rate(per_sec_value)])
lines.extend(
section(
"Rollout / Attempt / Span / Resource / Worker Throughput",
render_table(["Collection", "Count", "Per Sec"], throughput_table),
)
)
metric_categories: Sequence[Tuple[str, Sequence[MetricGroupSpec]]] = [
(
"HTTP Metrics",
(
MetricGroupSpec(
title="agl.http ungrouped",
histogram_bucket_metric="agl_http_latency_bucket",
label_names=tuple(),
label_headers=tuple(),
),
MetricGroupSpec(
title="agl.http grouped by path, method",
histogram_bucket_metric="agl_http_latency_bucket",
label_names=("path", "method"),
label_headers=("Path", "Method"),
),
MetricGroupSpec(
title="agl.http grouped by path, method, status",
histogram_bucket_metric="agl_http_latency_bucket",
label_names=("path", "method", "status"),
label_headers=("Path", "Method", "Status"),
),
),
),
(
"Store Metrics",
(
MetricGroupSpec(
title="agl.store ungrouped",
histogram_bucket_metric="agl_store_latency_bucket",
label_names=tuple(),
label_headers=tuple(),
),
MetricGroupSpec(
title="agl.store grouped by method",
histogram_bucket_metric="agl_store_latency_bucket",
label_names=("method",),
label_headers=("Method",),
),
MetricGroupSpec(
title="agl.store grouped by method, status",
histogram_bucket_metric="agl_store_latency_bucket",
label_names=("method", "status"),
label_headers=("Method", "Status"),
),
MetricGroupSpec(
title="agl.store grouped by store_pubmeth, method, status",
histogram_bucket_metric="agl_store_latency_bucket",
label_names=("store_pubmeth", "method", "status"),
label_headers=("Store Method", "Private Method", "Status"),
),
),
),
(
"Rollout Outcomes",
(
MetricGroupSpec(
title="agl.rollouts ungrouped",
histogram_bucket_metric="agl_rollouts_duration_bucket",
label_names=tuple(),
label_headers=tuple(),
),
MetricGroupSpec(
title="agl.rollouts grouped by status",
histogram_bucket_metric="agl_rollouts_duration_bucket",
label_names=("status",),
label_headers=("Status",),
),
),
),
(
"Collection Metrics",
(
MetricGroupSpec(
title="agl.collections grouped by store_pubmeth, collection, operation",
histogram_bucket_metric="agl_collections_latency_bucket",
label_names=("store_pubmeth", "collection"),
label_headers=("Store Method", "Collection"),
),
MetricGroupSpec(
title="agl.collections grouped by store_pubmeth, collection, operation, status",
histogram_bucket_metric="agl_collections_latency_bucket",
label_names=("store_pubmeth", "collection", "operation", "status"),
label_headers=("Store Method", "Collection", "Operation", "Status"),
),
MetricGroupSpec(
title="agl.collections grouped by store_privmeth, collection, operation, status",
histogram_bucket_metric="agl_collections_latency_bucket",
label_names=("store_privmeth", "collection", "operation", "status"),
label_headers=("Store Priv Meth", "Collection", "Operation", "Status"),
),
MetricGroupSpec(
title="agl.collections grouped by collection, operation, status",
histogram_bucket_metric="agl_collections_latency_bucket",
label_names=("collection", "operation", "status"),
label_headers=("Collection", "Operation", "Status"),
),
),
),
]
store_method_time_per_sec: Dict[str, Optional[float]] = {}
for category_title, specs in metric_categories:
category_lines: List[str] = []
for idx, spec in enumerate(specs):
rows = gather_metric_group(
client,
spec,
window=window,
window_seconds=window_seconds,
peak_window=peak_window,
subquery_step=subquery_step,
half_window=half_window,
half_window_seconds=half_window_seconds,
)
if spec.histogram_bucket_metric == "agl_store_latency_bucket" and spec.label_names == ("method",):
store_method_time_per_sec = {
row.label_values[0]: row.time_per_sec
for row in rows
if row.label_values and len(row.label_values) == 1
}
extra_column_specs: List[Tuple[str, Callable[[MetricRow], str]]] = []
if "store_pubmeth" in spec.label_names:
pubmeth_index = spec.label_names.index("store_pubmeth")
extra_column_specs.append(
make_time_share_column(
label_index=pubmeth_index,
column_title="Share %",
time_per_sec_map=store_method_time_per_sec,
)
)
if "store_privmeth" in spec.label_names:
privmeth_index = spec.label_names.index("store_privmeth")
extra_column_specs.append(
make_time_share_column(
label_index=privmeth_index,
column_title="Share (Priv) %",
time_per_sec_map=store_method_time_per_sec,
)
)
extra_columns = extra_column_specs or None
category_lines.append("### " + spec.title)
category_lines.extend(render_metric_group_table(spec, rows, extra_columns=extra_columns))
if idx != len(specs) - 1:
category_lines.append("")
lines.extend(section(category_title, category_lines))
# Diagnostics
diag = gather_diagnostics(client, window)
diagnostics_blocks: List[List[str]] = []
mongo_ops = cast(Dict[str, float], diag.get("mongo_ops", {}))
mongo_latency_p50 = cast(Dict[str, float], diag.get("mongo_latency_p50", {}))
mongo_latency_p95 = cast(Dict[str, float], diag.get("mongo_latency_p95", {}))
mongo_latency_p99 = cast(Dict[str, float], diag.get("mongo_latency_p99", {}))
mongo_op_keys = sorted(
{
*mongo_ops.keys(),
*mongo_latency_p50.keys(),
*mongo_latency_p95.keys(),
*mongo_latency_p99.keys(),
},
key=str,
)
mongo_ops_rows = [
[
op or "-",
fmt_rate(mongo_ops.get(op)),
fmt_latency(mongo_latency_p50.get(op)),
fmt_latency(mongo_latency_p95.get(op)),
fmt_latency(mongo_latency_p99.get(op)),
]
for op in mongo_op_keys
]
diagnostics_blocks.append(render_table(["Mongo Operation", "Ops/s", "P50", "P95", "P99"], mongo_ops_rows))
mongo_opcounters = cast(Dict[str, float], diag.get("mongo_opcounters", {}))
mongo_opcounters_rows = [
[op_type or "-", fmt_rate(rate)]
for op_type, rate in sorted(mongo_opcounters.items(), key=lambda item: str(item[0]))
]
diagnostics_blocks.append(render_table(["MongoDB Opcounter", "Ops/s"], mongo_opcounters_rows))
mongo_misc_rows: List[List[str]] = []
if diag.get("mongo_connections") is not None:
mongo_misc_rows.append(["MongoDB connections (avg)", f"{diag['mongo_connections']:.2f}"])
if mongo_misc_rows:
diagnostics_blocks.append(render_table(["Mongo Metric", "Value"], mongo_misc_rows))
node_rows: List[List[str]] = []
if diag.get("cpu_usage") is not None:
node_rows.append(["CPU usage", fmt_percentage(diag["cpu_usage"])])
mem_total = diag.get("memory_total")
mem_available = diag.get("memory_available")
if mem_total and mem_available:
used = mem_total - mem_available
node_rows.append(
["Memory usage", f"{fmt_bytes(used)} / {fmt_bytes(mem_total)} ({fmt_percentage(used / mem_total)})"]
)
node_rows.append(["Network rx", f"{fmt_bytes(diag.get('network_rx'))}/s"])
node_rows.append(["Network tx", f"{fmt_bytes(diag.get('network_tx'))}/s"])
node_rows.append(["Disk read ops", fmt_rate(diag.get("disk_read_ops"))])
node_rows.append(["Disk read bytes", f"{fmt_bytes(diag.get('disk_read_bytes'))}/s"])
node_rows.append(["Disk write ops", fmt_rate(diag.get("disk_write_ops"))])
node_rows.append(["Disk write bytes", f"{fmt_bytes(diag.get('disk_write_bytes'))}/s"])
diagnostics_blocks.append(render_table(["Node Metric", "Value"], node_rows))
diagnostics_lines: List[str] = []
for idx, block in enumerate(diagnostics_blocks):
diagnostics_lines.extend(block)
if idx != len(diagnostics_blocks) - 1:
diagnostics_lines.append("")
lines.extend(section("Diagnostics", diagnostics_lines))
print("\n".join(lines))
if __name__ == "__main__": # pragma: no cover - manual execution
main()