Skip to content

Commit

Permalink
Merge branch 'main' of github.com:green-coding-berlin/green-metrics-tool
Browse files Browse the repository at this point in the history
* 'main' of github.com:green-coding-berlin/green-metrics-tool:
  eco-ci will now post the energy reading in the PR conversation (#397)
  SCI metric (#412)
  Fixes the github security errors (#411)
  • Loading branch information
ArneTR committed Aug 4, 2023
2 parents 22e4366 + 367dc5a commit 755333a
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 87 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/tests-vm-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
permissions:
packages: write
contents: read
pull-requests: write
steps:
- name: 'Checkout repository'
uses: actions/checkout@v3
Expand Down Expand Up @@ -38,6 +39,5 @@ jobs:
with:
task: display-results
branch: main



pr-comment: true

44 changes: 24 additions & 20 deletions api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -311,42 +311,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")
Expand Down Expand Up @@ -548,7 +552,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")
Expand Down
33 changes: 24 additions & 9 deletions api/api_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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"{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

Expand Down
29 changes: 29 additions & 0 deletions config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ admin:
notify_admin_for_own_project_ready: False



cluster:
api_url: __API_URL__
metrics_url: __METRICS_URL__
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions frontend/css/green-coding.css
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ a,
text-overflow: ellipsis;
overflow-wrap: normal;
overflow: hidden;
white-space: nowrap;
}

.si-unit {
Expand Down
32 changes: 16 additions & 16 deletions frontend/js/ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const getChartOptions = (runs, chart_element) => {

const displayGraph = (runs) => {
const element = createChartContainer("#chart-container", "run-energy", runs);

const options = getChartOptions(runs, element);

const chart_instance = echarts.init(element);
Expand Down Expand Up @@ -241,26 +241,26 @@ const displayCITable = (runs, url_params) => {

var run_link = ''
if(source == 'github') {
run_link = `https://github.com/${url_params.get('repo')}/actions/runs/${run_id}`;
run_link = `https://github.com/${escapeString(url_params.get('repo'))}/actions/runs/${escapeString(run_id)}`;
}
else if (source == 'gitlab') {
run_link = `https://gitlab.com/${url_params.get('repo')}/-/pipelines/${run_id}`
run_link = `https://gitlab.com/${escapeString(url_params.get('repo'))}/-/pipelines/${escapeString(run_id)}`
}

const run_link_node = `<a href="${run_link}" target="_blank">${run_id}</a>`
const run_link_node = `<a href="${run_link}" target="_blank">${escapeString(run_id)}</a>`

const created_at = el[3]

const label = el[4]
const duration = el[7]

li_node.innerHTML = `<td class="td-index">${value}</td>\
<td class="td-index">${label}</td>\
li_node.innerHTML = `<td class="td-index">${escapeString(value)}</td>\
<td class="td-index">${escapeString(label)}</td>\
<td class="td-index">${run_link_node}</td>\
<td class="td-index"><span title="${created_at}">${dateToYMD(new Date(created_at))}</span></td>\
<td class="td-index" ${tooltip}>${short_hash}</td>\
<td class="td-index">${cpu}</td>\
<td class="td-index">${duration} seconds</td>`;
<td class="td-index"><span title="${escapeString(created_at)}">${dateToYMD(new Date(created_at))}</span></td>\
<td class="td-index" ${escapeString(tooltip)}>${escapeString(short_hash)}</td>\
<td class="td-index">${escapeString(cpu)}</td>\
<td class="td-index">${escapeString(duration)} seconds</td>`;
document.querySelector("#ci-table").appendChild(li_node);
});
$('table').tablesort();
Expand Down Expand Up @@ -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 = `<a href="${repo_link}" target="_blank">${url_params.get('repo')}</a>`
const repo_link_node = `<a href="${repo_link}" target="_blank">${escapeString(url_params.get('repo'))}</a>`
document.querySelector('#ci-data').insertAdjacentHTML('afterbegin', `<tr><td><strong>Repository:</strong></td><td>${repo_link_node}</td></tr>`)
document.querySelector('#ci-data').insertAdjacentHTML('afterbegin', `<tr><td><strong>Branch:</strong></td><td>${url_params.get('branch')}</td></tr>`)
document.querySelector('#ci-data').insertAdjacentHTML('afterbegin', `<tr><td><strong>Workflow:</strong></td><td>${url_params.get('workflow')}</td></tr>`)
document.querySelector('#ci-data').insertAdjacentHTML('afterbegin', `<tr><td><strong>Branch:</strong></td><td>${escapeString(url_params.get('branch'))}</td></tr>`)
document.querySelector('#ci-data').insertAdjacentHTML('afterbegin', `<tr><td><strong>Workflow:</strong></td><td>${escapeString(url_params.get('workflow'))}</td></tr>`)

displayCITable(badges_data.data, url_params);
chart_instance = displayGraph(badges_data.data)
displayAveragesTable(badges_data.data)
Expand Down
13 changes: 12 additions & 1 deletion frontend/js/helpers/config.js.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,15 @@ const machine_power_metric_condition = (metric) => {
const machine_energy_metric_condition = (metric) => {
if(metric.match(/^.*_energy_.*_machine$/) !== null) return true;
return false;
}
}

const sci_metric_condition = (metric) => {
if(metric == 'software_carbon_intensity_global') return true;
return false;
}

const embodied_carbon_share_metric_condition = (metric) => {
if(metric == 'embodied_carbon_share_machine') return true;
return false;
}

33 changes: 21 additions & 12 deletions frontend/js/helpers/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,27 @@ class GMTMenu extends HTMLElement {
customElements.define('gmt-menu', GMTMenu);

const replaceRepoIcon = (uri) => {
if (uri.startsWith("https://www.github.com") || uri.startsWith("https://github.com")) {
uri = uri.replace("https://www.github.com", '<i class="icon github"></i>');
uri = uri.replace("https://github.com", '<i class="icon github"></i>');
} else if (uri.startsWith("https://www.bitbucket.com") || uri.startsWith("https://bitbucket.com")) {
uri = uri.replace("https://www.bitbucket.com", '<i class="icon bitbucket"></i>');
uri = uri.replace("https://bitbucket.com", '<i class="icon bitbucket"></i>');
} else if (uri.startsWith("https://www.gitlab.com") || uri.startsWith("https://gitlab.com")) {
uri = uri.replace("https://www.gitlab.com", '<i class="icon gitlab"></i>');
uri = uri.replace("https://gitlab.com", '<i class="icon gitlab"></i>');
}
return uri;
}
const url = new URL(uri);
let iconClass = "";

switch (url.host) {
case "github.com":
case "www.github.com":
iconClass = "github";
break;
case "bitbucket.com":
case "www.bitbucket.com":
iconClass = "bitbucket";
break;
case "gitlab.com":
case "www.gitlab.com":
iconClass = "gitlab";
break;
default:
return uri;
}
return `<i class="icon ${iconClass}"></i>` + uri.substring(url.origin.length);
};

const showNotification = (message_title, message_text, type='warning') => {
$('body')
Expand Down
25 changes: 24 additions & 1 deletion frontend/js/helpers/metric-boxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,25 @@ class PhaseMetrics extends HTMLElement {
</div>
</div>
</div>
<div class="ui card software-carbon-intensity">
<div class="ui content">
<div class="ui top black attached label overflow-ellipsis">SCI</sub> <span class="si-unit"></span></div>
<div class="description">
<div class="ui fluid mini statistic">
<div class="value">
<i class="burn icon"></i> <span>N/A</span>
</div>
</div>
<div class="ui bottom right attached label icon" data-position="bottom right" data-inverted="" data-tooltip="SCI by the Green Software Foundation">
<u><a href="https://sci-guide.greensoftware.foundation/">via Formula</a></u>
<i class="question circle icon"></i>
</div>
<div class="ui bottom left attached label">
<span class="metric-type"></span>
</div>
</div>
</div>
</div>
</div><!-- end ui three cards stackable -->
<br>
<div class="ui accordion">
Expand Down Expand Up @@ -348,6 +367,10 @@ const updateKeyMetric = (phase, metric_name, clean_name, detail_name, value, std
selector = '.phase-duration';
} else if(network_co2_metric_condition(metric)) {
selector = '.network-co2';
} 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(machine_co2_metric_condition(metric)) {
Expand All @@ -360,7 +383,7 @@ 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
Expand Down
Loading

0 comments on commit 755333a

Please sign in to comment.