Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added average function to badges #1029

Merged
merged 10 commits into from
Dec 22, 2024
93 changes: 85 additions & 8 deletions api/eco_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,64 @@ async def get_ci_measurements(repo: str, branch: str, workflow: str, start_date:

return ORJSONResponse({'success': True, 'data': data})

@router.get('/v1/ci/stats')
async def get_ci_stats(repo: str, branch: str, workflow: str, start_date: date, end_date: date):


query = '''
WITH my_table as (
SELECT
SUM(energy_uj) as a,
SUM(duration_us) as b,
SUM(cpu_util_avg * duration_us) / NULLIF(SUM(duration_us), 0) as c, -- weighted average
SUM(carbon_intensity_g * duration_us) / NULLIF(SUM(duration_us), 0) as d,-- weighted average
SUM(carbon_ug) as e
FROM ci_measurements
WHERE
repo = %s AND branch = %s AND workflow_id = %s
AND DATE(created_at) >= TO_DATE(%s, 'YYYY-MM-DD') AND DATE(created_at) <= TO_DATE(%s, 'YYYY-MM-DD')
GROUP BY run_id
) SELECT
-- Cast is to avoid DECIMAL which ORJJSON cannot handle
AVG(a)::float, SUM(a)::float, STDDEV(a)::float, (STDDEV(a) / NULLIF(AVG(a), 0))::float * 100,
AVG(b)::float, SUM(b)::float, STDDEV(b)::float, (STDDEV(b) / NULLIF(AVG(b), 0))::float * 100,
AVG(c)::float, NULL, STDDEV(c)::float, (STDDEV(c) / NULLIF(AVG(c), 0))::float * 100, -- SUM of cpu_util_avg makes no sense
AVG(d)::float, NULL, STDDEV(d)::float, (STDDEV(d) / NULLIF(AVG(d), 0))::float * 100, -- SUM of carbon_intensity_g makes no sense
AVG(e)::float, SUM(e)::float, STDDEV(e)::float, (STDDEV(e) / NULLIF(AVG(e), 0))::float * 100,
COUNT(*)
FROM my_table;
'''
params = (repo, branch, workflow, str(start_date), str(end_date))
totals_data = DB().fetch_one(query, params=params)

if totals_data is None or totals_data[0] is None: # aggregate query always returns row
return Response(status_code=204) # No-Content

query = '''
SELECT
-- Cast is to avoid DECIMAL which ORJJSON cannot handle
-- Here we do not need a weighted average, even if the times differ, because we specifically want to look per step and duration is not relevant
AVG(energy_uj)::float, SUM(energy_uj)::float, STDDEV(energy_uj)::float, (STDDEV(energy_uj) / NULLIF(AVG(energy_uj), 0))::float * 100,
AVG(duration_us)::float, SUM(duration_us)::float, STDDEV(duration_us)::float, (STDDEV(duration_us) / NULLIF(AVG(duration_us), 0))::float * 100,
AVG(cpu_util_avg)::float, NULL, STDDEV(cpu_util_avg)::float, (STDDEV(cpu_util_avg) / NULLIF(AVG(cpu_util_avg), 0))::float * 100, -- SUM of cpu_util_avg makes no sense
AVG(carbon_intensity_g)::float, NULL, STDDEV(carbon_intensity_g)::float, (STDDEV(carbon_intensity_g) / NULLIF(AVG(carbon_intensity_g), 0))::float * 100, -- SUM of carbon_intensity_g makes no sense
AVG(carbon_ug)::float, SUM(carbon_ug)::float, STDDEV(carbon_ug)::float, (STDDEV(carbon_ug) / NULLIF(AVG(carbon_ug), 0))::float * 100,
COUNT(*), label
FROM ci_measurements
WHERE
repo = %s AND branch = %s AND workflow_id = %s
AND DATE(created_at) >= TO_DATE(%s, 'YYYY-MM-DD') AND DATE(created_at) <= TO_DATE(%s, 'YYYY-MM-DD')
GROUP BY label
'''
params = (repo, branch, workflow, str(start_date), str(end_date))
per_label_data = DB().fetch_all(query, params=params)

if per_label_data is None or per_label_data[0] is None:
return Response(status_code=204) # No-Content

return ORJSONResponse({'success': True, 'data': {'totals': totals_data, 'per_label': per_label_data}})


@router.get('/v1/ci/repositories')
async def get_ci_repositories(repo: str | None = None, sort_by: str = 'name'):

Expand Down Expand Up @@ -243,17 +301,21 @@ async def get_ci_badge_get(repo: str, branch: str, workflow:str, mode: str = 'la
if metric == 'energy':
metric = 'energy_uj'
metric_unit = 'uJ'
label = 'Energy used'
label = 'energy used'
default_color = 'orange'
elif metric == 'carbon':
metric = 'carbon_ug'
metric_unit = 'ug'
label = 'Carbon emitted'
label = 'carbon emitted'
default_color = 'black'
# Do not easily add values like cpu_util or carbon_intensity_g here. They need a weighted average in the SQL query later!
else:
raise RequestValidationError('Unsupported metric requested')


if duration_days and (duration_days < 1 or duration_days > 365):
raise RequestValidationError('Duration days must be between 1 and 365 days')
ArneTR marked this conversation as resolved.
Show resolved Hide resolved

params = [repo, branch, workflow]


Expand All @@ -263,15 +325,30 @@ async def get_ci_badge_get(repo: str, branch: str, workflow:str, mode: str = 'la
WHERE repo = %s AND branch = %s AND workflow_id = %s
"""

if mode == 'last':
query = f"""{query}
GROUP BY run_id
ORDER BY MAX(created_at) DESC
LIMIT 1
if mode == 'avg':
if not duration_days:
raise RequestValidationError('Duration days must be set for average')
query = f"""
WITH my_table as (
SELECT SUM({metric}) my_sum
FROM ci_measurements
WHERE repo = %s AND branch = %s AND workflow_id = %s AND DATE(created_at) > NOW() - make_interval(days => %s)
GROUP BY run_id
) SELECT AVG(my_sum) FROM my_table;
"""
params.append(duration_days)
label = f"Per run moving average ({duration_days} days) {label}"
elif mode == 'last':
query = f"{query} GROUP BY run_id ORDER BY MAX(created_at) DESC LIMIT 1"
label = f"Last run {label}"
elif mode == 'totals' and duration_days:
query = f"{query} AND created_at > NOW() - make_interval(days => %s)"
query = f"{query} AND DATE(created_at) > NOW() - make_interval(days => %s)"
params.append(duration_days)
label = f"Last {duration_days} days total {label}"
elif mode == 'totals':
label = f"All runs total {label}"
else:
raise RuntimeError('Unknown mode')


data = DB().fetch_one(query, params=params)
Expand Down
28 changes: 27 additions & 1 deletion frontend/ci.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,33 @@ <h3>Pipeline stats</h3>
</table>
</div>
</div>
<div class="ui segment" id="runs-table">

<div id="loader-question" class="ui icon info message blue">
<i class="info circle icon"></i>

<div class="content">
<div class="header">
Runs detail table is not displayed automatically
</div>
<p>Please click the button below to fetch data.</p>
<p>You can change the default behaviour under <a href="/settings.html" style="text-decoration: underline; font-weight: bold;">Settings</a></p>
<button id="display-run-details-table" class="blue ui button">Display runs table</button>
</div>
<ul></ul>
ArneTR marked this conversation as resolved.
Show resolved Hide resolved
</div>

<div class="ui one cards" id="api-loader" style="display:none;">
<div class="card" style="min-height: 300px">
<div class="ui active dimmer">
<div class="ui indeterminate text loader">Building table ...</div>
</div>
<p></p>
</div>
</div>
<div id="chart-container"></div>


<div class="ui segment" id="run-details-table" style="display: none">
<div class="header"><a class="ui teal ribbon label">
<h3 data-tooltip="The runs table shows all measurements your pipeline has made in the selected timeframe" data-position="top left">Runs Table <i class="question circle icon "></i> </h3>
</a></div>
Expand Down
4 changes: 4 additions & 0 deletions frontend/css/green-coding.css
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ a {
transition: opacity .1s ease;
}

.bold {
font-weight: bold;
}


/* PRINT STYLESHEETS */
@media print {
Expand Down
Loading
Loading