diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 532ea10e3..b25c4c4a3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,16 +7,16 @@ version: 2 updates: - package-ecosystem: "pip" directory: "/" - target-branch: "dev" + target-branch: "main" schedule: interval: "daily" - package-ecosystem: "docker" directory: "/" - target-branch: "dev" + target-branch: "main" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" - target-branch: "dev" + target-branch: "main" schedule: interval: "daily" diff --git a/.github/workflows/tests-bare-metal-dev.yml b/.github/workflows/tests-bare-metal-dev.yml deleted file mode 100644 index bfb9bef2e..000000000 --- a/.github/workflows/tests-bare-metal-dev.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Daily Test Run - Bare Metal - Dev Branch -run-name: Scheduled - DEV Branch -on: - schedule: - - cron: '0 */6 * * *' - workflow_dispatch: - -jobs: - run-tests-dev: - runs-on: self-hosted - permissions: - packages: write - contents: read - steps: - # - id: check-date - # if: ${{ github.event_name != 'workflow_dispatch' }} - # uses: green-coding-berlin/eco-ci-activity-checker@main - # with: - # repo: 'green-coding-berlin/green-metrics-tool' - # branch: 'dev' - # workflow-id: 45267390 - - # - if: ${{ github.event_name == 'workflow_dispatch' || steps.check-date.outputs.should_run == 'true'}} - - name: 'Checkout repository' - uses: actions/checkout@v3 - with: - ref: 'dev' - submodules: 'true' - - - name: Eco CI Energy Estimation - Initialize - uses: green-coding-berlin/eco-ci-energy-estimation@main - with: - task: start-measurement - - # - if: ${{ github.event_name == 'workflow_dispatch' || steps.check-date.outputs.should_run == 'true'}} - - name: 'Setup, Run, and Teardown Tests' - uses: ./.github/actions/gmt-pytest - with: - metrics-to-turn-off: 'Machine Sensors Debug MacOS' - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Eco CI Energy Estimation - Get Measurement - uses: green-coding-berlin/eco-ci-energy-estimation@main - with: - task: get-measurement - branch: dev - - - name: Eco CI Energy Estimation - End Measurement - uses: green-coding-berlin/eco-ci-energy-estimation@main - with: - task: display-results - branch: dev diff --git a/.github/workflows/tests-eco-ci-energy-estimation.yaml b/.github/workflows/tests-eco-ci-energy-estimation.yaml index c736043a6..e1a184d1c 100644 --- a/.github/workflows/tests-eco-ci-energy-estimation.yaml +++ b/.github/workflows/tests-eco-ci-energy-estimation.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: jobs: - run-tests-dev: + run-tests-main: runs-on: ubuntu-latest permissions: packages: write @@ -13,7 +13,7 @@ jobs: - name: 'Checkout repository' uses: actions/checkout@v3 with: - ref: 'dev' + ref: 'main' submodules: 'true' - name: Eco CI Energy Estimation - Initialize @@ -37,13 +37,13 @@ jobs: uses: green-coding-berlin/eco-ci-energy-estimation@testing with: task: get-measurement - branch: dev + branch: main - name: Eco CI Energy Estimation - End Measurement uses: green-coding-berlin/eco-ci-energy-estimation@testing with: task: display-results - branch: dev + branch: main diff --git a/.github/workflows/tests-vm-dev.yml b/.github/workflows/tests-vm-dev.yml deleted file mode 100644 index eb924308b..000000000 --- a/.github/workflows/tests-vm-dev.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Daily Test Run - Virtual Machine - Dev Branch -run-name: Scheduled - DEV Branch -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -jobs: - run-tests-dev: - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - id: check-date - if: ${{ github.event_name != 'workflow_dispatch' }} - uses: green-coding-berlin/eco-ci-activity-checker@v1 - with: - branch: 'dev' - - - if: ${{ github.event_name == 'workflow_dispatch' || steps.check-date.outputs.should_run == 'true'}} - name: 'Checkout repository' - uses: actions/checkout@v3 - with: - ref: 'dev' - submodules: 'true' - - - name: Eco CI Energy Estimation - Initialize - uses: green-coding-berlin/eco-ci-energy-estimation@701b5f2f4ba601be587823cd0786f07cb6ae2ee6 - with: - task: start-measurement - - - if: ${{ github.event_name == 'workflow_dispatch' || steps.check-date.outputs.should_run == 'true'}} - name: 'Setup, Run, and Teardown Tests' - uses: ./.github/actions/gmt-pytest - with: - metrics-to-turn-off: '--categories RAPL Machine Sensors Debug CGroupV2 MacOS --providers CpuFrequencySysfsCoreProvider' - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Eco CI Energy Estimation - Get Measurement - uses: green-coding-berlin/eco-ci-energy-estimation@701b5f2f4ba601be587823cd0786f07cb6ae2ee6 - with: - task: get-measurement - branch: dev - - - name: Eco CI Energy Estimation - End Measurement - uses: green-coding-berlin/eco-ci-energy-estimation@701b5f2f4ba601be587823cd0786f07cb6ae2ee6 - with: - task: display-results - branch: dev - - - diff --git a/.github/workflows/tests-vm-main.yml b/.github/workflows/tests-vm-main.yml index e6c5b6db0..bc46f217f 100644 --- a/.github/workflows/tests-vm-main.yml +++ b/.github/workflows/tests-vm-main.yml @@ -26,7 +26,7 @@ jobs: submodules: 'true' - name: Eco CI Energy Estimation - Initialize - uses: green-coding-berlin/eco-ci-energy-estimation@v2 + uses: green-coding-berlin/eco-ci-energy-estimation@701b5f2f4ba601be587823cd0786f07cb6ae2ee6 with: task: start-measurement @@ -38,13 +38,13 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Eco CI Energy Estimation - Get Measurement - uses: green-coding-berlin/eco-ci-energy-estimation@v2 + uses: green-coding-berlin/eco-ci-energy-estimation@701b5f2f4ba601be587823cd0786f07cb6ae2ee6 with: task: get-measurement branch: main - name: Eco CI Energy Estimation - End Measurement - uses: green-coding-berlin/eco-ci-energy-estimation@v2 + uses: green-coding-berlin/eco-ci-energy-estimation@701b5f2f4ba601be587823cd0786f07cb6ae2ee6 with: task: display-results branch: main diff --git a/.github/workflows/tests-vm-pr.yml b/.github/workflows/tests-vm-pr.yml index 0d859c465..f16f01261 100644 --- a/.github/workflows/tests-vm-pr.yml +++ b/.github/workflows/tests-vm-pr.yml @@ -9,6 +9,7 @@ jobs: permissions: packages: write contents: read + pull-requests: write steps: - name: 'Checkout repository' uses: actions/checkout@v3 @@ -31,13 +32,12 @@ jobs: uses: green-coding-berlin/eco-ci-energy-estimation@v2 with: task: get-measurement - branch: dev + branch: main - name: Eco CI Energy Estimation - End Measurement uses: green-coding-berlin/eco-ci-energy-estimation@v2 with: task: display-results - branch: dev - - - + branch: main + pr-comment: true + \ No newline at end of file diff --git a/README.md b/README.md index 862a14cd7..811f18779 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Tests Status - Main](https://github.com/green-coding-berlin/green-metrics-tool/actions/workflows/tests-vm-main.yml/badge.svg)](https://github.com/green-coding-berlin/green-metrics-tool/actions/workflows/tests-vm-main.yml) -[![Tests Status - Dev](https://github.com/green-coding-berlin/green-metrics-tool/actions/workflows/tests-vm-dev.yml/badge.svg)](https://github.com/green-coding-berlin/green-metrics-tool/actions/workflows/tests-vm-dev.yml) [![Energy Used](https://api.green-coding.berlin/v1/ci/badge/get/?repo=green-coding-berlin/green-metrics-tool&branch=dev&workflow=45267392)](https://metrics.green-coding.berlin/ci.html?repo=green-coding-berlin/green-metrics-tool&branch=dev&workflow=45267392) (This is the energy cost of running our CI-Pipelines on Github. [Find out more about Eco-CI](https://www.green-coding.berlin/projects/eco-ci/)) diff --git a/api/api.py b/api/api.py index 6a909aad9..56cfddec5 100644 --- a/api/api.py +++ b/api/api.py @@ -8,6 +8,7 @@ import sys import os +from xml.sax.saxutils import escape as xml_escape from fastapi import FastAPI, Request, Response, status from fastapi.responses import ORJSONResponse from fastapi.encoders import jsonable_encoder @@ -31,7 +32,6 @@ sanitize, get_phase_stats, get_phase_stats_object, is_valid_uuid, rescale_energy_value) - # It seems like FastAPI already enables faulthandler as it shows stacktrace on SEGFAULT # Is the redundant call problematic faulthandler.enable() # will catch segfaults and write to STDERR @@ -133,7 +133,7 @@ async def get_notes(project_id): @app.get('/v1/machines/') async def get_machines(): query = """ - SELECT id, description + SELECT id, description, available FROM machines ORDER BY description ASC """ @@ -146,14 +146,28 @@ async def get_machines(): # A route to return all of the available entries in our catalog. @app.get('/v1/projects') -async def get_projects(): +async def get_projects(repo: str, filename: str): query = """ SELECT a.id, a.name, a.uri, COALESCE(a.branch, 'main / master'), a.end_measurement, a.last_run, a.invalid_project, a.filename, b.description, a.commit_hash FROM projects as a LEFT JOIN machines as b on a.machine_id = b.id - ORDER BY a.created_at DESC -- important to order here, the charting library in JS cannot do that automatically! + WHERE 1=1 """ - data = DB().fetch_all(query) + params = [] + + filename = filename.strip() + if filename not in ('', 'null'): + query = f"{query} AND a.filename LIKE %s \n" + params.append(f"%{filename}%") + + repo = repo.strip() + if repo not in ('', 'null'): + query = f"{query} AND a.uri LIKE %s \n" + params.append(f"%{repo}%") + + query = f"{query} ORDER BY a.created_at DESC -- important to order here, the charting library in JS cannot do that automatically!" + + data = DB().fetch_all(query, params=tuple(params)) if data is None or data == []: return Response(status_code=204) # No-Content @@ -343,42 +357,46 @@ async def get_badge_single(project_id: str, metric: str = 'ml-estimated'): return ORJSONResponse({'success': False, 'err': 'Project ID is not a valid UUID or empty'}, status_code=400) query = ''' - WITH times AS ( - SELECT start_measurement, end_measurement FROM projects WHERE id = %s - ) SELECT - (SELECT start_measurement FROM times), (SELECT end_measurement FROM times), - SUM(measurements.value), measurements.unit - FROM measurements + SELECT + SUM(value), MAX(unit) + FROM + phase_stats WHERE - measurements.project_id = %s - AND measurements.time >= (SELECT start_measurement FROM times) - AND measurements.time <= (SELECT end_measurement FROM times) - AND measurements.metric LIKE %s - GROUP BY measurements.unit + project_id = %s + AND metric LIKE %s + AND phase LIKE '%%_[RUNTIME]' ''' value = None + label = 'Energy Cost' + via = '' if metric == 'ml-estimated': value = 'psu_energy_ac_xgboost_machine' + via = 'via XGBoost ML' elif metric == 'RAPL': - value = '%_rapl_%' + value = '%_energy_rapl_%' + via = 'via RAPL' elif metric == 'AC': value = 'psu_energy_ac_%' + via = 'via PSU (AC)' + elif metric == 'SCI': + label = 'SCI' + value = 'software_carbon_intensity_global' else: return ORJSONResponse({'success': False, 'err': f"Unknown metric '{metric}' submitted"}, status_code=400) - params = (project_id, project_id, value) + params = (project_id, value) data = DB().fetch_one(query, params=params) - if data is None or data == []: + if data is None or data == [] or not data[1] : badge_value = 'No energy data yet' else: - [energy_value, energy_unit] = rescale_energy_value(data[2], data[3]) - badge_value= f"{energy_value:.2f} {energy_unit} via {metric}" + [energy_value, energy_unit] = rescale_energy_value(data[0], data[1]) + badge_value= f"{energy_value:.2f} {energy_unit} {via}" badge = anybadge.Badge( - label='Energy cost', - value=badge_value, + label=xml_escape(label), + value=xml_escape(badge_value), num_value_padding_chars=1, default_color='cornflowerblue') return Response(content=str(badge), media_type="image/svg+xml") @@ -580,7 +598,7 @@ async def get_ci_badge_get(repo: str, branch: str, workflow:str): badge = anybadge.Badge( label='Energy Used', - value=badge_value, + value=xml_escape(badge_value), num_value_padding_chars=1, default_color='green') return Response(content=str(badge), media_type="image/svg+xml") diff --git a/api/api_helpers.py b/api/api_helpers.py index 57d131d57..14276adce 100644 --- a/api/api_helpers.py +++ b/api/api_helpers.py @@ -21,6 +21,17 @@ METRIC_MAPPINGS = { + + 'embodied_carbon_share_machine': { + 'clean_name': 'Embodied Carbon', + 'source': 'formula', + 'explanation': 'Embodied carbon attributed by time share of the life-span and total embodied carbon', + }, + 'software_carbon_intensity_global': { + 'clean_name': 'SCI', + 'source': 'formula', + 'explanation': 'SCI metric by the Green Software Foundation', + }, 'phase_time_syscall_system': { 'clean_name': 'Phase Duration', 'source': 'Syscall', @@ -223,17 +234,21 @@ def rescale_energy_value(value, unit): # We only expect values to be mJ for energy! - if unit != 'mJ': - raise RuntimeError('Unexpected unit occured for energy rescaling: ', unit) + if unit in ['mJ', 'ug'] or unit.startswith('ugCO2e/'): + unit_type = unit[1:] - energy_rescaled = [value, unit] + energy_rescaled = [value, unit] + + # pylint: disable=multiple-statements + if value > 1_000_000_000: energy_rescaled = [value/(10**12), f"G{unit_type}"] + elif value > 1_000_000_000: energy_rescaled = [value/(10**9), f"M{unit_type}"] + elif value > 1_000_000: energy_rescaled = [value/(10**6), f"k{unit_type}"] + elif value > 1_000: energy_rescaled = [value/(10**3), f"m{unit_type}"] + elif value < 0.001: energy_rescaled = [value*(10**3), f"n{unit_type}"] + + else: + raise RuntimeError('Unexpected unit occured for energy rescaling: ', unit) - # pylint: disable=multiple-statements - if value > 1_000_000_000: energy_rescaled = [value/(10**12), 'GJ'] - elif value > 1_000_000_000: energy_rescaled = [value/(10**9), 'MJ'] - elif value > 1_000_000: energy_rescaled = [value/(10**6), 'kJ'] - elif value > 1_000: energy_rescaled = [value/(10**3), 'J'] - elif value < 0.001: energy_rescaled = [value*(10**3), 'nJ'] return energy_rescaled @@ -309,13 +324,14 @@ def determine_comparison_case(ids): # these cannot be just averaged. But they have to be split and then compared via t-test # For the moment I think it makes sense to restrict to two repositories. Comparing three is too much to handle I believe if we do not want to drill down to one specific metric - # Currently we support five cases: + # Currently we support six cases: # case = 'Repository' # Case D : RequirementsEngineering Case # case = 'Branch' # Case C_3 : SoftwareDeveloper Case # case = 'Usage Scenario' # Case C_2 : SoftwareDeveloper Case # case = 'Machine' # Case C_1 : DataCenter Case # case = 'Commit' # Case B: DevOps Case # case = 'Repeated Run' # Case A: Blue Angel + # case = 'Multi-Commit' # Case D: Evolution of repo over time if repos == 2: # diff repos @@ -362,7 +378,7 @@ def determine_comparison_case(ids): if commit_hashes == 2: # same repo, same usage scenarios, same machines, diff commit hashes case = 'Commit' # Case B elif commit_hashes > 2: # same repo, same usage scenarios, same machines, many commit hashes - raise RuntimeError('Multiple Commits not supported. Please switch to Timeline view') + raise RuntimeError('Multiple commits comparison not supported. Please switch to Timeline view') else: # same repo, same usage scenarios, same machines, same branches, same commit hashes case = 'Repeated Run' # Case A else: # same repo, same usage scenarios, same machines, diff branch @@ -503,8 +519,7 @@ def get_phase_stats_object(phase_stats, case): phase_stats_object = { 'comparison_case': case, - 'comparison_details': [], - 'statistics': {}, + 'comparison_details': set(), 'data': {} } @@ -527,14 +542,10 @@ def get_phase_stats_object(phase_stats, case): else: key = commit_hash # No comparison case / Case A: Blue Angel / Case B: DevOps Case - if key not in phase_stats_object['data']: - phase_stats_object['data'][key] = {} - phase_stats_object['comparison_details'].append(key) + if phase not in phase_stats_object['data']: phase_stats_object['data'][phase] = {} - if phase not in phase_stats_object['data'][key]: phase_stats_object['data'][key][phase] = {} - - if metric_name not in phase_stats_object['data'][key][phase]: - phase_stats_object['data'][key][phase][metric_name] = { + if metric_name not in phase_stats_object['data'][phase]: + phase_stats_object['data'][phase][metric_name] = { 'clean_name': METRIC_MAPPINGS[metric_name]['clean_name'], 'explanation': METRIC_MAPPINGS[metric_name]['explanation'], 'type': metric_type, @@ -548,20 +559,43 @@ def get_phase_stats_object(phase_stats, case): 'data': {}, } - if detail_name not in phase_stats_object['data'][key][phase][metric_name]['data']: - phase_stats_object['data'][key][phase][metric_name]['data'][detail_name] = { + if detail_name not in phase_stats_object['data'][phase][metric_name]['data']: + phase_stats_object['data'][phase][metric_name]['data'][detail_name] = { 'name': detail_name, - 'mean': None, # this is the mean over all repetitions of the detail_name + # 'mean': None, # mean for a detail over multiple machines / branches makes no sense + # 'max': max_value, # max for a detail over multiple machines / branches makes no sense + # 'min': min_value, # min for a detail over multiple machines / branches makes no sense + # 'stddev': None, # stddev for a detail over multiple machines / branches makes no sense + # 'ci': None, # since we only compare two keys atm this could no be calculated. + 'p_value': None, # comparing the means of two machines, branches etc. Both cases must have multiple values for this to get populated + 'is_significant': None, # comparing the means of two machines, branches etc. Both cases must have multiple values for this to get populated + 'data': {}, + } + + detail_data = phase_stats_object['data'][phase][metric_name]['data'][detail_name]['data'] + if key not in detail_data: + detail_data[key] = { + 'mean': None, # this is the mean over all repetitions of the detail_name for the key 'max': max_value, 'min': min_value, + 'max_mean': None, + 'min_mean': None, 'stddev': None, 'ci': None, 'p_value': None, # only for the last key the list compare to the rest. one-sided t-test 'is_significant': None, # only for the last key the list compare to the rest. one-sided t-test 'values': [], } + phase_stats_object['comparison_details'].add(key) - phase_stats_object['data'][key][phase][metric_name]['data'][detail_name]['values'].append(value) + detail_data[key]['values'].append(value) + + # since we do not save the min/max values we need to to the comparison here in every loop again + # all other statistics are derived later in add_phase_stats_statistics() + detail_data[key]['max'] = max((x for x in [max_value, detail_data[key]['max']] if x is not None), default=None) + detail_data[key]['min'] = min((x for x in [min_value, detail_data[key]['min']] if x is not None), default=None) + + phase_stats_object['comparison_details'] = list(phase_stats_object['comparison_details']) return phase_stats_object @@ -569,72 +603,66 @@ def get_phase_stats_object(phase_stats, case): ''' Here we need to traverse the object again and calculate all the averages we need This could have also been done while constructing the object through checking when a change - in phase / detail_name etc. occurs. + in phase / detail_name etc. occurs., however this is more efficient ''' def add_phase_stats_statistics(phase_stats_object): - ## build per comparison key stats - for key in phase_stats_object['data']: - for phase, phase_data in phase_stats_object['data'][key].items(): - for metric_name, metric in phase_data.items(): - for detail_name, detail in metric['data'].items(): + for _, phase_data in phase_stats_object['data'].items(): + for _, metric in phase_data.items(): + for _, detail in metric['data'].items(): + for _, key_obj in detail['data'].items(): + # if a detail has multiple values we calculate a std.dev and the one-sided t-test for the last value - detail['mean'] = detail['values'][0] # default. might be overridden + key_obj['mean'] = key_obj['values'][0] # default. might be overridden + key_obj['max_mean'] = key_obj['values'][0] # default. might be overridden + key_obj['min_mean'] = key_obj['values'][0] # default. might be overridden - if len(detail['values']) > 1: - t_stat = get_t_stat(len(detail['values'])) + if len(key_obj['values']) > 1: + t_stat = get_t_stat(len(key_obj['values'])) # JSON does not recognize the numpy data types. Sometimes int64 is returned - detail['mean'] = float(np.mean(detail['values'])) - detail['stddev'] = float(np.std(detail['values'])) - detail['max'] = float(np.max(detail['values'])) # overwrite with max of list - detail['min'] = float(np.min(detail['values'])) # overwrite with min of list - detail['ci'] = detail['stddev']*t_stat - - if len(detail['values']) > 2: - data_c = detail['values'].copy() + key_obj['mean'] = float(np.mean(key_obj['values'])) + key_obj['stddev'] = float(np.std(key_obj['values'])) + key_obj['max_mean'] = np.max(key_obj['values']) # overwrite with max of list + key_obj['min_mean'] = np.min(key_obj['values']) # overwrite with min of list + key_obj['ci'] = key_obj['stddev']*t_stat + + if len(key_obj['values']) > 2: + data_c = key_obj['values'].copy() pop_mean = data_c.pop() _, p_value = scipy.stats.ttest_1samp(data_c, pop_mean) if not np.isnan(p_value): - detail['p_value'] = p_value - if detail['p_value'] > 0.05: - detail['is_significant'] = False + key_obj['p_value'] = p_value + if key_obj['p_value'] > 0.05: + key_obj['is_significant'] = False else: - detail['is_significant'] = True + key_obj['is_significant'] = True ## builds stats between the keys if len(phase_stats_object['comparison_details']) == 2: # since we currently allow only two comparisons we hardcode this here + # this is then needed to rebuild if we would allow more key1 = phase_stats_object['comparison_details'][0] key2 = phase_stats_object['comparison_details'][1] # we need to traverse only one branch of the tree like structure, as we only need to compare matching metrics - for phase, phase_data in phase_stats_object['data'][key1].items(): - phase_stats_object['statistics'][phase] = {} - for metric_name, metric in phase_data.items(): - phase_stats_object['statistics'][phase][metric_name] = {} - for detail_name, detail in metric['data'].items(): - phase_stats_object['statistics'][phase][metric_name][detail_name] = {} - try: # other metric or phase might not be present - detail2 = phase_stats_object['data'][key2][phase][metric_name]['data'][detail_name] - except KeyError: + for _, phase_data in phase_stats_object['data'].items(): + for _, metric in phase_data.items(): + for _, detail in metric['data'].items(): + if key1 not in detail['data'] or key2 not in detail['data']: continue - statistics_node = phase_stats_object['statistics'][phase][metric_name][detail_name] # Welch-Test because we cannot assume equal variances - _, p_value = scipy.stats.ttest_ind(detail['values'], detail2['values'], equal_var=False) # + _, p_value = scipy.stats.ttest_ind(detail['data'][key1]['values'], detail['data'][key2]['values'], equal_var=False) - if np.isnan(p_value): - statistics_node['p_value'] = None - statistics_node['is_significant'] = None - else: - statistics_node['p_value'] = p_value - if statistics_node['p_value'] > 0.05: - statistics_node['is_significant'] = False + if not np.isnan(p_value): + detail['p_value'] = p_value + if detail['p_value'] > 0.05: + detail['is_significant'] = False else: - statistics_node['is_significant'] = True + detail['is_significant'] = True return phase_stats_object diff --git a/config.yml.example b/config.yml.example index 7ae1018ae..b1170a447 100644 --- a/config.yml.example +++ b/config.yml.example @@ -27,6 +27,7 @@ admin: notify_admin_for_own_project_ready: False + cluster: api_url: __API_URL__ metrics_url: __METRICS_URL__ @@ -120,3 +121,31 @@ measurement: # HW_MemAmountGB: 16 # Hardware_Availability_Year: 2011 #--- END + + +sci: + # https://github.com/Green-Software-Foundation/sci/blob/main/Software_Carbon_Intensity/Software_Carbon_Intensity_Specification.md + + # The values specific to the machine will be set here. The values that are specific to the + # software, like R – Functional unit, will be set in the usage_scenario.yml + + # EL Expected Lifespan; the anticipated time that the equipment will be installed. Value is in years + # The number 3.5 comes from a typical developer machine (Apple Macbook 16" 2023 - https://dataviz.boavizta.org/manufacturerdata?lifetime=3.5&name=14-inch%20MacBook%20Pro%20with%2064GB) + EL: 3.5 + # RS Resource-share; the share of the total available resources of the hardware reserved for use by the software. + # This ratio is typically 1 with the Green Metrics Tool unless you use a custom distributed orchestrator + RS: 1 + # TE Total Embodied Emissions; the sum of Life Cycle Assessment (LCA) emissions for all hardware components. + # Value is in gCO2eq + # The value has to be identified from vendor datasheets. Here are some example sources: + # https://dataviz.boavizta.org/manufacturerdata + # https://tco.exploresurface.com/sustainability/calculator + # https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/Full_LCA_Dell_R740.pdf + # The default is the value for a developer machine (Apple Macbook 16" 2023 - https://dataviz.boavizta.org/manufacturerdata?lifetime=3.5&name=14-inch%20MacBook%20Pro%20with%2064GB) + TE: 194000 + # I is the Carbon Intensity at the location of this machine + # The value can either be a number in gCO2e/kWh or a carbon intensity provider that fetches this number dynamically + # https://docs.green-coding.berlin/docs/measuring/carbon-intensity-providers/carbon-intensity-providers-overview/ + # For fixed values get the number from https://ember-climate.org/insights/research/global-electricity-review-2022/ + # The number 475 that comes as default is for Germany from 2022 + I: 475 \ No newline at end of file diff --git a/docker/Dockerfile-gunicorn b/docker/Dockerfile-gunicorn index 73bc24efc..2f77559c4 100644 --- a/docker/Dockerfile-gunicorn +++ b/docker/Dockerfile-gunicorn @@ -1,14 +1,8 @@ # syntax=docker/dockerfile:1 -FROM ubuntu:22.04 +FROM python:3.11.4-slim-bookworm ENV DEBIAN_FRONTEND=noninteractive -RUN rm -rf /var/lib/apt/lists/* -RUN apt update && \ - apt install python3 python3-pip gunicorn -y - COPY requirements.txt requirements.txt RUN pip3 install -r requirements.txt -RUN rm -rf /var/lib/apt/lists/* - -ENTRYPOINT ["/bin/gunicorn", "--workers=2", "--log-file=-", "--worker-tmp-dir=/dev/shm", "--threads=4", "--worker-class=gthread", "--bind", "unix:/tmp/green-coding-api.sock", "-m", "007", "--user", "www-data", "--chdir", "/var/www/green-metrics-tool/api", "-k", "uvicorn.workers.UvicornWorker", "api:app"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/gunicorn", "--workers=2", "--access-logfile=-", "--error-logfile=-", "--worker-tmp-dir=/dev/shm", "--threads=4", "--worker-class=gthread", "--bind", "unix:/tmp/green-coding-api.sock", "-m", "007", "--user", "www-data", "--chdir", "/var/www/green-metrics-tool/api", "-k", "uvicorn.workers.UvicornWorker", "api:app"] \ No newline at end of file diff --git a/docker/requirements.txt b/docker/requirements.txt index a6f34eef3..881b3955f 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,9 +1,10 @@ -psycopg[binary]==3.1.9 -fastapi==0.100.0 -uvicorn[standard]==0.23.1 +gunicorn==21.2.0 +psycopg[binary]==3.1.10 +fastapi==0.101.0 +uvicorn[standard]==0.23.2 pandas==2.0.3 PyYAML==6.0.1 anybadge==1.14.0 scipy==1.11.1 -orjson==3.9.2 -schema==0.7.5 \ No newline at end of file +orjson==3.9.3 +schema==0.7.5 diff --git a/frontend/css/green-coding.css b/frontend/css/green-coding.css index dee643bd5..b03199f83 100644 --- a/frontend/css/green-coding.css +++ b/frontend/css/green-coding.css @@ -160,6 +160,7 @@ a, text-overflow: ellipsis; overflow-wrap: normal; overflow: hidden; + white-space: nowrap; } .si-unit { diff --git a/frontend/index.html b/frontend/index.html index 4ab15ac55..61f7c9500 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -53,6 +53,13 @@
${value} | \ -${label} | \ + li_node.innerHTML = `${escapeString(value)} | \ +${escapeString(label)} | \${run_link_node} | \ -${dateToYMD(new Date(created_at))} | \ -${short_hash} | \ -${cpu} | \ -${duration} seconds | `; +${dateToYMD(new Date(created_at))} | \ +${escapeString(short_hash)} | \ +${escapeString(cpu)} | \ +${escapeString(duration)} seconds | `; document.querySelector("#ci-table").appendChild(li_node); }); $('table').tablesort(); @@ -318,17 +318,17 @@ $(document).ready((e) => { let repo_link = '' if(badges_data.data[0][8] == 'github') { - repo_link = `https://github.com/${url_params.get('repo')}`; + repo_link = `https://github.com/${escapeString(url_params.get('repo'))}`; } else if(badges_data.data[0][8] == 'gitlab') { - repo_link = `https://gitlab.com/${url_params.get('repo')}`; + repo_link = `https://gitlab.com/${escapeString(url_params.get('repo'))}`; } //${repo_link} - const repo_link_node = `${url_params.get('repo')}` + const repo_link_node = `${escapeString(url_params.get('repo'))}` document.querySelector('#ci-data').insertAdjacentHTML('afterbegin', `
Repository: | ${repo_link_node} | |||||||||||
Branch: | ${url_params.get('branch')} | |||||||||||
Workflow: | ${url_params.get('workflow')} | |||||||||||
Branch: | ${escapeString(url_params.get('branch'))} | |||||||||||
Workflow: | ${escapeString(url_params.get('workflow'))} | |||||||||||
${key} | ${phase_stats_data['common_info'][key]} | ${metric_data.clean_name} | -${metric_data.source} | -${scope} | -${detail_data.name} | -${value} | -${unit} | - -${max_value} | -${min_value} | `; + + if(detail_data.stddev != null) { + tr.innerHTML = ` +${metric_data.clean_name} | +${metric_data.source} | +${scope} | +${detail_name} | +${metric_data.type} | +${value} | +${unit} | +${std_dev_text_table} | +${max_value} | +${min_value} | +${max_mean_value} | +${min_mean_value} | `; + + } else { + tr.innerHTML = ` +${metric_data.clean_name} | +${metric_data.source} | +${scope} | +${detail_name} | +${metric_data.type} | +${value} | +${unit} | +${max_value} | +${min_value} | `; + } + updateKeyMetric( - phase, metric_name, metric_data.clean_name, detail_data.name, + phase, metric_name, metric_data.clean_name, detail_name, value , std_dev_text, unit, metric_data.explanation, metric_data.source ); @@ -214,18 +263,26 @@ const displaySimpleMetricBox = (phase, metric_name, metric_data, detail_data, co This function assumes that detail_data has only two elements. For everything else we would need to calculate a trend / regression and not a simple comparison */ -const displayDiffMetricBox = (phase, metric_name, metric_data, detail_data_array, comparison_key, is_significant) => { - let extra_label = ''; - if (is_significant == true) extra_label = 'Significant'; - else extra_label = 'not significant / no-test'; +const displayDiffMetricBox = (phase, metric_name, metric_data, detail_name, detail_data_array, is_significant) => { // no max, we use significant rather + let extra_label = 'not significant / no-test'; + if (is_significant == true) extra_label = 'Significant'; - // no value conversion, cause we just use relatives - let value = detail_data_array[0].mean == 0 ? 0: (((detail_data_array[1].mean - detail_data_array[0].mean)/detail_data_array[0].mean)*100).toFixed(2); + // TODO: Remove this guard clause once we want to support more than 2 compared items + if (detail_data_array.length > 2) throw "Comparions > 2 currently not implemented" - let icon_color = 'positive'; + // no value conversion in this block, cause we just use relatives + let value = 'N/A'; + if (detail_data_array[0] == 0 && detail_data_array[1] == 0) { + value = 0; + } else if (detail_data_array[0] == null || detail_data_array[1] == null) { + value = 'not comparable'; + } else { + value = detail_data_array[0] == 0 ? 0: (((detail_data_array[1] - detail_data_array[0])/detail_data_array[0])*100).toFixed(2); + } + let icon_color = 'positive'; if (value > 0) { icon_color = 'error'; value = `+ ${value} %`; @@ -236,15 +293,16 @@ const displayDiffMetricBox = (phase, metric_name, metric_data, detail_data_array let scope = metric_name.split('_') scope = scope[scope.length-1] - let [value_1, unit] = convertValue(detail_data_array[0].mean, metric_data.unit); - let [value_2, _] = convertValue(detail_data_array[1].mean, metric_data.unit); + let [value_1, unit] = convertValue(detail_data_array[0], metric_data.unit); + let [value_2, _] = convertValue(detail_data_array[1], metric_data.unit); let tr = document.querySelector(`div.tab[data-tab='${phase}'] table.compare-metrics-table tbody`).insertRow(); tr.innerHTML = `${metric_data.clean_name} | ${metric_data.source} | ${scope} | -${detail_data_array[0].name} | +${detail_name} | +${metric_data.type} | ${value_1} | ${value_2} | ${unit} | @@ -252,7 +310,7 @@ const displayDiffMetricBox = (phase, metric_name, metric_data, detail_data_array${extra_label} | `; updateKeyMetric( - phase, metric_name, metric_data.clean_name, detail_data_array[0].name, + phase, metric_name, metric_data.clean_name, detail_name, value, '', metric_data.unit, metric_data.explanation, metric_data.source ); @@ -301,17 +359,21 @@ const updateKeyMetric = (phase, metric_name, clean_name, detail_name, value, std let selector = null; // key metrics are already there, cause we want a fixed order, so we just replace - if(metric.match(/^.*_energy_.*_machine$/) !== null) { + if(machine_energy_metric_condition(metric)) { selector = '.machine-energy'; - } else if(metric == 'network_energy_formula_global') { + } else if(network_energy_metric_condition(metric)) { selector = '.network-energy'; - } else if(metric == 'phase_time_syscall_system') { + } else if(phase_time_metric_condition(metric)) { selector = '.phase-duration'; - } else if(metric == 'network_co2_formula_global') { + } else if(network_co2_metric_condition(metric)) { selector = '.network-co2'; - } else if(metric.match(/^.*_power_.*_machine$/) !== null) { + } else if(embodied_carbon_share_metric_condition(metric)) { + selector = '.embodied-carbon'; + } else if(sci_metric_condition(metric)) { + selector = '.software-carbon-intensity'; + } else if(machine_power_metric_condition(metric)) { selector = '.machine-power'; - } else if(metric.match(/^.*_co2_.*_machine$/) !== null) { + } else if(machine_co2_metric_condition(metric)) { selector = '.machine-co2'; } else { return; // could not match key metric @@ -321,11 +383,9 @@ const updateKeyMetric = (phase, metric_name, clean_name, detail_name, value, std document.querySelector(`div.tab[data-tab='${phase}'] ${selector} .value span`).innerText = `${(value)} ${std_dev_text}` document.querySelector(`div.tab[data-tab='${phase}'] ${selector} .si-unit`).innerText = `[${unit}]` if(std_dev_text != '') document.querySelector(`div.tab[data-tab='${phase}'] ${selector} .metric-type`).innerText = `(AVG + STD.DEV)`; - else if(value.indexOf('%') !== -1) document.querySelector(`div.tab[data-tab='${phase}'] ${selector} .metric-type`).innerText = `(Diff. in %)`; + else if(String(value).indexOf('%') !== -1) document.querySelector(`div.tab[data-tab='${phase}'] ${selector} .metric-type`).innerText = `(Diff. in %)`; node = document.querySelector(`div.tab[data-tab='${phase}'] ${selector} .source`) if (node !== null) node.innerText = source // not every key metric shall have a custom detail_name } - - diff --git a/frontend/js/helpers/phase-stats.js b/frontend/js/helpers/phase-stats.js index 99759d2f4..e1bcfee00 100644 --- a/frontend/js/helpers/phase-stats.js +++ b/frontend/js/helpers/phase-stats.js @@ -1,33 +1,43 @@ -const setupPhaseTabs = (phase_stats_object, multi_comparison) => { - let keys = Object.keys(phase_stats_object['data']) - // only need to traverse one branch in case of a comparison - // no need to display phases that do not exist in both - for (phase in phase_stats_object['data'][keys[0]]) { - createPhaseTab(phase); - let tr = document.querySelector(`div.tab[data-tab='${phase}'] .compare-metrics-table thead`).insertRow(); - if (multi_comparison) { - tr.innerHTML = ` -Metric | -Source | -Scope | -Detail Name | -${replaceRepoIcon(keys[0])} | -${replaceRepoIcon(keys[1])} | -Unit | -Change | -Significant (T-Test) | `; - } else { - tr.innerHTML = ` -Metric | -Source | -Scope | -Detail Name | -Value | -Unit | - -MAX | -MIN | `; - } +const createTableHeader = (phase, comparison_keys, comparison_case, comparison_amounts) => { + let tr = document.querySelector(`div.tab[data-tab='${phase}'] .compare-metrics-table thead`).insertRow(); + + if (comparison_amounts >= 2) { + tr.innerHTML = ` +Metric | +Source | +Scope | +Detail Name | +Type | +${replaceRepoIcon(comparison_keys[0])} | +${replaceRepoIcon(comparison_keys[1])} | +Unit | +Change | +Significant (T-Test) | `; + } else if(comparison_case !== null) { + tr.innerHTML = ` +Metric | +Source | +Scope | +Detail Name | +Type | +Value | +Unit | +StdDev | +Max. | +Min. | +Max. (of means) | +Min. (of means) | `; + } else { + tr.innerHTML = ` +Metric | +Source | +Scope | +Detail Name | +Type | +Value | +Unit | +Max. | +Min. | `; } } @@ -37,29 +47,8 @@ const showWarning = (phase, warning) => { const newListItem = document.createElement("li"); newListItem.textContent = warning; document.querySelector(`div.tab[data-tab='${phase}'] .ui.warning.message ul`).appendChild(newListItem); - - } -const determineMultiComparison = (comparison_case) => { - switch (comparison_case) { - case null: // single value - case 'Repeated Run': - return false; - break; - case 'Branch': - case 'Usage Scenario': - case 'Commit': - case 'Machine': - case 'Repository': - return true; - break; - default: - throw `Unknown comparison case: ${comparison_case}` - } -} - - /* This function was originally written to include new phases in the "steps" @@ -78,21 +67,21 @@ const determineMultiComparison = (comparison_case) => { not prepend */ const createPhaseTab = (phase) => { - let phase_tab_node = document.querySelector(`a.step[data-tab='${phase}']`); - if(phase_tab_node == null || phase_tab_node == undefined) { + + if(phase_tab_node == null) { let runtime_tab_node = document.querySelector('a.runtime-step'); let cloned_tab_node = runtime_tab_node.cloneNode(true); cloned_tab_node.style.display = ''; cloned_tab_node.innerText = phase; cloned_tab_node.setAttribute('data-tab', phase); - runtime_tab_node.parentNode.insertBefore(cloned_tab_node, runtime_tab_node) + runtime_tab_node.parentNode.appendChild(cloned_tab_node) let phase_step_node = document.querySelector('.runtime-tab'); let cloned_step_node = phase_step_node.cloneNode(true); cloned_step_node.style.display = ''; cloned_step_node.setAttribute('data-tab', phase); - phase_step_node.parentNode.insertBefore(cloned_step_node, phase_step_node) + phase_step_node.parentNode.appendChild(cloned_step_node) } } @@ -100,27 +89,35 @@ const createPhaseTab = (phase) => { We traverse the multi-dimensional metrics object only once and fill data in the appropriate variables for metric-boxes and charts */ -const displayComparisonMetrics = (phase_stats_object, comparison_case, multi_comparison) => { - - let keys = Object.keys(phase_stats_object['data']) +const displayComparisonMetrics = (phase_stats_object) => { + // we now traverse all branches of the tree + // the tree is sparese, so although we see all metrics that are throughout all phases and comparison_keys + // not every branch has a leaf with the metric for the corresponding comparsion_key ... it might be missing + // we must fill this value than with a NaN in the charts - // we need to traverse only one branch of the tree and copy in all other values if - // a matching metric exists in the other branch - // we first go through one branch until we reach the detail object - // we identify if a metric is a key metric regarding something and handle that - // we identify if a metric is a normal metric regarding something and handle that // if we have a comparison case between the two we just display the difference between them // if we have a repetition case we display the STDDEV // otherwise we just display the value + // unsure atm what to do in a Diff scenario ... filling with 0 can be misleading + let total_chart_bottom_data = {}; let total_chart_bottom_legend = {}; let total_chart_bottom_labels = []; - for (phase in phase_stats_object['data'][keys[0]]) { - let phase_data = phase_stats_object['data'][keys[0]][phase]; + + for (phase in phase_stats_object['data']) { + createPhaseTab(phase); // will not create already existing phase tabs + createTableHeader( + phase, + phase_stats_object.comparison_details, + phase_stats_object.comparison_case, + phase_stats_object.comparison_details.length + ) + + let phase_data = phase_stats_object['data'][phase]; let radar_chart_labels = []; - let radar_chart_data = [[],[]]; + let radar_chart_data = [...Array(phase_stats_object.comparison_details.length)].map(e => Array(0)); let top_bar_chart_labels = []; let top_bar_chart_data = [[],[]]; @@ -128,142 +125,130 @@ const displayComparisonMetrics = (phase_stats_object, comparison_case, multi_com total_chart_bottom_legend[phase] = []; let co2_calculated = false; - let found_bottom_chart_metric = false; - let phase_key0_has_machine_energy = false; - let phase_key1_has_machine_energy = false; + // the following variables are needed for filling missing values in charts and the machine energy + // we need to keep track if in the coming loop a matching metric was found or mitigate the missing value + let found_bottom_chart_metric = false; + const bottom_chart_present_keys = Object.fromEntries(phase_stats_object.comparison_details.map(e => [e, false])) for (metric in phase_data) { let metric_data = phase_data[metric] + let found_radar_chart_item = false; + for (detail in metric_data['data']) { let detail_data = metric_data['data'][detail] - // push data to chart that we need in any case - if(radar_chart_condition(metric) && multi_comparison) { + /* + BLOCK LABELS + This block must be done outside of the key loop and cannot use a Set() datastructure + as we can have the same metric multiple times just with different detail names + */ + if(radar_chart_condition(metric) && phase_stats_object.comparison_details.length >= 2) { radar_chart_labels.push(metric_data.clean_name); - radar_chart_data[0].push(detail_data.mean) } if (top_bar_chart_condition(metric)) { top_bar_chart_labels.push(`${metric_data.clean_name} (${metric_data.source})`); - top_bar_chart_data[0].push(detail_data.mean) } + if (total_chart_bottom_condition(metric)) { if(found_bottom_chart_metric) { showWarning(phase, `Another metric for the bottom chart was already set (${found_bottom_chart_metric}), skipping ${metric} and only first one will be shown.`); } else { total_chart_bottom_legend[phase].push(metric_data.clean_name); - - if(total_chart_bottom_data?.[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[0]}`] == null) { - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[0]}`] = [] - } - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[0]}`].push(detail_data.mean) - phase_key0_has_machine_energy = true - found_bottom_chart_metric = metric; + found_bottom_chart_metric = `${metric} ${detail_data['name']}`; } } + /* END BLOCK LABELS*/ - if (comparison_case == null && co2_metrics_condition(metric)) { - if(co2_calculated) { - showWarning(phase, 'CO2 was already calculated! Do you have CO2 Machine reporters set'); - } - co2_calculated = true; - calculateCO2(phase, detail_data.mean); + if (Object.keys(detail_data['data']).length != phase_stats_object.comparison_details.length) { + showWarning(phase, `${metric} ${detail} was missing from at least one comparison.`); } - if (!multi_comparison) { - displaySimpleMetricBox(phase,metric, metric_data, detail_data, keys[0]); - if(comparison_case !== null) { - displayCompareChart( - phase, - `${metric_data.clean_name} (${metric_data.source} ${detail}) - [${metric_data.unit}]`, - [`${comparison_case}: ${keys[0]}`], - [detail_data.values], - [{name:'Confidence Interval', bottom: detail_data.mean-detail_data.ci, top: detail_data.mean+detail_data.ci}], - ); + let compare_chart_data = [] + let compare_chart_mark = [] + let compare_chart_labels = [] + let metric_box_data = [...Array(phase_stats_object.comparison_details.length)].map(e => {{}}) + + // we loop over all keys that exist, not over the one that are present in detail_data['data'] + phase_stats_object.comparison_details.forEach((key,key_index) => { + if(radar_chart_condition(metric) && phase_stats_object.comparison_details.length >= 2) { + radar_chart_data[key_index].push(detail_data['data'][key]?.mean) } - } else { - let metric_data2 = phase_stats_object?.['data']?.[keys[1]]?.[phase]?.[metric] - let detail_data2 = metric_data2?.['data']?.[detail] - if (detail_data2 == undefined) { - // the metric or phase might not be present in the other run - // note that this debug statement does not log when on the second branch more metrics are - // present that are not shown. However we also do not want to display them. - showWarning(phase, `${metric} ${detail} was missing from one comparison. Skipping`); - continue; + if (top_bar_chart_condition(metric)) { + top_bar_chart_data[key_index].push(detail_data['data'][key]?.mean) + } + if (total_chart_bottom_condition(metric) && `${metric} ${detail_data['name']}` == found_bottom_chart_metric) { + if(total_chart_bottom_data?.[`${TOTAL_CHART_BOTTOM_LABEL} - ${key}`] == null) { + total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${key}`] = [] + } + total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${key}`].push(detail_data['data'][key]?.mean) + bottom_chart_present_keys[key] = true } + + if (phase_stats_object.comparison_case == null && machine_co2_metric_condition(metric)) { + if(co2_calculated) { + showWarning(phase, 'CO2 was already calculated! Do you have multiple machine energy reporters set?'); + } + // mean will always be present, as we only have one key and thus we need no ?. + calculateCO2(phase, detail_data['data'][key].mean); + co2_calculated = true; + } + + metric_box_data[key_index] = detail_data['data'][key]?.mean + compare_chart_data.push(detail_data['data'][key]?.values) + compare_chart_labels.push(`${phase_stats_object.comparison_case}: ${key}`) + compare_chart_mark.push({ + name:'Confidence Interval', + bottom: detail_data['data'][key]?.mean-detail_data['data'][key]?.ci, + top: detail_data['data'][key]?.mean+detail_data['data'][key]?.ci + }) + }) // end key + + if (phase_stats_object.comparison_details.length == 1) { + // Note: key is still the set variable from the for loop earlier + displaySimpleMetricBox(phase,metric, metric_data, detail_data['name'], detail_data['data'][phase_stats_object.comparison_details[0]]); + } else { displayDiffMetricBox( - phase, metric, metric_data, [detail_data, detail_data2], - keys[0], phase_stats_object.statistics?.[phase]?.[metric]?.[detail]?.is_significant + phase, metric, metric_data, detail_data['name'], metric_box_data, + detail_data.is_significant ); - - detail_chart_data = [detail_data.values,detail_data2.values] - detail_chart_mark = [ - {name:'Confidence Interval', bottom: detail_data.mean-detail_data.ci, top: detail_data.mean+detail_data.ci}, - {name:'Confidence Interval', bottom: detail_data2.mean-detail_data2.ci, top: detail_data2.mean+detail_data2.ci}, - ] + } + if(phase_stats_object.comparison_case !== null) { // compare charts will display for everything apart stats.html displayCompareChart( phase, `${metric_data.clean_name} (${detail}) - [${metric_data.unit}]`, - [`${comparison_case}: ${keys[0]}`, `${comparison_case}: ${keys[1]}`], - detail_chart_data, - detail_chart_mark, + compare_chart_labels, + compare_chart_data, + compare_chart_mark, ); - - if(radar_chart_condition(metric) && multi_comparison) { - radar_chart_data[1].push(detail_data2.mean) - } - - if (top_bar_chart_condition(metric)) { - top_bar_chart_data[1].push(detail_data2.mean) - } - if (total_chart_bottom_condition(metric)) { - if(found_bottom_chart_metric && found_bottom_chart_metric !== metric) { - showWarning(phase, `Another metric for the bottom chart was already set (${found_bottom_chart_metric}), skipping ${metric} and only first one will be shown.`); - } else { - if(total_chart_bottom_data?.[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[1]}`] == null) { - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[1]}`] = [] - } - - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[1]}`].push(detail_data2.mean) - phase_key1_has_machine_energy = true - } - } } + } // end detail + } // end metric + + // a phase had no bottom chart metric and must be null-filled + // this can for instance happen if a phase is too short and no metric was reported in the timespan + for (key in bottom_chart_present_keys) { + if(bottom_chart_present_keys[key] == false) { + if(total_chart_bottom_data?.[`${TOTAL_CHART_BOTTOM_LABEL} - ${key}`] == null) { + total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${key}`] = [] + } + total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${key}`].push(NaN) } } - // phase ended. Render out the chart - - - let radar_legend = [] - if (multi_comparison) { - radar_legend = [`${comparison_case}: ${keys[0]}`, `${comparison_case}: ${keys[1]}`] - } else { - radar_legend = [keys[0]] - } - if (phase_key0_has_machine_energy == false) { // add dummy - if(total_chart_bottom_data?.[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[0]}`] == null) { - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[0]}`] = [] - } - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[0]}`].push(0) - } - if (phase_key1_has_machine_energy == false && multi_comparison == 2) { // add dummy - if(total_chart_bottom_data?.[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[1]}`] == null) { - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[1]}`] = [] - } - total_chart_bottom_data[`${TOTAL_CHART_BOTTOM_LABEL} - ${keys[1]}`].push(0) - } + let radar_legend = phase_stats_object.comparison_details.map(e => `${phase_stats_object.comparison_case}: ${e}`) - if(multi_comparison) { + if(phase_stats_object.comparison_details.length >= 2) { displayKeyMetricsRadarChart( radar_legend, radar_chart_labels, radar_chart_data, phase ); - } else if(comparison_case != null) { // stats.html does not even have it. so only remove for Repeated Run etc. + } else if(phase_stats_object.comparison_case != null) { + // stats.html does not even have it. so only remove for Repeated Run etc. removeKeyMetricsRadarChart(phase) } @@ -276,7 +261,8 @@ const displayComparisonMetrics = (phase_stats_object, comparison_case, multi_com // displayKeyMetricsEmbodiedCarbonChart(phase); - } + } // phase end + displayTotalChart( total_chart_bottom_legend, total_chart_bottom_labels, @@ -313,4 +299,3 @@ const displayComparisonMetrics = (phase_stats_object, comparison_case, multi_com window.dispatchEvent(new Event('resize')); } - diff --git a/frontend/js/index.js b/frontend/js/index.js index 1ddaf0a8e..002bb9c72 100644 --- a/frontend/js/index.js +++ b/frontend/js/index.js @@ -5,7 +5,7 @@ const compareButton = () => { checkedBoxes.forEach(checkbox => { link = `${link}${checkbox.value},`; }); - window.location = link.substr(0,link.length-1); + window.open(link.substr(0,link.length-1), '_blank'); } const updateCompareCount = () => { const countButton = document.getElementById('compare-button'); @@ -13,7 +13,23 @@ const updateCompareCount = () => { countButton.textContent = `Compare: ${checkedCount} Run(s)`; } -function allow_group_select_checkboxes(checkbox_wrapper_id){ +const removeFilter = (paramName) => { + const urlSearchParams = new URLSearchParams(window.location.search); + urlSearchParams.delete(paramName); + const newUrl = `${window.location.pathname}?${urlSearchParams.toString()}`; + window.location.href = newUrl; +} + +const showActiveFilters = (key, value) => { + document.querySelector(`.ui.warning.message`).classList.remove('hidden'); + const newListItem = document.createElement("span"); + newListItem.innerHTML = `
---|