Skip to content

Commit 950f030

Browse files
ribalbaArneTR
andauthored
Implement warnings table and frontend display (#1255)
* Store warnings from system checks * Warnings must be saved after run as run_id is not set at the beginning of the run [skip ci] * Reworked API to not include plain invalidated status, but count for invalidated * Introduced a warning if the run was failed and breaking in API in compare * Allowed warning route for DEFAULT user * Reworked warnings display * Warning string in run overview shall only show if one warning is present * Test should now check for warning as system check warning are now transported to warnings and are always present in test scenarios [skip ci] --------- Co-authored-by: Arne Tarara <[email protected]>
1 parent 557c311 commit 950f030

File tree

13 files changed

+216
-53
lines changed

13 files changed

+216
-53
lines changed

api/api_helpers.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ def get_run_info(user, run_id):
180180
LEFT JOIN categories as t on t.id = elements) as categories,
181181
filename, start_measurement, end_measurement,
182182
measurement_config, machine_specs, machine_id, usage_scenario, usage_scenario_variables,
183-
created_at, invalid_run, phases, logs, failed, gmt_hash, runner_arguments
183+
created_at,
184+
(SELECT COUNT(id) FROM warnings as w WHERE w.run_id = runs.id) as invalid_run,
185+
phases, logs, failed, gmt_hash, runner_arguments
184186
FROM runs
185187
WHERE
186188
(TRUE = %s OR user_id = ANY(%s::int[]))
@@ -446,6 +448,19 @@ def determine_comparison_case(user, ids, force_mode=None):
446448

447449
raise RuntimeError('Could not determine comparison case after checking all conditions')
448450

451+
def check_run_failed(user, ids):
452+
query = """
453+
SELECT
454+
COUNT(failed)
455+
FROM runs
456+
WHERE
457+
(TRUE = %s OR user_id = ANY(%s::int[]))
458+
AND id = ANY(%s::uuid[])
459+
AND failed IS TRUE
460+
"""
461+
params = (user.is_super_user(), user.visible_users(), ids)
462+
return DB().fetch_one(query, params=params)[0]
463+
449464
def get_phase_stats(user, ids):
450465
query = """
451466
SELECT

api/scenario_runner.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from api.object_specifications import Software, JobChange
1515
from api.api_helpers import (ORJSONResponseObjKeep, add_phase_stats_statistics,
1616
determine_comparison_case,get_comparison_details,
17-
html_escape_multi, get_phase_stats, get_phase_stats_object,
17+
html_escape_multi, get_phase_stats, get_phase_stats_object, check_run_failed,
1818
is_valid_uuid, convert_value, get_timeline_query,
1919
get_run_info, get_machine_list, get_artifact, store_artifact,
2020
authenticate, check_int_field_api)
@@ -172,6 +172,30 @@ async def get_notes(run_id, user: User = Depends(authenticate)):
172172
return ORJSONResponseObjKeep({'success': True, 'data': escaped_data})
173173

174174

175+
@router.get('/v1/warnings/{run_id}')
176+
async def get_warnings(run_id, user: User = Depends(authenticate)):
177+
if run_id is None or not is_valid_uuid(run_id):
178+
raise RequestValidationError('Run ID is not a valid UUID or empty')
179+
180+
query = '''
181+
SELECT w.run_id, w.message, w.created_at
182+
FROM warnings as w
183+
JOIN runs as r on w.run_id = r.id
184+
WHERE
185+
(TRUE = %s OR r.user_id = ANY(%s::int[]))
186+
AND w.run_id = %s
187+
ORDER BY w.created_at DESC
188+
'''
189+
190+
params = (user.is_super_user(), user.visible_users(), run_id)
191+
data = DB().fetch_all(query, params=params)
192+
if data is None or data == []:
193+
return Response(status_code=204)
194+
195+
escaped_data = [html_escape_multi(note) for note in data]
196+
return ORJSONResponseObjKeep({'success': True, 'data': escaped_data})
197+
198+
175199
@router.get('/v1/network/{run_id}')
176200
async def get_network(run_id, user: User = Depends(authenticate)):
177201
if run_id is None or not is_valid_uuid(run_id):
@@ -252,7 +276,9 @@ def old_v1_runs_endpoint():
252276
async def get_runs(uri: str | None = None, branch: str | None = None, machine_id: int | None = None, machine: str | None = None, filename: str | None = None, job_id: int | None = None, failed: bool | None = None, limit: int | None = 50, uri_mode = 'none', user: User = Depends(authenticate)):
253277

254278
query = '''
255-
SELECT r.id, r.name, r.uri, r.branch, r.created_at, r.invalid_run, r.filename, r.usage_scenario_variables, m.description, r.commit_hash, r.end_measurement, r.failed, r.machine_id
279+
SELECT r.id, r.name, r.uri, r.branch, r.created_at,
280+
(SELECT COUNT(id) FROM warnings as w WHERE w.run_id = r.id) as invalid_run,
281+
r.filename, r.usage_scenario_variables, m.description, r.commit_hash, r.end_measurement, r.failed, r.machine_id
256282
FROM runs as r
257283
LEFT JOIN machines as m on r.machine_id = m.id
258284
WHERE
@@ -333,6 +359,12 @@ async def compare_in_repo(ids: str, force_mode:str | None = None, user: User = D
333359

334360
comparison_details = get_comparison_details(user, ids, comparison_db_key)
335361

362+
# check if a run failed
363+
364+
if check_run_failed(user, ids) >= 1:
365+
raise RequestValidationError('At least one run in your runs to compare failed. Comparsion for failed runs is not supported.')
366+
367+
336368
if not (phase_stats := get_phase_stats(user, ids)):
337369
return Response(status_code=204) # No-Content
338370

docker/structure.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ VALUES (
5252
"api": {
5353
"quotas": {},
5454
"routes": [
55+
"/v1/warnings/{run_id}",
5556
"/v1/insights",
5657
"/v1/ci/insights",
5758
"/v1/machines",
@@ -341,6 +342,19 @@ CREATE TRIGGER notes_moddatetime
341342
FOR EACH ROW
342343
EXECUTE PROCEDURE moddatetime (updated_at);
343344

345+
CREATE TABLE warnings (
346+
id SERIAL PRIMARY KEY,
347+
run_id uuid REFERENCES runs(id) ON DELETE CASCADE ON UPDATE CASCADE,
348+
message text,
349+
created_at timestamp with time zone DEFAULT now(),
350+
updated_at timestamp with time zone
351+
);
352+
CREATE INDEX "warnings_run_id" ON "warnings" USING HASH ("run_id");
353+
CREATE TRIGGER warnings_moddatetime
354+
BEFORE UPDATE ON warnings
355+
FOR EACH ROW
356+
EXECUTE PROCEDURE moddatetime (updated_at);
357+
344358
CREATE TABLE ci_measurements (
345359
id SERIAL PRIMARY KEY,
346360
energy_uj bigint,

frontend/compare.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ <h3>Run Data</h3>
7575
</div>
7676
</div><!-- end ui full-width-card card -->
7777

78+
<div id="run-warnings" class="ui icon message warning hidden">
79+
<i class="info warning icon"></i>
80+
<div class="content">
81+
<div class="header">
82+
Warnings - At least one run contains the following warnings
83+
</div>
84+
<ul></ul>
85+
</div>
86+
</div>
87+
7888
<div class="ui steps attached phases">
7989
<a class="active step" data-tab="[BASELINE]">
8090
<div class="content">

frontend/js/compare.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,32 @@ async function fetchDiff() {
1818

1919
}
2020

21+
const fetchWarningsForRuns = async (ids) => {
22+
const warnings = [];
23+
for (const id of ids) {
24+
try {
25+
const data = await makeAPICall('/v1/warnings/' + id);
26+
if (data?.data) warnings.push(...data.data);
27+
} catch (err) {
28+
showNotification('Could not get warnings data from API', err);
29+
}
30+
}
31+
return warnings;
32+
};
33+
34+
const fillWarnings = (warnings) => {
35+
if (!warnings || warnings.length === 0) return;
36+
const warnings_texts = warnings.map(sub => sub[1]);
37+
const unique_warnings = [...new Set(warnings_texts)];
38+
39+
const container = document.querySelector('#run-warnings');
40+
const ul = container.querySelector('ul');
41+
unique_warnings.forEach(w => {
42+
ul.insertAdjacentHTML('beforeend', `<li>${w}</li>`);
43+
});
44+
container.classList.remove('hidden');
45+
};
46+
2147
$(document).ready( (e) => {
2248
(async () => {
2349
const url_params = getURLParams();
@@ -43,6 +69,9 @@ $(document).ready( (e) => {
4369
return
4470
}
4571

72+
const warnings = await fetchWarningsForRuns(url_params['ids'].split(','));
73+
fillWarnings(warnings);
74+
4675
let comparison_identifiers = phase_stats_data.comparison_identifiers.map((el) => replaceRepoIcon(el));
4776
comparison_identifiers = comparison_identifiers.join(' vs. ')
4877
document.querySelector('#run-data-top').insertAdjacentHTML('beforeend', `<tr><td><strong>Comparison Type</strong></td><td>${phase_stats_data.comparison_case}</td></tr>`)

frontend/js/helpers/runs.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ const getRunsTable = async (el, url, include_uri=true, include_button=true, sear
190190
if(row[11] == true) el = `${el} <span class="ui red horizontal label">Failed</span>`;
191191
else if(row[10] == null) el = `${el} (in progress 🔥)`;
192192

193-
if(row[5] != null) el = `${el} <span class="ui yellow horizontal label" title="${row[5]}">invalidated</span>`;
193+
194+
if(row[5] != 0) el = `${el} <span class="ui yellow horizontal label" title="${row[5]}">Warnings</span>`;
194195

195196
return `<a href="/stats.html?id=${row[0]}" target="_blank">${el}</a>`
196197
},

frontend/js/stats.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,11 @@ const fetchAndFillRunData = async (url_params) => {
119119
} else if(item == 'name' || item == 'filename' || item == 'branch') {
120120
document.querySelector('#run-data-top').insertAdjacentHTML('beforeend', `<tr><td><strong>${item}</strong></td><td>${run_data?.[item]}</td></tr>`)
121121
} else if(item == 'failed' && run_data?.[item] == true) {
122-
document.querySelector('#run-data-top').insertAdjacentHTML('beforeend', `<tr><td><strong>Status</strong></td><td><span class="ui red horizontal label">This run has failed. Please see logs for details</span></td></tr>`)
122+
document.querySelector('#run-failed').classList.remove('hidden');
123123
} else if(item == 'start_measurement' || item == 'end_measurement') {
124124
document.querySelector('#run-data-accordion').insertAdjacentHTML('beforeend', `<tr><td><strong>${item}</strong></td><td title="${run_data?.[item]}">${new Date(run_data?.[item] / 1e3)}</td></tr>`)
125125
} else if(item == 'created_at' ) {
126126
document.querySelector('#run-data-accordion').insertAdjacentHTML('beforeend', `<tr><td><strong>${item}</strong></td><td title="${run_data?.[item]}">${new Date(run_data?.[item])}</td></tr>`)
127-
} else if(item == 'invalid_run' && run_data?.[item] != null) {
128-
document.querySelector('#run-data-top').insertAdjacentHTML('beforeend', `<tr><td><strong>${item}</strong></td><td><span class="ui yellow horizontal label">${run_data?.[item]}</span></td></tr>`)
129127
} else if(item == 'gmt_hash') {
130128
document.querySelector('#run-data-accordion').insertAdjacentHTML('beforeend', `<tr><td><strong>${item}</strong></td><td><a href="https://github.com/green-coding-solutions/green-metrics-tool/commit/${run_data?.[item]}">${run_data?.[item]}</a></td></tr>`);
131129
} else if(item == 'uri') {
@@ -144,10 +142,7 @@ const fetchAndFillRunData = async (url_params) => {
144142

145143
document.querySelector('#run-data-accordion').insertAdjacentHTML('beforeend', `<tr><td><strong>duration</strong></td><td title="${measurement_duration_in_s} seconds">${measurement_duration_display}</td></tr>`)
146144

147-
if (run_data.invalid_run) {
148-
showNotification('Run measurement has been marked as invalid', run_data.invalid_run);
149-
document.body.classList.add("invalidated-measurement")
150-
}
145+
// warnings will be fetched separately
151146

152147
}
153148

@@ -592,6 +587,25 @@ const fetchTimelineNotes = async (url_params) => {
592587
return notes?.data;
593588
}
594589

590+
const fetchAndFillWarnings = async (url_params) => {
591+
let warnings = null;
592+
try {
593+
warnings = await makeAPICall('/v1/warnings/' + url_params['id'])
594+
if (!warnings || warnings?.data?.length === 0) return;
595+
} catch (err) {
596+
showNotification('Could not get warnings data from API', err);
597+
return;
598+
}
599+
600+
const container = document.querySelector('#run-warnings');
601+
const ul = container.querySelector('ul');
602+
warnings.data.forEach(w => {
603+
ul.insertAdjacentHTML('beforeend', `<li>${w[1]}</li>`);
604+
});
605+
container.classList.remove('hidden');
606+
}
607+
608+
595609

596610
/* Chart starting code*/
597611
$(document).ready( (e) => {
@@ -611,6 +625,7 @@ $(document).ready( (e) => {
611625
fetchAndFillNetworkIntercepts(url_params);
612626
fetchAndFillOptimizationsData(url_params);
613627
fetchAndFillAIData(url_params);
628+
fetchAndFillWarnings(url_params);
614629

615630
(async () => { // since we need to wait for fetchAndFillPhaseStatsData we wrap in async so later calls cann already proceed
616631
const phase_stats = await fetchAndFillPhaseStatsData(url_params);

frontend/stats.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,26 @@ <h3>Usage Scenario File</h3>
126126
</div>
127127
</div><!-- end ui full-width-card card -->
128128

129+
<div id="run-failed" class="ui icon message error hidden">
130+
<i class="times icon"></i>
131+
<div class="content">
132+
<div class="header">
133+
Failed
134+
</div>
135+
This run has failed. Please see logs for details
136+
</div>
137+
</div>
138+
139+
<div id="run-warnings" class="ui icon message warning hidden">
140+
<i class="warning icon"></i>
141+
<div class="content">
142+
<div class="header">
143+
Warnings
144+
</div>
145+
<ul></ul>
146+
</div>
147+
</div>
148+
129149
<div class="ui steps attached phases">
130150
<a class="active step" data-tab="[BASELINE]">
131151
<div class="content">

lib/scenario_runner.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def __init__(self,
151151
self.__working_folder_rel = ''
152152
self.__image_sizes = {}
153153
self.__volume_sizes = {}
154+
self.__warnings = []
154155

155156
# we currently do not use this variable
156157
# self.__filename = self._original_filename # this can be changed later if working directory changes
@@ -226,7 +227,9 @@ def check_system(self, mode='start'):
226227
return
227228

228229
if mode =='start':
229-
system_checks.check_start()
230+
warnings = system_checks.check_start()
231+
for warn in warnings:
232+
self.__warnings.append(warn)
230233
else:
231234
raise RuntimeError('Unknown mode for system check:', mode)
232235

@@ -1281,6 +1284,13 @@ def add_to_log(self, container_name, message, cmd=''):
12811284
self.__stdout_logs[log_entry_name] = ''
12821285
self.__stdout_logs[log_entry_name] = '\n'.join((self.__stdout_logs[log_entry_name], message))
12831286

1287+
def save_warnings(self):
1288+
if not self._run_id or self._dev_no_save:
1289+
print("Skipping saving warning due to missing run id or --dev-no-save")
1290+
return
1291+
for message in self.__warnings:
1292+
DB().query("INSERT INTO warnings (run_id, message) VALUES (%s, %s)", (self._run_id, message))
1293+
12841294
def add_containers_to_metric_providers(self):
12851295
for metric_provider in self.__metric_providers:
12861296
if metric_provider._metric_name.endswith('_container'):
@@ -1763,12 +1773,7 @@ def identify_invalid_run(self):
17631773
if not self._run_id or self._dev_no_save:
17641774
print(TerminalColors.WARNING, '\nSkipping saving identification if run is invalid due to missing run id or --dev-no-save', TerminalColors.ENDC)
17651775
else:
1766-
DB().query('''
1767-
UPDATE runs
1768-
SET invalid_run = COALESCE(invalid_run, '') || %s
1769-
WHERE id=%s''',
1770-
params=(invalid_message, self._run_id)
1771-
)
1776+
self.__warnings.append(invalid_message)
17721777

17731778
for argument in self._arguments:
17741779
# dev no optimizations does not make the run invalid ... all others do
@@ -1779,12 +1784,7 @@ def identify_invalid_run(self):
17791784
if not self._run_id or self._dev_no_save:
17801785
print(TerminalColors.WARNING, '\nSkipping saving identification if run is invalid due to missing run id or --dev-no-save', TerminalColors.ENDC)
17811786
else:
1782-
DB().query('''
1783-
UPDATE runs
1784-
SET invalid_run = COALESCE(invalid_run, '') || %s
1785-
WHERE id=%s''',
1786-
params=(invalid_message, self._run_id)
1787-
)
1787+
self.__warnings.append(invalid_message)
17881788
break # one is enough
17891789

17901790
def cleanup(self, continue_measurement=False):
@@ -1841,6 +1841,7 @@ def cleanup(self, continue_measurement=False):
18411841
self.__working_folder_rel = ''
18421842
self.__image_sizes = {}
18431843
self.__volume_sizes = {}
1844+
self.__warnings = []
18441845

18451846

18461847
print(TerminalColors.OKBLUE, '-Cleanup gracefully completed', TerminalColors.ENDC)
@@ -1993,23 +1994,30 @@ def run(self):
19931994
raise exc
19941995
finally:
19951996
try:
1996-
if self._run_id and self._dev_no_phase_stats is False and self._dev_no_save is False:
1997-
# After every run, even if it failed, we want to generate phase stats.
1998-
# They will not show the accurate data, but they are still neded to understand how
1999-
# much a failed run has accrued in total energy and carbon costs
2000-
print(TerminalColors.HEADER, '\nCalculating and storing phases data. This can take a couple of seconds ...', TerminalColors.ENDC)
2001-
2002-
# get all the metrics from the measurements table grouped by metric
2003-
# loop over them issuing separate queries to the DB
2004-
from tools.phase_stats import build_and_store_phase_stats # pylint: disable=import-outside-toplevel
2005-
build_and_store_phase_stats(self._run_id, self._sci)
2006-
1997+
self.save_warnings()
20071998
except BaseException as exc:
20081999
self.add_to_log(exc.__class__.__name__, str(exc))
20092000
self.set_run_failed()
20102001
raise exc
20112002
finally:
2012-
self.cleanup() # always run cleanup automatically after each run
2003+
try:
2004+
if self._run_id and self._dev_no_phase_stats is False and self._dev_no_save is False:
2005+
# After every run, even if it failed, we want to generate phase stats.
2006+
# They will not show the accurate data, but they are still neded to understand how
2007+
# much a failed run has accrued in total energy and carbon costs
2008+
print(TerminalColors.HEADER, '\nCalculating and storing phases data. This can take a couple of seconds ...', TerminalColors.ENDC)
2009+
2010+
# get all the metrics from the measurements table grouped by metric
2011+
# loop over them issuing separate queries to the DB
2012+
from tools.phase_stats import build_and_store_phase_stats # pylint: disable=import-outside-toplevel
2013+
build_and_store_phase_stats(self._run_id, self._sci)
2014+
2015+
except BaseException as exc:
2016+
self.add_to_log(exc.__class__.__name__, str(exc))
2017+
self.set_run_failed()
2018+
raise exc
2019+
finally:
2020+
self.cleanup() # always run cleanup automatically after each run
20132021

20142022

20152023

0 commit comments

Comments
 (0)