blob: 7f13fdced252ad44a95d03dc48923c121bf79808 [file]
# SPDX-License-Identifier: Apache-2.0
#
# Modifications by Apache Solr contributors; see git log for details.
# Licensed under the Apache License, Version 2.0.
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
def render_results_html(test_run, cfg) -> str:
"""
Build an HTML benchmark report for the given TestRun.
"""
# 1) Normalize the test_run to a dict
if isinstance(test_run, dict):
doc = test_run
elif hasattr(test_run, "as_dict"):
doc = test_run.as_dict()
else:
# fallback minimal dict
doc = {
"test-run-id": test_run.test_run_id,
"benchmark-version": test_run.benchmark_version,
"benchmark-revision": test_run.benchmark_revision,
"environment": test_run.environment_name,
"pipeline": test_run.pipeline,
"workload": test_run.workload_name,
"test-procedure": getattr(test_run, "test_procedure_name", None),
"cluster": {
"revision": test_run.revision,
"distribution-version": test_run.distribution_version,
"distribution-flavor": test_run.distribution_flavor,
"provision-config-revision": test_run.provision_config_revision,
}
}
if getattr(test_run, "results", None):
# results might already be a dict or an object
if isinstance(test_run.results, dict):
doc["results"] = test_run.results
elif hasattr(test_run.results, "as_dict"):
doc["results"] = test_run.results.as_dict()
# 2) Pull top-level fields
test_id = doc.get("test-run-id", "<unknown>")
orbit_ver = doc.get("benchmark-version", "")
orbit_rev = doc.get("benchmark-revision", "")
environment = doc.get("environment", "")
pipeline = doc.get("pipeline", "")
workload = doc.get("workload", "")
test_procedure= doc.get("test-procedure", "")
# 3) Cluster info
cluster_info = doc.get("cluster", {})
distro_ver = cluster_info.get("distribution-version", "")
distro_flav = cluster_info.get("distribution-flavor", "")
prov_conf_rev = cluster_info.get("provision-config-revision", None)
# 4) Config table dict
config_dict = {
"Solr Orbit Version": orbit_ver,
"Solr Orbit Revision (git)": orbit_rev,
"Environment": environment,
"Pipeline": pipeline,
"Workload": workload,
"Test Procedure": test_procedure,
"Distribution Version": distro_ver,
"Distribution Flavor": distro_flav,
"Provision Config Revision": prov_conf_rev,
}
# 5) Extract op_metrics
results_dict = doc.get("results", {}) or {}
op_metrics = results_dict.get("op_metrics", [])
# Build rows
table_rows = []
for item in op_metrics:
th = item.get("throughput", {})
st = item.get("service_time", {})
clients = item.get("search_clients") or item.get("clients", "") or "–"
table_rows.append({
"task": item.get("task", ""),
"operation": item.get("operation", ""),
"throughput_mean": th.get("mean"),
"throughput_unit": th.get("unit", ""),
"service_time_mean": st.get("mean"),
"service_time_unit": st.get("unit", ""),
"search_clients": clients,
})
# 6) Render helpers
def render_config_table(cfg_d):
rows = ""
for k, v in cfg_d.items():
disp = "N/A" if v is None else v
rows += f"<tr><th>{k}</th><td>{disp}</td></tr>"
return f"<table class='config-table'><tbody>{rows}</tbody></table>"
def render_metrics_table(rows):
header = (
"<tr>"
"<th>Task</th><th>Operation</th>"
"<th>Throughput (mean)</th>"
"<th>Service Time (mean)</th>"
"<th>Search Clients</th>"
"</tr>"
)
body = ""
for r in rows:
th_val = f"{r['throughput_mean']} {r['throughput_unit']}" if r['throughput_mean'] is not None else "–"
st_val = f"{r['service_time_mean']} {r['service_time_unit']}" if r['service_time_mean'] is not None else "–"
body += (
"<tr>"
f"<td>{r['task']}</td>"
f"<td>{r['operation']}</td>"
f"<td>{th_val}</td>"
f"<td>{st_val}</td>"
f"<td>{r['search_clients']}</td>"
"</tr>"
)
return f"<table class='metrics-table'><thead>{header}</thead><tbody>{body}</tbody></table>"
# 7) Put it all together
cfg_table_html = render_config_table(config_dict)
metrics_html = render_metrics_table(table_rows)
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Solr Orbit Report &mdash; {test_id}</title>
<style>
/* ───────────────────────────────────────────────────────────────── */
/* Base Styles + Resets */
body {{
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
background-color: #f5f7fa;
color: #333;
}}
a {{ text-decoration: none; color: inherit; }}
/* Header Bar (light‐blue version) */
header {{
background-color: #2374aa;
color: white;
padding: 1rem 2rem;
display: flex;
align-items: center;
}}
header h1 {{
margin: 0;
font-size: 1.5rem;
font-weight: 400;
color: #e1f0ff; /* a very light, almost‐white blue */
}}
/* Container to center the content */
.container {{
max-width: 1100px;
margin: 2rem auto;
padding: 0 1rem;
}}
/* Card Styles */
.card {{
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
margin-bottom: 2rem;
padding: 1.5rem 2rem;
}}
.card h2 {{
margin-top: 0;
margin-bottom: 0.75rem;
color: #2374aa;
font-size: 1.35rem;
font-weight: 500;
}}
.subtitle {{
margin-top: 0.25rem;
margin-bottom: 1rem;
font-size: 0.95rem;
color: #555;
}}
/* Configuration Table (key/value pairs) */
table.config-table {{
width: 100%;
border-collapse: collapse;
}}
table.config-table th,
table.config-table td {{
text-align: left;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid #e0e0e0;
}}
table.config-table th {{
width: 30%;
font-weight: 500;
color: #444;
background-color: #f0f4f8;
}}
/* Metrics Table (per‐operation) */
table.metrics-table {{
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}}
table.metrics-table th,
table.metrics-table td {{
border: 1px solid #ddd;
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
}}
table.metrics-table th {{
background-color: #e8f2fa;
color: #2374aa;
font-weight: 500;
text-align: left;
}}
table.metrics-table tr:nth-child(even) {{
background-color: #fbfcfe;
}}
/* Responsive tweaks */
@media (max-width: 800px) {{
header {{
flex-direction: column;
align-items: flex-start;
}}
header h1 {{ font-size: 1.25rem; }}
.container {{
margin: 1rem auto;
padding: 0 0.5rem;
}}
table.metrics-table th,
table.metrics-table td {{
font-size: 0.8rem;
padding: 0.4rem 0.5rem;
}}
}}
/* ───────────────────────────────────────────────────────────────── */
</style>
</head>
<body>
<!-- ─── Header Bar ───────────────────────────────────────────────── -->
<header>
<h1>Solr Orbit Report: <code>{test_id}</code></h1>
</header>
<!-- ─── Main Content ──────────────────────────────────────────────── -->
<div class="container">
<!-- Configuration Card -->
<div class="card">
<h2>Cluster & Benchmark Configuration</h2>
<div class="subtitle">
Solr Orbit version, Git revision, environment, pipeline, workload, distro info, etc.
</div>
{cfg_table_html}
</div>
<!-- Metrics Card -->
<div class="card">
<h2>Redline Results (Per Task / Operation)</h2>
<div class="subtitle">
Throughput &amp; Service Time (mean), plus any “search clients” used.
</div>
{metrics_html}
</div>
</div>
</body>
</html>
"""