Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions docs/guides/portal-execution-profiles-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,16 @@ Completed:
`BK_TRIGGER_REASON`. `scripts/result.sh` stores these values under
`execution_trigger` in Result JSON, and the Portal shows them as the result
`Run Cause`.
- Received benchmark and estimation JSON metadata is indexed into
`result_metadata_index` at ingest time. JSON/tgz artifacts remain the raw
records and existing result pages remain file-backed.

Remaining follow-up:

1. Index received benchmark and estimation JSON metadata into SQLite while
keeping JSON/tgz artifacts as raw records. The first index should be an
auxiliary lookup table populated at ingest time; existing result and
estimate pages can remain file-backed until the indexed views are reviewed.
2. Add environment snapshot storage after deciding which host/runtime metadata
1. Add environment snapshot storage after deciding which host/runtime metadata
should define an environment identity.
2. Review which result and estimate views should move from file-backed scans to
indexed lookup once the operational view requirements are stable.

GitLab schedules should not be the primary governance point. The Portal should
own periodic and event-triggered execution decisions, then trigger GitLab CI
Expand All @@ -77,6 +78,11 @@ without such a scheduler requirement should leave the field empty. BenchPark
bridge controls in this repository are legacy; active BenchPark CI/CD/CB result
handling has moved to a separate project.

Node-hour accounting follows the Result JSON `execution_mode` value. `cross`
results count run time only, because build and run are separated. Systems that
build and run in one scheduler job are recorded as `native`; `native` counts
build time plus run time.

## GitLab Pipeline Trigger Configuration

Dry-run payload rendering requires:
Expand Down
1 change: 1 addition & 0 deletions result_server/routes/results_usage_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ def usage_report():
current_app.config["RECEIVED_DIR"],
request.args,
get_fiscal_year(datetime.now()),
current_app.config.get("EXECUTION_PROFILE_DB_PATH"),
)
return render_template("usage_report.html", **usage_context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<section class="page-card table-card">
<h2 class="section-title">Profile Operations Overview</h2>
<p class="section-intro">Execution profile activity across allocation, triggers, collected results, and node-hour usage.</p>

{% if profile_usage_overview.available %}
<div class="diagnostic-grid">
<div class="diagnostic-card">
<h3>Profiles</h3>
<p>{{ profile_usage_overview.summary.profile_count }} registered / {{ profile_usage_overview.summary.profile_with_results_count }} with results</p>
</div>
<div class="diagnostic-card">
<h3>Triggers</h3>
<p>{{ profile_usage_overview.summary.trigger_count }} definitions</p>
</div>
<div class="diagnostic-card">
<h3>Results</h3>
<p>{{ profile_usage_overview.summary.result_count }} records / {{ profile_usage_overview.summary.node_hours }} node-hours</p>
</div>
</div>

{% if profile_usage_overview.rows %}
<div class="table-wrap">
<table class="profile-usage-table">
<thead>
<tr>
<th>Profile</th>
<th>Scope</th>
<th>Allocation</th>
<th>Triggers</th>
<th>Latest Trigger Run</th>
<th>Results</th>
<th>Latest Result</th>
</tr>
</thead>
<tbody>
{% for row in profile_usage_overview.rows %}
<tr>
<td>
<a href="{{ url_for('admin.execution_profiles', edit=row.profile_id) }}">{{ row.profile_id }}</a>
<span class="profile-usage-subline">{{ row.status }}{% if not row.enabled %} / disabled{% endif %}</span>
</td>
<td>
<span class="profile-usage-mono">{{ row.code }}</span>
/ <span class="profile-usage-mono">{{ row.system }}</span>
<span class="profile-usage-subline">exp {{ row.exp }}</span>
</td>
<td><span class="profile-usage-mono">{{ row.allocation_project_id }}</span></td>
<td>
{{ row.enabled_trigger_count }} / {{ row.trigger_count }} enabled
{% for label in row.trigger_labels %}
<span class="profile-usage-subline">{{ label }}</span>
{% endfor %}
</td>
<td>
{% if row.latest_trigger_run %}
{{ row.latest_trigger_run.status }}
<span class="profile-usage-subline">{{ row.latest_trigger_run.created_at }}</span>
<span class="profile-usage-subline">{{ row.latest_trigger_run.reason }}</span>
{% else %}
-
{% endif %}
</td>
<td>
{{ row.result_count }} results
<span class="profile-usage-subline">{{ row.node_hours }} node-hours</span>
</td>
<td>
{% if row.latest_result %}
<a href="{{ url_for('results.result_detail', filename=row.latest_result.filename) }}">{{ row.latest_result.timestamp }}</a>
<span class="profile-usage-subline">{{ row.latest_result.code }} / {{ row.latest_result.system }} / {{ row.latest_result.exp }}</span>
<span class="profile-usage-subline">{{ row.latest_result.trigger_headline }} / pipeline {{ row.latest_result.pipeline_id }}</span>
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="muted-empty">No execution profiles are registered.</p>
{% endif %}
{% else %}
<p class="muted-empty">Execution profile database is not configured for this Portal.</p>
{% endif %}
</section>
30 changes: 29 additions & 1 deletion result_server/templates/usage_report.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,36 @@
min-width: 280px;
flex: 1 1 320px;
}
.profile-usage-table {
min-width: 1120px;
table-layout: fixed;
}
.profile-usage-table th:nth-child(1) { width: 170px; }
.profile-usage-table th:nth-child(2) { width: 190px; }
.profile-usage-table th:nth-child(3) { width: 120px; }
.profile-usage-table th:nth-child(4) { width: 180px; }
.profile-usage-table th:nth-child(5) { width: 230px; }
.profile-usage-table th:nth-child(6) { width: 120px; }
.profile-usage-table th:nth-child(7) { width: 230px; }
.profile-usage-table td {
vertical-align: top;
white-space: normal;
}
.profile-usage-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
}
.profile-usage-subline {
display: block;
margin-top: 2px;
color: #64748b;
font-size: 12px;
line-height: 1.3;
overflow-wrap: anywhere;
}
</style>

{% include "_usage_report_profile_overview_section.html" %}
{% include "_usage_report_node_hours_section.html" %}
{% include "_usage_report_configuration_checks.html" %}
{% include "_usage_report_coverage_section.html" %}
Expand All @@ -214,7 +242,7 @@
function applyUsageSearch() {
const input = document.getElementById('usageSearchInput');
const needle = (input?.value || '').toLowerCase().trim();
const targets = ['.coverage-table tbody tr', '.quality-table tbody tr'];
const targets = ['.profile-usage-table tbody tr', '.coverage-table tbody tr', '.quality-table tbody tr'];

targets.forEach((selector) => {
document.querySelectorAll(selector).forEach((row) => {
Expand Down
4 changes: 4 additions & 0 deletions result_server/tests/test_portal_list_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,10 @@ def test_usage_report_template_renders_search_box():
coverage_systems=[],
app_support_rows=[],
result_quality_rollup={"rows": []},
profile_usage_overview={"available": False, "rows": []},
)

assert "Profile Operations Overview" in html
assert "Filter coverage and current-state tables" in html
assert "applyUsageSearch" in html
assert "Application Entry Points" in html
Expand Down Expand Up @@ -392,6 +394,7 @@ def test_usage_report_node_hours_table_uses_explicit_column_widths():
coverage_systems=[],
app_support_rows=[],
result_quality_rollup={"rows": []},
profile_usage_overview={"available": False, "rows": []},
)

assert 'class="usage-node-hours-app-col"' in html
Expand Down Expand Up @@ -479,6 +482,7 @@ def test_usage_report_quality_section_renders_actions_and_improvement_candidates
},
coverage_systems=[],
app_support_rows=[],
profile_usage_overview={"available": False, "rows": []},
result_quality_rollup={
"rows": [
{
Expand Down
110 changes: 110 additions & 0 deletions result_server/tests/test_profile_usage_overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import json
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from utils.execution_profiles import ( # noqa: E402
ExecutionProfileStore,
normalize_profile,
normalize_trigger_definition,
)
from utils.profile_usage_overview import build_profile_usage_overview # noqa: E402


def _profile(**overrides):
data = {
"id": "qws-fugaku",
"enabled": True,
"status": "approved",
"code": "qws",
"system": "Fugaku",
"exp": [],
"allocation_project_id": "rkp00010",
}
data.update(overrides)
profile, errors = normalize_profile(data)
assert errors == []
assert profile is not None
return profile


def _trigger(**overrides):
data = {
"id": "qws-fugaku-time",
"trigger_type": "scheduled",
"profile_id": "qws-fugaku",
"enabled": True,
"gitlab_target": "swc",
"target_ref": "develop",
"cron_expr": "0 14 * * *",
"timezone": "Asia/Tokyo",
}
data.update(overrides)
trigger, errors = normalize_trigger_definition(data)
assert errors == []
assert trigger is not None
return trigger


def test_profile_usage_overview_links_profile_triggers_results_and_node_hours(tmp_path):
db_path = tmp_path / "cx_portal.sqlite3"
received_dir = tmp_path / "received"
received_dir.mkdir()

store = ExecutionProfileStore(str(db_path))
store.upsert_profile(_profile(), actor="admin@test.com")
store.upsert_trigger_definition(_trigger(), actor="admin@test.com")
store.create_trigger_run(
trigger_id="qws-fugaku-time",
trigger_type="scheduled",
status="submitted",
dry_run=False,
reason="cron:0 14 * * *@2026-08-10T14:00+09:00",
payload={"submit": {"response": {"id": 123}}},
)
result_file = "result_20260810_140500_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.json"
(received_dir / result_file).write_text(
json.dumps(
{
"code": "qws",
"system": "Fugaku",
"Exp": "CASE0",
"node_count": "2",
"execution_mode": "cross",
"pipeline_timing": {"run_time": 3600},
"pipeline_id": 123,
"execution_trigger": {
"id": "qws-fugaku-time",
"type": "scheduled",
"reason": "cron:0 14 * * *@2026-08-10T14:00+09:00",
},
}
),
encoding="utf-8",
)

overview = build_profile_usage_overview(str(received_dir), str(db_path))

assert overview["available"] is True
assert overview["summary"]["profile_count"] == 1
assert overview["summary"]["profile_with_results_count"] == 1
assert overview["summary"]["trigger_count"] == 1
assert overview["summary"]["result_count"] == 1
assert overview["summary"]["node_hours"] == 2.0
row = overview["rows"][0]
assert row["profile_id"] == "qws-fugaku"
assert row["allocation_project_id"] == "rkp00010"
assert row["enabled_trigger_count"] == 1
assert row["result_count"] == 1
assert row["node_hours"] == 2.0
assert row["latest_trigger_run"]["status"] == "submitted"
assert row["latest_result"]["filename"] == result_file
assert row["latest_result"]["trigger_headline"] == "Scheduled / qws-fugaku-time"


def test_profile_usage_overview_handles_missing_db(tmp_path):
overview = build_profile_usage_overview(str(tmp_path), None)

assert overview["available"] is False
assert overview["rows"] == []
7 changes: 7 additions & 0 deletions result_server/tests/test_usage_report_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,17 @@ def test_build_usage_report_context_builds_named_coverage_headers(monkeypatch):
"build_result_quality_rollup",
lambda directory: {"rows": []},
)
monkeypatch.setattr(
usage_report_view,
"build_profile_usage_overview",
lambda directory, db_path: {"available": False, "rows": []},
)

context = usage_report_view.build_usage_report_context(
"received",
MultiDict(),
2025,
"cx_portal.sqlite3",
)

assert context["period_type"] == "fiscal_year"
Expand All @@ -58,3 +64,4 @@ def test_build_usage_report_context_builds_named_coverage_headers(monkeypatch):
assert context["app_support_rows"] == [{"app": "qws", "systems": {}}]
assert context["site_diagnostics"] == {"registered_system_count": 1}
assert context["result_quality_rollup"] == {"rows": []}
assert context["profile_usage_overview"] == {"available": False, "rows": []}
5 changes: 4 additions & 1 deletion result_server/tests/test_usage_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,11 @@ def test_usage_route_uses_default_parameters(self, app, client, monkeypatch):

captured = {}

def fake_build_usage_report_context(directory, args, current_fiscal_year):
def fake_build_usage_report_context(directory, args, current_fiscal_year, db_path=None):
captured["directory"] = directory
captured["args"] = args
captured["current_fiscal_year"] = current_fiscal_year
captured["db_path"] = db_path
return {
"result": {
"apps": [],
Expand All @@ -169,6 +170,7 @@ def fake_build_usage_report_context(directory, args, current_fiscal_year):
"partial_support": [],
},
"result_quality_rollup": {"rows": []},
"profile_usage_overview": {"available": False, "rows": []},
}

import routes.results_usage_routes as usage_routes_mod
Expand All @@ -179,6 +181,7 @@ def fake_build_usage_report_context(directory, args, current_fiscal_year):
resp = client.get("/results/usage")
assert resp.status_code == 200
assert captured["directory"] == app.config["RECEIVED_DIR"]
assert captured["db_path"] == app.config.get("EXECUTION_PROFILE_DB_PATH")
assert captured["current_fiscal_year"] == 2025
assert captured["args"].get("period_type") is None

Expand Down
Loading
Loading