From 9ae56312139c168f8b30fa87ffa7c2dc81a9f5f3 Mon Sep 17 00:00:00 2001 From: Yoshifumi Nakamura Date: Sun, 21 Jun 2026 11:05:36 +0900 Subject: [PATCH] Improve estimated detail dashboard layout Reorder the estimated detail view so the system comparison and section breakdowns are shown first, move package comparison into its own full-width section, and format breakdown/package numbers for dashboard readability. Also stabilize the Usage Report node-hour table column widths so grouped system headers do not stretch the first period column. Signed-off-by: Yoshifumi Nakamura --- .../templates/_estimated_breakdown_card.html | 231 ++++++++++-------- .../_usage_report_node_hours_section.html | 11 + result_server/templates/estimated_detail.html | 83 ++++--- result_server/templates/usage_report.html | 8 + .../tests/test_estimated_detail_template.py | 12 +- .../tests/test_portal_list_templates.py | 54 ++++ result_server/utils/estimated_detail_view.py | 124 +++++++++- 7 files changed, 383 insertions(+), 140 deletions(-) diff --git a/result_server/templates/_estimated_breakdown_card.html b/result_server/templates/_estimated_breakdown_card.html index ba39f2a..3d9bedc 100644 --- a/result_server/templates/_estimated_breakdown_card.html +++ b/result_server/templates/_estimated_breakdown_card.html @@ -10,75 +10,142 @@ {% endif %} {%- endmacro %} -{% macro render_kernel_package_comparisons(item) -%} - {% set kernel_summaries = item.get('metrics', {}).get('kernel_summaries', []) %} - {% if kernel_summaries %} -
+{% macro render_section_package_comparison_table(heading, items, first_column_label, first_column_key, join_list_values=False) -%} + {% set ns = namespace(has_candidates=False, has_kernels=False) %} + {% for item in items %} + {% if item.get('candidate_estimates') %} + {% set ns.has_candidates = True %} + {% endif %} + {% if item.get('metrics', {}).get('kernel_summaries') %} + {% set ns.has_kernels = True %} + {% endif %} + {% endfor %} + {% if ns.has_candidates or ns.has_kernels %} +

{{ heading }}

+ {% if ns.has_candidates %} +
+ + + + + + + + + + + + + {% for item in items %} + {% set first_value = item.get(first_column_key, 'N/A') %} + {% if join_list_values and first_value is iterable and first_value is not string %} + {% set first_value = first_value | join(', ') %} + {% endif %} + {% for candidate in item.get('candidate_estimates', []) %} + + + + + + + + + {% endfor %} + {% endfor %} + +
{{ first_column_label }}Selected PackageCandidate PackageCandidate TimeTime RatioStatus
{{ first_value }}{{ item.get('estimation_package', 'N/A') }}{{ candidate.get('estimation_package', 'N/A') }}{{ candidate.get('time_display', candidate.get('time', 'N/A')) }}{{ candidate.get('time_ratio_display', candidate.get('metrics', {}).get('time_ratio_predicted_over_source', 'N/A')) }}{{ candidate.get('package_applicability', {}).get('status', 'N/A') }}
+
+ {% endif %} + {% if ns.has_kernels %} +
Kernel package comparison; metrics are shown as reported by each package.
- {% for kernel in kernel_summaries %} -
-
{{ kernel.get('name', 'N/A') }}
- - - - - - - - - - - - - + {% for item in items %} + {% set first_value = item.get(first_column_key, 'N/A') %} + {% if join_list_values and first_value is iterable and first_value is not string %} + {% set first_value = first_value | join(', ') %} + {% endif %} + {% for kernel in item.get('metrics', {}).get('kernel_summaries', []) %} +
+
{{ first_column_label }}: {{ first_value }} / Kernel: {{ kernel.get('name', 'N/A') }}
+
PackageSamplesSource Mean (ns)Predicted Mean (ns)RatioSource GPUTarget GPU
+ + + + + + + + + + + + + {% for package in kernel.get('package_summaries', []) %} + + + + + + + + + + {% endfor %} + +
PackageSamplesSource Mean (ns)Predicted Mean (ns)RatioSource GPUTarget GPU
{{ package.get('estimation_package', 'N/A') }}{{ package.get('sample_count', 'N/A') }}{{ package.get('source_time_ns_mean_display', package.get('source_time_ns_mean', 'N/A')) }}{{ package.get('predicted_time_ns_mean_display', package.get('predicted_time_ns_mean', 'N/A')) }}{{ package.get('mean_time_ratio_display', package.get('mean_time_ratio_predicted_over_source', 'N/A')) }}{{ package.get('source_gpus', []) | join(', ') }}{{ package.get('target_gpus', []) | join(', ') }}
{% for package in kernel.get('package_summaries', []) %} - - {{ package.get('estimation_package', 'N/A') }} - {{ package.get('sample_count', 'N/A') }} - {{ package.get('source_time_ns_mean', 'N/A') }} - {{ package.get('predicted_time_ns_mean', 'N/A') }} - {{ package.get('mean_time_ratio_predicted_over_source', 'N/A') }} - {{ package.get('source_gpus', []) | join(', ') }} - {{ package.get('target_gpus', []) | join(', ') }} - {% if package.get('metric_comparisons') %} - - -
-
{{ package.get('estimation_package', 'N/A') }} metrics
- - - - - - - - - - - - {% for metric in package.get('metric_comparisons', []) %} - - - - - - - - {% endfor %} - -
MetricSamplesSource MeanPredicted MeanRatio
{{ metric.get('name', 'N/A') }}{{ metric.get('sample_count', 'N/A') }}{{ metric.get('source_value_mean', 'N/A') }}{{ metric.get('predicted_value_mean', 'N/A') }}{{ metric.get('ratio_predicted_over_source_mean', 'N/A') }}
-
- - +
+
{{ package.get('estimation_package', 'N/A') }} metrics
+ + + + + + + + + + + + {% for metric in package.get('metric_comparisons', []) %} + + + + + + + + {% endfor %} + +
MetricSamplesSource MeanPredicted MeanRatio
{{ metric.get('name', 'N/A') }}{{ metric.get('sample_count', 'N/A') }}{{ metric.get('source_value_mean_display', metric.get('source_value_mean', 'N/A')) }}{{ metric.get('predicted_value_mean_display', metric.get('predicted_value_mean', 'N/A')) }}{{ metric.get('ratio_display', metric.get('ratio_predicted_over_source_mean', 'N/A')) }}
+
{% endif %} {% endfor %} - - -
+
+ {% endfor %} {% endfor %}
{% endif %} + {% endif %} +{%- endmacro %} + +{% macro render_package_comparison_card(title, breakdown, empty_note) -%} +
+

{{ title }}

+ {% set ns = namespace(has_data=False) %} + {% for item in breakdown.get('sections', []) + breakdown.get('overlaps', []) %} + {% if item.get('candidate_estimates') or item.get('metrics', {}).get('kernel_summaries') %} + {% set ns.has_data = True %} + {% endif %} + {% endfor %} + {% if ns.has_data %} + {{ render_section_package_comparison_table('Sections', breakdown.get('sections', []), 'Section', 'name') }} + {{ render_section_package_comparison_table('Overlaps', breakdown.get('overlaps', []), 'Overlap Sections', 'sections', True) }} + {% else %} +
{{ empty_note }}
+ {% endif %} +
{%- endmacro %} {% macro render_breakdown_table(heading, items, first_column_label, first_column_key, join_list_values=False) -%} @@ -89,8 +156,9 @@

{{ heading }}

{{ first_column_label }} - Bench Time - Estimated Time + Before Scaling + After Scaling + Time Ratio Package Scaling Fallback @@ -105,45 +173,14 @@

{{ heading }}

{% endif %} {{ first_value }} - {{ item.get('bench_time', item.get('time', 'N/A')) }} - {{ item.get('time', 'N/A') }} + {{ item.get('before_scaling_display', item.get('bench_time', item.get('time', 'N/A'))) }} + {{ item.get('after_scaling_display', item.get('time', 'N/A')) }} + {{ item.get('time_ratio_display', item.get('metrics', {}).get('time_ratio_predicted_over_source', 'N/A')) }} {{ item.get('estimation_package', 'N/A') }} {{ item.get('scaling_method', 'N/A') }} {{ item.get('fallback_used') or '-' }} {{ render_applicability_cell(item.get('package_applicability', {})) }} - {% if item.get('candidate_estimates') %} - - -
-
Candidate estimates; mean time is used for FOM composition.
- - - - - - {% for candidate in item.get('candidate_estimates', []) %} - - - - - - - - {% endfor %} - -
PackageTimeScalingStatusTime Ratio
{{ candidate.get('estimation_package', 'N/A') }}{{ candidate.get('time', 'N/A') }}{{ candidate.get('scaling_method', 'N/A') }}{{ candidate.get('package_applicability', {}).get('status', 'N/A') }}{{ candidate.get('metrics', {}).get('time_ratio_predicted_over_source', 'N/A') }}
-
- - - {% endif %} - {% if item.get('metrics', {}).get('kernel_summaries') %} - - - {{ render_kernel_package_comparisons(item) }} - - - {% endif %} {% endfor %} diff --git a/result_server/templates/_usage_report_node_hours_section.html b/result_server/templates/_usage_report_node_hours_section.html index dff2a00..e37b2ce 100644 --- a/result_server/templates/_usage_report_node_hours_section.html +++ b/result_server/templates/_usage_report_node_hours_section.html @@ -7,6 +7,17 @@

Node-Hour Usage

{% if result.apps %}
+ + + {% for system in result.systems %} + {% for period in filtered_periods %} + + {% endfor %} + {% endfor %} + {% for period in filtered_periods %} + + {% endfor %} + diff --git a/result_server/templates/estimated_detail.html b/result_server/templates/estimated_detail.html index 68c0835..257e975 100644 --- a/result_server/templates/estimated_detail.html +++ b/result_server/templates/estimated_detail.html @@ -1,6 +1,6 @@ {% extends "_results_base.html" %} {% from "_detail_tables.html" import render_json_block, render_titled_key_value_table %} -{% from "_estimated_breakdown_card.html" import render_breakdown_card %} +{% from "_estimated_breakdown_card.html" import render_breakdown_card, render_package_comparison_card %} {% block title %}Estimate Detail{% endblock %} {% block page_subtitle %}Inspect package resolution, system-side benchmarks, and estimate metadata for one projected result.{% endblock %} @@ -79,10 +79,11 @@ .breakdown-table td:nth-child(2), .breakdown-table td:nth-child(3), .breakdown-table td:nth-child(4), - .breakdown-table td:nth-child(5) { + .breakdown-table td:nth-child(5), + .breakdown-table td:nth-child(6) { white-space: nowrap; } - .breakdown-table td:nth-child(6) { + .breakdown-table td:nth-child(7) { min-width: 220px; line-height: 1.45; } @@ -121,6 +122,7 @@ border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; + overflow-x: auto; } .kernel-comparisons-title { margin-bottom: 8px; @@ -148,6 +150,7 @@ .kernel-package-table, .kernel-metrics-table { width: 100%; + min-width: 920px; border-collapse: collapse; font-size: 12px; } @@ -166,6 +169,12 @@ .kernel-metrics { margin-top: 8px; } + .package-kernel-comparisons { + margin-top: 16px; + } + .detail-bottom-grid { + align-items: start; + } .kernel-metrics-summary { margin-bottom: 5px; color: #475569; @@ -175,7 +184,36 @@ .empty-note { color: #6b7280; font-size: 13px; } - {{ render_titled_key_value_table("Meta Information", meta_rows, "detail-table", "detail-table-wrap", "detail-card") }} +
+

System Comparison

+
+
+ + + + + + + + + {% for row in system_comparison_rows %} + + + {{ row.current }} + {{ row.future }} + + {% endfor %} + +
ItemCurrent SystemFuture System
{{ row.label }}
+
+ + +
+ {{ render_breakdown_card('Current Breakdown', current_breakdown, 'No section or overlap breakdown is stored for the current-side estimate.') }} + {{ render_breakdown_card('Future Breakdown', future_breakdown, 'No section or overlap breakdown is stored for the future-side estimate.') }} +
+ + {{ render_package_comparison_card('Package Comparison', future_breakdown, 'No package comparison data is stored for this estimate.') }}

Applicability Summary

@@ -190,7 +228,7 @@

Applicability Summary

{% endif %}
-
+
{{ render_titled_key_value_table("Package Resolution", package_rows, "detail-table", "detail-table-wrap", "", "") }} {% if reestimation_rows %} @@ -198,40 +236,9 @@

Applicability Summary

{% endif %} {{ render_json_block("Measurement", measurement_json) }} {{ render_json_block("Confidence", confidence_json) }} + {{ render_json_block("Assumptions", assumptions_json) }}
- -
-

System Comparison

-
- - - - - - - - - - {% for row in system_comparison_rows %} - - - {{ row.current }} - {{ row.future }} - - {% endfor %} - -
ItemCurrent SystemFuture System
{{ row.label }}
-
-
-
- -
- {{ render_json_block("Assumptions", assumptions_json) }} -
- -
- {{ render_breakdown_card('Current Breakdown', current_breakdown, 'No section or overlap breakdown is stored for the current-side estimate.') }} - {{ render_breakdown_card('Future Breakdown', future_breakdown, 'No section or overlap breakdown is stored for the future-side estimate.') }} + {{ render_titled_key_value_table("Meta Information", meta_rows, "detail-table", "detail-table-wrap", "detail-card") }}
{% endblock %} diff --git a/result_server/templates/usage_report.html b/result_server/templates/usage_report.html index ad473a3..66e1082 100644 --- a/result_server/templates/usage_report.html +++ b/result_server/templates/usage_report.html @@ -42,12 +42,20 @@ .usage-node-hours-table { border-collapse: separate; border-spacing: 0; + table-layout: fixed; width: max-content; font-size: 12px; } + .usage-node-hours-table .usage-node-hours-app-col { + width: 220px; + } + .usage-node-hours-table .usage-node-hours-period-col { + width: 78px; + } .usage-node-hours-table th, .usage-node-hours-table td { border: 1px solid #ddd; + box-sizing: border-box; padding: 3px 5px; } .usage-node-hours-table th { diff --git a/result_server/tests/test_estimated_detail_template.py b/result_server/tests/test_estimated_detail_template.py index ce1e461..5e697b7 100644 --- a/result_server/tests/test_estimated_detail_template.py +++ b/result_server/tests/test_estimated_detail_template.py @@ -275,10 +275,13 @@ def test_estimated_detail_template_renders_sections(app): assert "Required Actions" in html assert "section_package_unsupported:half" in html assert "overlap_package_unsupported:half" in html - assert "Candidate estimates" in html + assert "Current Breakdown" in html + assert "Future Breakdown" in html + assert "Package Comparison" in html + assert "Candidate Package" in html assert "Time Ratio" in html - assert "Bench Time" in html - assert "Estimated Time" in html + assert "Before Scaling" in html + assert "After Scaling" in html assert "gpu_kernel_ensemble_average" in html assert "gpu_kernel_lightgbm_v10" in html assert "gpu_kernel_mlp_v15" in html @@ -289,3 +292,6 @@ def test_estimated_detail_template_renders_sections(app): assert "O-Memory Throughput [%]" in html assert "Memory Throughput [%]" in html assert "GB200" in html + assert html.index("System Comparison") < html.index("Current Breakdown") + assert html.index("Future Breakdown") < html.index("Package Comparison") + assert html.index("Package Comparison") < html.index("Meta Information") diff --git a/result_server/tests/test_portal_list_templates.py b/result_server/tests/test_portal_list_templates.py index d4ba58e..f796a0f 100644 --- a/result_server/tests/test_portal_list_templates.py +++ b/result_server/tests/test_portal_list_templates.py @@ -343,6 +343,60 @@ def test_usage_report_template_renders_search_box(): assert "UNKNOWN_SYSTEM" in html +def test_usage_report_node_hours_table_uses_explicit_column_widths(): + app = build_portal_shell_app( + templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"), + ) + with app.test_request_context("/results/usage"): + from flask import render_template + + html = render_template( + "usage_report.html", + result={ + "apps": ["genesis"], + "systems": ["AI4SS", "Fugaku"], + "periods": ["2026-04", "2026-05"], + "available_fiscal_years": [2026], + "table": { + "genesis": { + "AI4SS": {"2026-04": 0.0, "2026-05": 0.0}, + "Fugaku": {"2026-04": 1.23, "2026-05": 0.0}, + } + }, + "row_totals": {"genesis": {"2026-04": 1.23, "2026-05": 0.0}}, + "col_totals": { + "AI4SS": {"2026-04": 0.0, "2026-05": 0.0}, + "Fugaku": {"2026-04": 1.23, "2026-05": 0.0}, + }, + "grand_totals": {"2026-04": 1.23, "2026-05": 0.0}, + }, + filtered_periods=["2026-04", "2026-05"], + period_type="monthly", + fiscal_year=2026, + period_filter="", + site_diagnostics={ + "registered_system_count": 0, + "unused_systems": [], + "missing_system_info": [], + "missing_queue_definitions": [], + "application_count": 0, + "partial_support": [], + "application_directory_count": 0, + "apps_missing_files": [], + "apps_without_estimate": [], + "apps_with_estimate_count": 0, + "unknown_listed_systems": [], + }, + coverage_systems=[], + app_support_rows=[], + result_quality_rollup={"rows": []}, + ) + + assert 'class="usage-node-hours-app-col"' in html + assert html.count('class="usage-node-hours-period-col"') == 6 + assert "table-layout: fixed" in html + + def test_result_compare_template_renders_headline(): app = build_portal_shell_app( templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"), diff --git a/result_server/utils/estimated_detail_view.py b/result_server/utils/estimated_detail_view.py index 52e4084..8c35970 100644 --- a/result_server/utils/estimated_detail_view.py +++ b/result_server/utils/estimated_detail_view.py @@ -24,8 +24,8 @@ def build_estimated_detail_context(result): "measurement_json": result.get("measurement", {}), "confidence_json": result.get("confidence", {}), "assumptions_json": result.get("assumptions", {}), - "current_breakdown": current.get("fom_breakdown", {}), - "future_breakdown": future.get("fom_breakdown", {}), + "current_breakdown": _build_display_breakdown(current.get("fom_breakdown", {})), + "future_breakdown": _build_display_breakdown(future.get("fom_breakdown", {})), } @@ -177,6 +177,126 @@ def _append_list_row(rows, label, values): rows.append({"label": label, "list": values}) +def _build_display_breakdown(breakdown): + if not isinstance(breakdown, dict): + return {} + + return { + **breakdown, + "sections": [ + _build_display_breakdown_item(item) + for item in breakdown.get("sections", []) + if isinstance(item, dict) + ], + "overlaps": [ + _build_display_breakdown_item(item) + for item in breakdown.get("overlaps", []) + if isinstance(item, dict) + ], + } + + +def _build_display_breakdown_item(item): + before = item.get("bench_time", item.get("time", "N/A")) + after = item.get("time", "N/A") + ratio = _get_nested(item, ("metrics", "time_ratio_predicted_over_source")) + if ratio in (None, "", "N/A", "null", "nan"): + ratio = _safe_ratio(after, before) + + display_item = { + **item, + "before_scaling_display": _format_display_numeric(before), + "after_scaling_display": _format_display_numeric(after), + "time_ratio_display": _format_display_numeric(ratio), + } + if "candidate_estimates" in item: + display_item["candidate_estimates"] = [ + _build_display_candidate_estimate(candidate) + for candidate in item.get("candidate_estimates", []) + if isinstance(candidate, dict) + ] + metrics = item.get("metrics", {}) + if isinstance(metrics, dict) and metrics.get("kernel_summaries"): + display_item["metrics"] = { + **metrics, + "kernel_summaries": [ + _build_display_kernel_summary(kernel) + for kernel in metrics.get("kernel_summaries", []) + if isinstance(kernel, dict) + ], + } + return display_item + + +def _build_display_candidate_estimate(candidate): + ratio = _get_nested(candidate, ("metrics", "time_ratio_predicted_over_source")) + return { + **candidate, + "time_display": _format_display_numeric(candidate.get("time", "N/A")), + "time_ratio_display": _format_display_numeric(ratio), + } + + +def _build_display_kernel_summary(kernel): + return { + **kernel, + "package_summaries": [ + _build_display_kernel_package(package) + for package in kernel.get("package_summaries", []) + if isinstance(package, dict) + ], + } + + +def _build_display_kernel_package(package): + return { + **package, + "source_time_ns_mean_display": _format_display_numeric(package.get("source_time_ns_mean", "N/A")), + "predicted_time_ns_mean_display": _format_display_numeric(package.get("predicted_time_ns_mean", "N/A")), + "mean_time_ratio_display": _format_display_numeric(package.get("mean_time_ratio_predicted_over_source", "N/A")), + "metric_comparisons": [ + _build_display_metric_comparison(metric) + for metric in package.get("metric_comparisons", []) + if isinstance(metric, dict) + ], + } + + +def _build_display_metric_comparison(metric): + return { + **metric, + "source_value_mean_display": _format_display_numeric(metric.get("source_value_mean", "N/A")), + "predicted_value_mean_display": _format_display_numeric(metric.get("predicted_value_mean", "N/A")), + "ratio_display": _format_display_numeric(metric.get("ratio_predicted_over_source_mean", "N/A")), + } + + +def _get_nested(data, keys): + current = data + for key in keys: + if not isinstance(current, dict): + return None + current = current.get(key) + return current + + +def _safe_ratio(numerator, denominator): + try: + numerator_value = float(numerator) + denominator_value = float(denominator) + except (TypeError, ValueError): + return "N/A" + if denominator_value == 0: + return "N/A" + return numerator_value / denominator_value + + +def _format_display_numeric(value): + if value in (None, "", "N/A", "null", "nan"): + return "N/A" + return format_numeric_value(value) + + def _count_breakdown_fallbacks(system_data): breakdown = system_data.get("fom_breakdown", {}) section_count = sum(1 for item in breakdown.get("sections", []) if item.get("fallback_used"))