From 2a60cd1f1f0dab2b89574f468de0981f7bb3635b Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Tue, 17 Dec 2024 23:24:24 +0100 Subject: [PATCH 01/15] Added unselect button [skip ci] (#1022) --- frontend/css/green-coding.css | 6 +++++- frontend/index.html | 1 - frontend/js/helpers/runs.js | 12 +++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/css/green-coding.css b/frontend/css/green-coding.css index ea911dac0..45c65cf10 100644 --- a/frontend/css/green-coding.css +++ b/frontend/css/green-coding.css @@ -278,7 +278,11 @@ a, overflow: scroll; } -#compare-button, #sort-button { +#unselect-button { + display: none; +} + +#sort-button { float: right; } diff --git a/frontend/index.html b/frontend/index.html index 1ff762dab..c3021819b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -54,7 +54,6 @@

-
diff --git a/frontend/js/helpers/runs.js b/frontend/js/helpers/runs.js index 4acfb61df..22b8cbc75 100644 --- a/frontend/js/helpers/runs.js +++ b/frontend/js/helpers/runs.js @@ -11,6 +11,11 @@ const updateCompareCount = () => { const countButton = document.getElementById('compare-button'); const checkedCount = document.querySelectorAll('input[type=checkbox]:checked').length; countButton.textContent = `Compare: ${checkedCount} Run(s)`; + if (checkedCount == 0) { + document.querySelector('#unselect-button').style.display = 'none'; + } else { + document.querySelector('#unselect-button').style.display = 'block'; + } } let lastChecked = null; @@ -141,7 +146,7 @@ const getRunsTable = (el, url, include_uri=true, include_button=true, searching= columns.push({ data: 7, title: 'Machine' }); columns.push({ data: 4, title: 'Last run', render: (el, type, row) => el == null ? '-' : `${dateToYMD(new Date(el))}
History  ` }); - const button_title = include_button ? '' : ''; + const button_title = include_button ? '
' : ''; columns.push({ data: 0, @@ -165,6 +170,11 @@ const getRunsTable = (el, url, include_uri=true, include_button=true, searching= e.removeEventListener('change', updateCompareCount); e.addEventListener('change', updateCompareCount); }) + document.querySelector('#unselect-button').addEventListener('click', e => { + document.querySelectorAll('input[type="checkbox"]').forEach(el => { + el.checked = ''; + }) + }) allow_group_select_checkboxes(); updateCompareCount(); }, From 88fb07d8cad009a189b2322d64c58946c9d1cd10 Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Tue, 17 Dec 2024 23:24:52 +0100 Subject: [PATCH 02/15] (improvement): More strict check --- frontend/js/helpers/runs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/js/helpers/runs.js b/frontend/js/helpers/runs.js index 22b8cbc75..d981fe7bd 100644 --- a/frontend/js/helpers/runs.js +++ b/frontend/js/helpers/runs.js @@ -11,7 +11,7 @@ const updateCompareCount = () => { const countButton = document.getElementById('compare-button'); const checkedCount = document.querySelectorAll('input[type=checkbox]:checked').length; countButton.textContent = `Compare: ${checkedCount} Run(s)`; - if (checkedCount == 0) { + if (checkedCount === 0) { document.querySelector('#unselect-button').style.display = 'none'; } else { document.querySelector('#unselect-button').style.display = 'block'; From 8620e6f5eb6669ed48302f5f25b222d0f4312453 Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Wed, 18 Dec 2024 00:37:33 +0100 Subject: [PATCH 03/15] Noise reduced run start (#1023) * Merge initialize_run and update_and_insert_specs * Short circuit if run_id not set * Test fix --- runner.py | 84 +++++++++++++++++++++-------------------- tests/test_functions.py | 5 +-- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/runner.py b/runner.py index 8db8b9d91..bf5301a8f 100755 --- a/runner.py +++ b/runner.py @@ -126,26 +126,6 @@ def custom_sleep(self, sleep_time): print(TerminalColors.HEADER, '\nSleeping for : ', sleep_time, TerminalColors.ENDC) time.sleep(sleep_time) - def initialize_run(self): - # We issue a fetch_one() instead of a query() here, cause we want to get the RUN_ID - - # we also update the branch here again, as this might not be main in case of local filesystem - self._run_id = DB().fetch_one(""" - INSERT INTO runs ( - job_id, name, uri, email, branch, filename, commit_hash, - commit_timestamp, runner_arguments, user_id, created_at - ) - VALUES ( - %s, %s, %s, 'manual', %s, %s, %s, - %s, %s, %s, NOW() - ) - RETURNING id - """, params=( - self._job_id, self._name, self._uri, self._branch, self._original_filename, self._commit_hash, - self._commit_timestamp, json.dumps(self._arguments), self._user_id - ))[0] - return self._run_id - def get_optimizations_ignore(self): return self._usage_scenario.get('optimizations_ignore', []) @@ -189,6 +169,9 @@ def initialize_folder(self, path): Path(path).mkdir(parents=True, exist_ok=True) def save_notes_runner(self): + if not self._run_id: + return # Nothing to do, but also no hard error needed + print(TerminalColors.HEADER, '\nSaving notes: ', TerminalColors.ENDC, self.__notes_helper.get_notes()) self.__notes_helper.save_to_db(self._run_id) @@ -448,7 +431,7 @@ def register_machine_id(self): machine = Machine(machine_id=config['machine'].get('id'), description=config['machine'].get('description')) machine.register() - def update_and_insert_specs(self): + def initialize_run(self): config = GlobalConfig().config gmt_hash, _ = get_repo_info(CURRENT_DIR) @@ -469,21 +452,32 @@ def update_and_insert_specs(self): measurement_config['providers'] = utils.get_metric_providers(config) measurement_config['sci'] = self._sci - # Insert auxilary info for the run. Not critical. - DB().query(""" - UPDATE runs - SET - machine_id=%s, machine_specs=%s, measurement_config=%s, - usage_scenario = %s, gmt_hash=%s - WHERE id = %s - """, params=( - config['machine']['id'], - escape(json.dumps(machine_specs), quote=False), - json.dumps(measurement_config), - escape(json.dumps(self._usage_scenario), quote=False), - gmt_hash, - self._run_id) - ) + + # We issue a fetch_one() instead of a query() here, cause we want to get the RUN_ID + self._run_id = DB().fetch_one(""" + INSERT INTO runs ( + job_id, name, uri, branch, filename, + commit_hash, commit_timestamp, runner_arguments, + machine_specs, measurement_config, + usage_scenario, gmt_hash, + machine_id, user_id, created_at + ) + VALUES ( + %s, %s, %s, %s, %s, + %s, %s, %s, + %s, %s, + %s, %s, + %s, %s, NOW() + ) + RETURNING id + """, params=( + self._job_id, self._name, self._uri, self._branch, self._original_filename, + self._commit_hash, self._commit_timestamp, json.dumps(self._arguments), + escape(json.dumps(machine_specs), quote=False), json.dumps(measurement_config), + escape(json.dumps(self._usage_scenario), quote=False), gmt_hash, + GlobalConfig().config['machine']['id'], self._user_id, + ))[0] + return self._run_id def import_metric_providers(self): if self._dev_no_metrics: @@ -1379,6 +1373,9 @@ def end_measurement(self, skip_on_already_ended=False): self.__notes_helper.add_note({'note': 'End of measurement', 'detail_name': '[NOTES]', 'timestamp': self.__end_measurement}) def update_start_and_end_times(self): + if not self._run_id: + return # Nothing to do, but also no hard error needed + print(TerminalColors.HEADER, '\nUpdating start and end measurement times', TerminalColors.ENDC) DB().query(""" UPDATE runs @@ -1400,6 +1397,9 @@ def set_run_failed(self): def store_phases(self): + if not self._run_id: + return # Nothing to do, but also no hard error needed + print(TerminalColors.HEADER, '\nUpdating phases in DB', TerminalColors.ENDC) # internally PostgreSQL stores JSON ordered. This means our name-indexed dict will get # re-ordered. Therefore we change the structure and make it a list now. @@ -1446,6 +1446,9 @@ def read_container_logs(self): self.add_to_log(container_id, f"stderr: {log.stderr}") def save_stdout_logs(self): + if not self._run_id: + return # Nothing to do, but also no hard error needed + print(TerminalColors.HEADER, '\nSaving logs to DB', TerminalColors.ENDC) logs_as_str = '\n\n'.join([f"{k}:{v}" for k,v in self.__stdout_logs.items()]) logs_as_str = logs_as_str.replace('\x00','') @@ -1525,20 +1528,19 @@ def run(self): ''' try: config = GlobalConfig().config - self.start_measurement() + self.start_measurement() # we start as early as possible to include initialization overhead self.check_system('start') self.initialize_folder(self._tmp_folder) self.checkout_repository() - self.initialize_run() self.initial_parse() + self.register_machine_id() self.import_metric_providers() self.populate_image_names() self.prepare_docker() self.check_running_containers() self.remove_docker_images() self.download_dependencies() - self.register_machine_id() - self.update_and_insert_specs() + self.initialize_run() # have this as close to the start of measurement if self._debugger.active: self._debugger.pause('Initial load complete. Waiting to start metric providers') @@ -1653,7 +1655,7 @@ def run(self): raise exc finally: try: - if self._dev_no_phase_stats is False: + if self._run_id and self._dev_no_phase_stats is False: # After every run, even if it failed, we want to generate phase stats. # They will not show the accurate data, but they are still neded to understand how # much a failed run has accrued in total energy and carbon costs diff --git a/tests/test_functions.py b/tests/test_functions.py index 111ed50f5..e204052c8 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -79,8 +79,8 @@ def run_until(self, step): self.__runner.check_system('start') self.__runner.initialize_folder(self.__runner._tmp_folder) self.__runner.checkout_repository() - self.__runner.initialize_run() self.__runner.initial_parse() + self.__runner.register_machine_id() self.__runner.import_metric_providers() if step == 'import_metric_providers': return @@ -89,8 +89,7 @@ def run_until(self, step): self.__runner.check_running_containers() self.__runner.remove_docker_images() self.__runner.download_dependencies() - self.__runner.register_machine_id() - self.__runner.update_and_insert_specs() + self.__runner.initialize_run() self.__runner.start_metric_providers(allow_other=True, allow_container=False) self.__runner.custom_sleep(config['measurement']['pre-test-sleep']) From 1884d5f1157d9f668d2d58ccfc18f54b10857534 Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Thu, 19 Dec 2024 10:57:30 +0100 Subject: [PATCH 04/15] (fix): Unselect button broke repositories view --- frontend/css/green-coding.css | 5 +---- frontend/js/helpers/runs.js | 2 +- frontend/repositories.html | 9 ++++++--- tests/frontend/test_frontend.py | 18 +++++++++--------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/css/green-coding.css b/frontend/css/green-coding.css index 45c65cf10..aff02be47 100644 --- a/frontend/css/green-coding.css +++ b/frontend/css/green-coding.css @@ -280,10 +280,7 @@ a, #unselect-button { display: none; -} - -#sort-button { - float: right; + margin-top: 10px; } .wide.card { diff --git a/frontend/js/helpers/runs.js b/frontend/js/helpers/runs.js index d981fe7bd..93f19fbd1 100644 --- a/frontend/js/helpers/runs.js +++ b/frontend/js/helpers/runs.js @@ -146,7 +146,7 @@ const getRunsTable = (el, url, include_uri=true, include_button=true, searching= columns.push({ data: 7, title: 'Machine' }); columns.push({ data: 4, title: 'Last run', render: (el, type, row) => el == null ? '-' : `${dateToYMD(new Date(el))}
History  ` }); - const button_title = include_button ? '
' : ''; + const button_title = include_button ? '
' : ''; columns.push({ data: 0, diff --git a/frontend/repositories.html b/frontend/repositories.html index c29f5ce2d..5a607a6b4 100644 --- a/frontend/repositories.html +++ b/frontend/repositories.html @@ -61,9 +61,12 @@

- Repositories ( / / etc.) - - + Repositories ( / / etc.)+ +
+ + +
+ diff --git a/tests/frontend/test_frontend.py b/tests/frontend/test_frontend.py index 94499ec4a..bc4dfd00d 100644 --- a/tests/frontend/test_frontend.py +++ b/tests/frontend/test_frontend.py @@ -205,14 +205,14 @@ def test_stats(): assert chart_label.strip() == 'CPU % via procfs' - first_metric = new_page.locator("#runtime-steps > div.ui.bottom.attached.active.tab.segment > div.ui.segment.secondary > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(1) > td:nth-child(1)").text_content() - assert first_metric.strip() == 'CPU Energy (Package)' + first_metric = new_page.locator("#runtime-steps > div.ui.bottom.attached.active.tab.segment > div.ui.segment.secondary > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(7) > td:nth-child(1)").text_content() + assert first_metric.strip() == 'Machine Power' - first_value = new_page.locator("#runtime-steps > div.ui.bottom.attached.active.tab.segment > div.ui.segment.secondary > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(1) > td:nth-child(6)").text_content() - assert first_value.strip() == '45.05' + first_value = new_page.locator("#runtime-steps > div.ui.bottom.attached.active.tab.segment > div.ui.segment.secondary > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(7) > td:nth-child(6)").text_content() + assert first_value.strip() == '14.62' - first_unit = new_page.locator("#runtime-steps > div.ui.bottom.attached.active.tab.segment > div.ui.segment.secondary > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(1) > td:nth-child(7)").text_content() - assert first_unit.strip() == 'J' + first_unit = new_page.locator("#runtime-steps > div.ui.bottom.attached.active.tab.segment > div.ui.segment.secondary > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(7) > td:nth-child(7)").text_content() + assert first_unit.strip() == 'W' # click on baseline @@ -220,13 +220,13 @@ def test_stats(): new_page.locator('div[data-tab="[BASELINE]"] .ui.accordion .title > a').click() first_metric = new_page.locator("#main > div.ui.tab.attached.segment.secondary.active > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(1) > td:nth-child(1)").text_content() - assert first_metric.strip() == 'CPU Energy (Package)' + assert first_metric.strip() == 'Embodied Carbon' first_value = new_page.locator("#main > div.ui.tab.attached.segment.secondary.active > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(1) > td:nth-child(6)").text_content() - assert first_value.strip() == '9.69' + assert first_value.strip() == '0.01' first_unit = new_page.locator("#main > div.ui.tab.attached.segment.secondary.active > phase-metrics > div.ui.accordion > div.content.active > table > tbody > tr:nth-child(1) > td:nth-child(7)").text_content() - assert first_unit.strip() == 'J' + assert first_unit.strip() == 'g' new_page.close() From ffdcd3c1ca0a800cda8bc9afa54ee7137a523041 Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Thu, 19 Dec 2024 16:26:37 +0100 Subject: [PATCH 05/15] Updated cloud energy --- metric_providers/psu/energy/ac/xgboost/machine/model | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metric_providers/psu/energy/ac/xgboost/machine/model b/metric_providers/psu/energy/ac/xgboost/machine/model index 5b7cc582e..afc5f1708 160000 --- a/metric_providers/psu/energy/ac/xgboost/machine/model +++ b/metric_providers/psu/energy/ac/xgboost/machine/model @@ -1 +1 @@ -Subproject commit 5b7cc582e749ee826fe45379cb1dbe1190a2bacf +Subproject commit afc5f1708f87771dd3d70a986ed1b5496256efbf From c3564ef7fa95ef85ad357acc98752b2749a5ace5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:37:34 +0100 Subject: [PATCH 06/15] Bump deepdiff from 8.0.1 to 8.1.1 (#1020) Bumps [deepdiff](https://github.com/seperman/deepdiff) from 8.0.1 to 8.1.1. - [Release notes](https://github.com/seperman/deepdiff/releases) - [Changelog](https://github.com/seperman/deepdiff/blob/master/docs/changelog.rst) - [Commits](https://github.com/seperman/deepdiff/compare/8.0.1...8.1.1) --- updated-dependencies: - dependency-name: deepdiff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/requirements.txt b/docker/requirements.txt index 84dc95cc9..8dabd3346 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -10,7 +10,7 @@ anybadge==1.14.0 orjson==3.10.12 scipy==1.14.1 schema==0.7.7 -deepdiff==8.0.1 +deepdiff==8.1.1 redis==5.2.1 hiredis==3.1.0 requests==2.32.3 From e5c35ae829f3c125e5dacc79e506c580cd9c58f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:37:52 +0100 Subject: [PATCH 07/15] Bump pydantic from 2.10.3 to 2.10.4 (#1024) Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.3 to 2.10.4. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.10.3...v2.10.4) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f59fc9576..829bb3649 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -r requirements.txt -pydantic==2.10.3 +pydantic==2.10.4 pylint==3.3.2 pytest-randomly==3.16.0 pytest-playwright==0.6.2 \ No newline at end of file From 9c6f31af3b19bec12fd0a0043e4bee62803c2205 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:38:08 +0100 Subject: [PATCH 08/15] Bump aiohttp from 3.11.10 to 3.11.11 (#1025) Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.10 to 3.11.11. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.11.10...v3.11.11) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 18951e60e..b081ea916 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ psycopg_pool==3.2.4 pyserial==3.5 psutil==6.1.0 schema==0.7.7 -aiohttp==3.11.10 +aiohttp==3.11.11 # calibration script dep tqdm==4.67.1 From ff995f4e1fb98457ebc709c95ce717e580e883a1 Mon Sep 17 00:00:00 2001 From: Didi Hoffmann Date: Thu, 19 Dec 2024 16:39:33 +0100 Subject: [PATCH 09/15] Removes dead link --- api/api_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api_helpers.py b/api/api_helpers.py index 7b75b226a..0a5206bb3 100644 --- a/api/api_helpers.py +++ b/api/api_helpers.py @@ -65,7 +65,7 @@ def rescale_energy_value(value, unit): unit = 'uJ' # We only expect values to be uJ for energy in the future. Changing values now temporarily. - # TODO: Refactor this once all data in the DB is uJ + # TODO: Refactor this once all data in the DB is uJ # pylint: disable=fixme if unit != 'uJ' and not unit.startswith('ugCO2e/'): raise ValueError('Unexpected unit occured for energy rescaling: ', unit) @@ -690,7 +690,7 @@ def __init__( header_scheme = APIKeyHeader( name='X-Authentication', scheme_name='Header', - description='Authentication key - See https://docs.green-coding.io/authentication', + description='Authentication key', auto_error=False ) From 82f2946af19108e583d220aa3f5a0fab34af9a4e Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Thu, 19 Dec 2024 18:43:58 +0100 Subject: [PATCH 10/15] Badges for CI now include carbon and also Totals (#998) * CarbonDB and PowerHOG must be actively deactivated * Badges for CI now include carbon and also Totals * Totals badge with dynamic duration added * style: variable name 'energy_value' renamed * style: badge renamed to metric value * Adding limit for last badge --- api/api_helpers.py | 8 ++--- api/eco_ci.py | 54 ++++++++++++++++++++-------- api/main.py | 8 ++--- frontend/ci.html | 28 ++++++++++++++- frontend/images/no-data-badge.webp | Bin 0 -> 4498 bytes frontend/js/ci.js | 56 +++++++++++++++++++++++++---- tests/api/test_api_helpers.py | 20 +++++------ 7 files changed, 135 insertions(+), 39 deletions(-) create mode 100644 frontend/images/no-data-badge.webp diff --git a/api/api_helpers.py b/api/api_helpers.py index 0a5206bb3..f217db868 100644 --- a/api/api_helpers.py +++ b/api/api_helpers.py @@ -59,15 +59,15 @@ def store_artifact(artifact_type: Enum, key:str, data, ex=2592000): # Use this function never in the phase_stats. The metrics must always be on # The same unit for proper comparison! # -def rescale_energy_value(value, unit): +def rescale_metric_value(value, unit): if unit == 'mJ': value = value * 1_000 unit = 'uJ' # We only expect values to be uJ for energy in the future. Changing values now temporarily. - # TODO: Refactor this once all data in the DB is uJ # pylint: disable=fixme - if unit != 'uJ' and not unit.startswith('ugCO2e/'): - raise ValueError('Unexpected unit occured for energy rescaling: ', unit) + # TODO: Refactor this once all data in the DB is uJ + if unit not in ('uJ', 'ug') and not unit.startswith('ugCO2e/'): + raise ValueError('Unexpected unit occured for metric rescaling: ', unit) unit_type = unit[1:] diff --git a/api/eco_ci.py b/api/eco_ci.py index b6815915b..91109be1d 100644 --- a/api/eco_ci.py +++ b/api/eco_ci.py @@ -3,8 +3,9 @@ from fastapi import APIRouter from fastapi import Request, Response, Depends from fastapi.responses import ORJSONResponse +from fastapi.exceptions import RequestValidationError -from api.api_helpers import authenticate, html_escape_multi, get_connecting_ip, rescale_energy_value +from api.api_helpers import authenticate, html_escape_multi, get_connecting_ip, rescale_metric_value from api.object_specifications import CI_Measurement_Old, CI_Measurement import anybadge @@ -238,30 +239,55 @@ async def get_ci_runs(repo: str, sort_by: str = 'name'): return ORJSONResponse({'success': True, 'data': data}) # no escaping needed, as it happend on ingest @router.get('/v1/ci/badge/get') -async def get_ci_badge_get(repo: str, branch: str, workflow:str): - query = """ - SELECT SUM(energy_uj), MAX(run_id) +async def get_ci_badge_get(repo: str, branch: str, workflow:str, mode: str = 'last', metric: str = 'energy', duration_days: int | None = None): + if metric == 'energy': + metric = 'energy_uj' + metric_unit = 'uJ' + label = 'Energy used' + default_color = 'orange' + elif metric == 'carbon': + metric = 'carbon_ug' + metric_unit = 'ug' + label = 'Carbon emitted' + default_color = 'black' + else: + raise RequestValidationError('Unsupported metric requested') + + + params = [repo, branch, workflow] + + + query = f""" + SELECT SUM({metric}) FROM ci_measurements WHERE repo = %s AND branch = %s AND workflow_id = %s - GROUP BY run_id - ORDER BY MAX(created_at) DESC - LIMIT 1 """ - params = (repo, branch, workflow) + if mode == 'last': + query = f"""{query} + GROUP BY run_id + ORDER BY MAX(created_at) DESC + LIMIT 1 + """ + elif mode == 'totals' and duration_days: + query = f"{query} AND created_at > NOW() - make_interval(days => %s)" + params.append(duration_days) + + data = DB().fetch_one(query, params=params) - if data is None or data == [] or data[1] is None: # special check for data[1] as this is aggregate query which always returns result + if data is None or data == [] or data[0] is None: # special check for SUM element as this is aggregate query which always returns result return Response(status_code=204) # No-Content - energy_value = data[0] + metric_value = data[0] - [energy_value, energy_unit] = rescale_energy_value(energy_value, 'uJ') - badge_value= f"{energy_value:.2f} {energy_unit}" + [metric_value, metric_unit] = rescale_metric_value(metric_value, metric_unit) + badge_value= f"{metric_value:.2f} {metric_unit}" badge = anybadge.Badge( - label='Energy Used', + label=label, value=xml_escape(badge_value), num_value_padding_chars=1, - default_color='green') + default_color=default_color) + return Response(content=str(badge), media_type="image/svg+xml") diff --git a/api/main.py b/api/main.py index ca27e4adc..521e8adbd 100644 --- a/api/main.py +++ b/api/main.py @@ -25,7 +25,7 @@ from api.api_helpers import (ORJSONResponseObjKeep, add_phase_stats_statistics, determine_comparison_case,get_comparison_details, html_escape_multi, get_phase_stats, get_phase_stats_object, - is_valid_uuid, rescale_energy_value, get_timeline_query, + is_valid_uuid, rescale_metric_value, get_timeline_query, get_run_info, get_machine_list, get_artifact, store_artifact, authenticate) @@ -514,10 +514,10 @@ async def get_badge_single(run_id: str, metric: str = 'ml-estimated'): data = DB().fetch_one(query, params=params) if data is None or data == [] or data[1] is None: # special check for data[1] as this is aggregate query which always returns result - badge_value = 'No energy data yet' + badge_value = 'No metric data yet' else: - [energy_value, energy_unit] = rescale_energy_value(data[0], data[1]) - badge_value= f"{energy_value:.2f} {energy_unit} {via}" + [metric_value, energy_unit] = rescale_metric_value(data[0], data[1]) + badge_value= f"{metric_value:.2f} {energy_unit} {via}" badge = anybadge.Badge( label=xml_escape(label), diff --git a/frontend/ci.html b/frontend/ci.html index 96fe5e281..64ab3cc82 100644 --- a/frontend/ci.html +++ b/frontend/ci.html @@ -44,7 +44,33 @@

General Info

Last Run Badge: - + + + + + + + + + + Totals Badge: + + + + + + + + + + + Monthly Badge : + + + + + + diff --git a/frontend/images/no-data-badge.webp b/frontend/images/no-data-badge.webp new file mode 100644 index 0000000000000000000000000000000000000000..37daf70fe352d469cd05bfaf16954e4a047894ca GIT binary patch literal 4498 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 N9uzeCzfYtG { +const getBadges = async (repo, branch, workflow_id) => { try { const link_node = document.createElement("a") const img_node = document.createElement("img") img_node.src = `${API_URL}/v1/ci/badge/get?repo=${repo}&branch=${branch}&workflow=${workflow_id}` - link_node.href = window.location.href - link_node.appendChild(img_node) - document.querySelector("span.energy-badge-container").appendChild(link_node) - document.querySelector(".copy-badge").addEventListener('click', copyToClipboard) + img_node.onerror = function() {this.src='/images/no-data-badge.webp'} + link_node.href = '#' + + const energy_last = link_node.cloneNode(true) + const energy_last_image = img_node.cloneNode(true) + energy_last_image.onerror = function() {this.src='/images/no-data-badge.webp'} + energy_last.appendChild(energy_last_image) + + const carbon_last = link_node.cloneNode(true) + const carbon_last_image = img_node.cloneNode(true) + carbon_last_image.src = `${carbon_last_image.src}&metric=carbon` + carbon_last_image.onerror = function() {this.src='/images/no-data-badge.webp'} + carbon_last.appendChild(carbon_last_image) + + const energy_totals = link_node.cloneNode(true) + const energy_totals_image = img_node.cloneNode(true) + energy_totals_image.src = `${energy_totals_image.src}&mode=totals` + energy_totals_image.onerror = function() {this.src='/images/no-data-badge.webp'} + energy_totals.appendChild(energy_totals_image) + + const carbon_totals = link_node.cloneNode(true) + const carbon_totals_image = img_node.cloneNode(true) + carbon_totals_image.src = `${carbon_totals_image.src}&mode=totals&metric=carbon` + carbon_totals_image.onerror = function() {this.src='/images/no-data-badge.webp'} + carbon_totals.appendChild(carbon_totals_image) + + const carbon_totals_monthly = link_node.cloneNode(true) + const carbon_totals_monthly_image = img_node.cloneNode(true) + carbon_totals_monthly_image.src = `${carbon_totals_monthly_image.src}&mode=totals&metric=carbon&duration_days=30` + carbon_totals_monthly_image.onerror = function() {this.src='/images/no-data-badge.webp'} + carbon_totals_monthly.appendChild(carbon_totals_monthly_image) + + const energy_totals_monthly = link_node.cloneNode(true) + const energy_totals_monthly_image = img_node.cloneNode(true) + energy_totals_monthly_image.src = `${energy_totals_monthly_image.src}&mode=totals&duration_days=30` + energy_totals_monthly_image.onerror = function() {this.src='/images/no-data-badge.webp'} + energy_totals_monthly.appendChild(energy_totals_monthly_image) + + + document.querySelector("#energy-badge-container-last").appendChild(energy_last) + document.querySelector("#energy-badge-container-totals").appendChild(energy_totals) + document.querySelector("#carbon-badge-container-last").appendChild(carbon_last) + document.querySelector("#carbon-badge-container-totals").appendChild(carbon_totals) + + document.querySelector("#energy-badge-container-totals-monthly").appendChild(energy_totals_monthly) + document.querySelector("#carbon-badge-container-totals-monthly").appendChild(carbon_totals_monthly) + + document.querySelectorAll(".copy-badge").forEach(el => {el.addEventListener('click', copyToClipboard)}) } catch (err) { showNotification('Could not get badge data from API', err); } @@ -458,7 +502,7 @@ $(document).ready((e) => { ci_data_node.insertAdjacentHTML('afterbegin', `Branch:${escapeString(url_params.get('branch'))}`) ci_data_node.insertAdjacentHTML('afterbegin', `Workflow ID:${escapeString(workflow_id)}`) - getLastRunBadge(repo, branch, workflow_id) // async + getBadges(repo, branch, workflow_id) // async $('#rangestart input').val(new Date((new Date()).setDate((new Date).getDate() -7))) // set default on load $('#rangeend input').val(new Date()) // set default on load diff --git a/tests/api/test_api_helpers.py b/tests/api/test_api_helpers.py index b359083e4..9cbc26ba8 100644 --- a/tests/api/test_api_helpers.py +++ b/tests/api/test_api_helpers.py @@ -24,27 +24,27 @@ class CI_Measurement(BaseModel): duration: int -def test_rescale_energy_value(): - assert api_helpers.rescale_energy_value(100, 'uJ') == [100, 'uJ'] +def test_rescale_metric_value(): + assert api_helpers.rescale_metric_value(100, 'uJ') == [100, 'uJ'] - assert api_helpers.rescale_energy_value(10000, 'uJ') == [10, 'mJ'] + assert api_helpers.rescale_metric_value(10000, 'uJ') == [10, 'mJ'] - assert api_helpers.rescale_energy_value(10000, 'mJ') == [10, 'J'] + assert api_helpers.rescale_metric_value(10000, 'mJ') == [10, 'J'] - assert api_helpers.rescale_energy_value(324_000_000_000, 'uJ') == [324, 'kJ'] + assert api_helpers.rescale_metric_value(324_000_000_000, 'uJ') == [324, 'kJ'] - assert api_helpers.rescale_energy_value(324_000_000_000, 'ugCO2e/Page Request') == [324, 'kgCO2e/Page Request'] + assert api_helpers.rescale_metric_value(324_000_000_000, 'ugCO2e/Page Request') == [324, 'kgCO2e/Page Request'] - assert api_helpers.rescale_energy_value(222_000_000_000_000, 'ugCO2e/Kill') == [222, 'MgCO2e/Kill'] + assert api_helpers.rescale_metric_value(222_000_000_000_000, 'ugCO2e/Kill') == [222, 'MgCO2e/Kill'] - assert api_helpers.rescale_energy_value(0.0003, 'ugCO2e/Kill') == [0.3, 'ngCO2e/Kill'] + assert api_helpers.rescale_metric_value(0.0003, 'ugCO2e/Kill') == [0.3, 'ngCO2e/Kill'] with pytest.raises(ValueError): - api_helpers.rescale_energy_value(100, 'xJ') # expecting only mJ and uJ + api_helpers.rescale_metric_value(100, 'xJ') with pytest.raises(ValueError): - api_helpers.rescale_energy_value(100, 'uj') # expecting only mJ and uJ + api_helpers.rescale_metric_value(100, 'uj') From 82e5703166413530848237089437054b73a04799 Mon Sep 17 00:00:00 2001 From: Arne Tarara Date: Thu, 19 Dec 2024 18:45:34 +0100 Subject: [PATCH 11/15] Removing href for non actual links [skip ci] (#1026) --- frontend/authentication.html | 2 +- frontend/ci-index.html | 2 +- frontend/ci.html | 18 +++++++++--------- frontend/compare.html | 20 ++++++++++---------- frontend/css/green-coding.css | 5 +++++ frontend/data-analysis.html | 8 ++++---- frontend/energy-timeline.html | 2 +- frontend/index.html | 2 +- frontend/js/energy-timeline.js | 4 ++-- frontend/js/timeline.js | 2 +- frontend/repositories.html | 2 +- frontend/request.html | 2 +- frontend/settings.html | 2 +- frontend/stats.html | 30 +++++++++++++++--------------- frontend/status.html | 2 +- frontend/timeline.html | 4 ++-- 16 files changed, 56 insertions(+), 51 deletions(-) diff --git a/frontend/authentication.html b/frontend/authentication.html index a865513c3..37d3fbe64 100644 --- a/frontend/authentication.html +++ b/frontend/authentication.html @@ -27,7 +27,7 @@

- + Authentication

diff --git a/frontend/ci-index.html b/frontend/ci-index.html index e76cff138..855347db2 100644 --- a/frontend/ci-index.html +++ b/frontend/ci-index.html @@ -31,7 +31,7 @@

- + CI Projects