diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 674aef8a..21249cf0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,24 +20,27 @@ permissions: jobs: test-action: - runs-on: ubuntu-latest + runs-on: ${matrix.os} + continue-on-error: false + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, ubuntu-24.04, ubuntu-20.04, ubuntu-18.04, ubuntu-20.04-16core] + # It might be the case steps: - uses: actions/checkout@v3 with: ref: ${{ github.ref }} - - name: actions/checkout@v3 + - name: API Base Debug run: | echo "Current API Base is" ${{ github.api_url }} - - name: Initialize Energy Estimation uses: ./ with: task: start-measurement - - - name: Sleep step run: sleep 2 @@ -48,7 +51,8 @@ jobs: label: "Sleep 3s" - name: Filesystem - run: ls -alhR /usr/lib + run: timeout 10s ls -alhR /usr/lib + continue-on-error: true - name: Test measurement 2 uses: ./ diff --git a/.gitlab-ci.yml.example b/.gitlab-ci.yml.example index 14ca74f2..2774a503 100644 --- a/.gitlab-ci.yml.example +++ b/.gitlab-ci.yml.example @@ -14,6 +14,10 @@ test-job: #- export ECO_CI_PROJECT_UUID="YOUR PROJECT UUID" #- export ECO_CI_MACHINE_UUID="YOUR MACHINE UUID" + # Change this to you machine, if you are not using the default. + # https://docs.gitlab.com/ee/ci/runners/hosted_runners/linux.html + #- export MACHINE_POWER_DATA="gitlab_EPYC_7B12_saas-linux-small-amd64.txt" + - !reference [.initialize_energy_estimator, script] - !reference [.start_measurement, script] - sleep 10s diff --git a/README.md b/README.md index ca6d3ffa..b26fd786 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,7 @@ test-job: + The plugin is tested on: + `ubuntu-latest` (22.04 at the time of writing) + `ubuntu-24.04` + + [Autoscaling Github Runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners/managing-larger-runners#configuring-autoscaling-for-larger-runners) are not supported + It is known to not work on `ubuntu-20.04` ([See here](https://github.com/green-coding-solutions/eco-ci-energy-estimation/issues/72)) + Also Windows and macOS are currently not supported. diff --git a/action.yml b/action.yml index d37ab27f..6967637f 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,10 @@ inputs: description: 'Label for the get-measurement task, to mark what this measurement correlates to in your workflow' default: null required: false + machine-power-data: + description: 'The file to read the machine power data from. Default will be 4 core AMD EPYC 7763 Github Runner' + default: "github_EPYC_7763_4_CPU_shared.sh" + required: false send-data: description: 'Send metrics data to metrics.green-coding.io to create and display badge, and see an overview of the energy of your CI runs. Set to false to send no data.' default: true @@ -35,7 +39,7 @@ inputs: pr-comment: description: 'Add a comment to the PR with the results during display-results step' default: false - api-base: + gh-api-base: description: 'Base URL of the Github API to send data to. Default is api.github.com, but can be changed to your hostname if you have Github Enterprise' default: ${{ github.api_url }} required: false @@ -75,7 +79,7 @@ runs: name: Setup shell: bash run: | - # call the initialize function of setup.sh + ${{github.action_path}}/scripts/vars.sh add_var "MACHINE_POWER_DATA" "${{inputs.machine-power-data}}" ${{github.action_path}}/scripts/setup.sh initialize - if: inputs.task == 'start-measurement' @@ -85,7 +89,7 @@ runs: # we prefer this over manual startint / stopping as it is less error prone for users run: | if ${{inputs.send-data}}; then - curl_response=$(curl -s -H "Authorization: Bearer ${{github.token}}" ${{ inputs.api-base }}/repos/${{ github.repository }}/actions/workflows) + curl_response=$(curl -s -H "Authorization: Bearer ${{github.token}}" ${{ inputs.gh-api-base }}/repos/${{ github.repository }}/actions/workflows) workflow_id=$(echo $curl_response | jq '.workflows[] | select(.name == "${{ github.workflow }}") | .id') ${{github.action_path}}/scripts/vars.sh add_var "WORKFLOW_ID" $workflow_id else @@ -124,7 +128,7 @@ runs: env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | - COMMENTS=$(curl -s -H "Authorization: Bearer ${{github.token}}" "${{ inputs.api-base }}/repos/${{ github.repository }}/issues/$PR_NUMBER/comments") + COMMENTS=$(curl -s -H "Authorization: Bearer ${{github.token}}" "${{ inputs.gh-api-base }}/repos/${{ github.repository }}/issues/$PR_NUMBER/comments") echo "$COMMENTS" | jq -c --arg username "github-actions[bot]" '.[] | select(.user.login == $username and (.body | index("Eco-CI") // false))' | while read -r comment; do COMMENT_ID=$(echo "$comment" | jq -r '.id') @@ -137,12 +141,12 @@ runs: $INNER_BODY " '{"body": $body}') - curl -s -H "Authorization: Bearer ${{github.token}}" -X PATCH -d "$PAYLOAD" "${{ inputs.api-base }}/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" + curl -s -H "Authorization: Bearer ${{github.token}}" -X PATCH -d "$PAYLOAD" "${{ inputs.gh-api-base }}/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" echo "Comment $COMMENT_ID collapsed." done NEW_COMMENT=$(cat "/tmp/eco-ci/output-pr.txt" | jq -Rs '.') - API_URL="${{ inputs.api-base }}/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" + API_URL="${{ inputs.gh-api-base }}/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" curl -X POST -H "Authorization: Bearer ${{github.token}}" -d @- $API_URL <> /tmp/eco-ci/energy-total.txt - done < /tmp/eco-ci/cpu-util-total.txt - max_measurement_number=1 + if [[ $(wc -l < /tmp/eco-ci/energy-total.txt) -gt 0 ]]; then + echo "Could not display table as no measurement data was present!" + echo "Could not display table as no measurement data was present!" >> $GITHUB_STEP_SUMMARY + return 1 fi cpu_avg=$(awk '{ total += $2; count++ } END { print total/count }' /tmp/eco-ci/cpu-util-total.txt) @@ -95,8 +95,8 @@ function display_results { branch_enc=$( echo ${branch} | jq -Rr @uri) if [[ ${show_carbon} == 'true' ]]; then - source "$(dirname "$0")/vars.sh" get_energy_co2 "$total_energy" - source "$(dirname "$0")/vars.sh" get_embodied_co2 "$total_time" + source "$(dirname "$0")/misc.sh" get_energy_co2 "$total_energy" + source "$(dirname "$0")/misc.sh" get_embodied_co2 "$total_time" if [ -n "$CO2EQ_EMBODIED" ] && [ -n "$CO2EQ_ENERGY" ]; then # We only check for co2 as if this is set the others should be set too @@ -117,7 +117,7 @@ function display_results { if [[ ${send_data} == 'true' && ${display_badge} == 'true' ]]; then - get_endpoint=$API_BASE"/v1/ci/measurement/get" + get_endpoint=$DASHBOARD_API_BASE"/v1/ci/measurement/get" metrics_url="https://metrics.green-coding.io" echo "Badge for your README.md:" >> $output diff --git a/scripts/make_measurement.sh b/scripts/make_measurement.sh index 750fc352..2704963b 100755 --- a/scripts/make_measurement.sh +++ b/scripts/make_measurement.sh @@ -10,7 +10,9 @@ function make_measurement() { MODEL_NAME=${MODEL_NAME:-} MEASUREMENT_COUNT=${MEASUREMENT_COUNT:-} WORKFLOW_ID=${WORKFLOW_ID:-} - API_BASE=${API_BASE:-} + DASHBOARD_API_BASE=${DASHBOARD_API_BASE:-} + MACHINE_POWER_HASHMAP=${MACHINE_POWER_HASHMAP:-} + MACHINE_POWER_DATA=${MACHINE_POWER_DATA:-} @@ -30,10 +32,18 @@ function make_measurement() { # check wc -l of cpu-util is greater than 0 if [[ $(wc -l < /tmp/eco-ci/cpu-util-temp.txt) -gt 0 ]]; then - while read -r time util; do - echo "$time * $util" | bc -l >> /tmp/eco-ci/energy-step.txt - done < /tmp/eco-ci/cpu-util-temp.txt - + if [[ $MACHINE_POWER_HASHMAP == "" ]]; then + echo "Using bash mode inference" + while read -r time util; do + echo "$time * ${MACHINE_POWER_HASHMAP[$util]}" | bc -l >> /tmp/eco-ci/energy-step.txt + done < /tmp/eco-ci/cpu-util-temp.txt + else + echo "Using legacy mode inference" + while read -r time util; do + power_value=$(awk -F "=" -v pattern="[$util]" '{ if ($1 == pattern) print $2 }' $MACHINE_POWER_DATA) + echo "$time * ${power_value}" | bc -l >> /tmp/eco-ci/energy-step.txt + done < /tmp/eco-ci/cpu-util-temp.txt + fi if [[ $MEASUREMENT_COUNT == '' ]]; then MEASUREMENT_COUNT=1 @@ -56,7 +66,6 @@ function make_measurement() { source "$(dirname "$0")/vars.sh" add_var $key_to_add "$value_to_add" echo $total_energy >> /tmp/eco-ci/energy-values.txt - source "$(dirname "$0")/vars.sh" add_var MEASUREMENT_RAN true if [[ $send_data == 'true' ]]; then @@ -65,7 +74,7 @@ function make_measurement() { CO2EQ=$(echo "$CO2EQ_EMBODIED + $CO2EQ_ENERGY" | bc -l) - add_endpoint=$API_BASE"/v1/ci/measurement/add" + add_endpoint=$DASHBOARD_API_BASE"/v1/ci/measurement/add" value_mJ=$(echo "$total_energy*1000" | bc -l | cut -d '.' -f 1) unit="mJ" model_name_uri=$(echo $MODEL_NAME | jq -Rr @uri) diff --git a/scripts/misc.sh b/scripts/misc.sh new file mode 100755 index 00000000..6495d573 --- /dev/null +++ b/scripts/misc.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env sh +set -euo pipefail + +get_geo_ipapi_co() { + response=$(curl -s https://ipapi.co/json || true) + + if [[ -z "$response" ]] || ! echo "$response" | jq empty; then + echo "Failed to retrieve data or received invalid JSON. Exiting" >&2 + return + fi + + if echo "$response" | jq '.latitude, .longitude, .city' | grep -q null; then + echo "Required data is missing. Exiting" >&2 + return + fi + + echo "$response" +} + +get_carbon_intensity() { + latitude=$1 + longitude=$2 + + if [ -z "${ELECTRICITY_MAPS_TOKEN+x}" ]; then + export ELECTRICITY_MAPS_TOKEN='no_token' + fi + + response=$(curl -s -H "auth-token: $ELECTRICITY_MAPS_TOKEN" "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=$latitude&lon=$longitude" || true) + + if [[ -z "$response" ]] || ! echo "$response" | jq empty; then + echo "Failed to retrieve data or received invalid JSON. Exiting" >&2 + return + fi + + if echo "$response" | jq '.carbonIntensity' | grep -q null; then + echo "Required carbonIntensity is missing. Exiting" >&2 + return + fi + + echo "$response" | jq '.carbonIntensity' +} + +get_embodied_co2_val (){ + time=$1 + + if [ -n "$SCI_M" ]; then + co2_value=$(echo "$SCI_M * ($time/$SCI_USAGE_DURATION)" | bc -l) + export CO2EQ_EMBODIED="$co2_value" + else + echo "SCI_M was not set" >&2 + fi + +} + +get_energy_co2_val (){ + total_energy=$1 + + geo_data=$(get_geo_ipapi_co) || true + if [ -n "$geo_data" ]; then + latitude=$(echo "$geo_data" | jq '.latitude') + longitude=$(echo "$geo_data" | jq '.longitude') + city=$(echo "$geo_data" | jq -r '.city') + + export CITY="$city" + export LAT="$latitude" + export LON="$longitude" + + carbon_intensity=$(get_carbon_intensity $latitude $longitude) || true + + if [[ -n "$carbon_intensity" ]]; then + export CO2I="$carbon_intensity" + + value_mJ=$(echo "$total_energy*1000" | bc -l | cut -d '.' -f 1) + value_kWh=$(echo "$value_mJ * 10^-9" | bc -l) + co2_value=$(echo "$value_kWh * $carbon_intensity" | bc -l) + + export CO2EQ_ENERGY="$co2_value" + + else + echo "Failed to get carbon intensity data." >&2 + fi + else + echo "Failed to get geolocation data." >&2 + fi +} + +# Main script logic +if [ $# -eq 0 ]; then + echo "No option provided. Please specify an option to misc.sh" + exit 1 +fi + +option="$1" +case $option in + get_energy_co2) + get_energy_co2_val $2 + ;; + get_embodied_co2) + get_embodied_co2_val $2 + ;; + *) + echo "Invalid option ($option). Please specify a valid option to misc.sh" + exit 1 + ;; +esac \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh index 65617fbf..4ab373a8 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -14,7 +14,12 @@ function initialize { fi # call init_variables source "$(dirname "$0")/vars.sh" cpu_vars - source "$(dirname "$0")/vars.sh" add_var API_BASE "https://api.green-coding.io" + source "$(dirname "$0")/vars.sh" add_var DASHBOARD_API_BASE "https://api.green-coding.io" + + if [[ -n "$BASH_VERSION" ]] && (( ${BASH_VERSION:0:1} >= 4 )); then + source "$(dirname "$0")/machine-power-data/${MACHINE_POWER_DATA}" + source "$(dirname "$0")/vars.sh" add_var MACHINE_POWER_HASHMAP $cloud_energy_hashmap + fi } diff --git a/scripts/vars.sh b/scripts/vars.sh index d9799ffd..fa4130de 100755 --- a/scripts/vars.sh +++ b/scripts/vars.sh @@ -1,8 +1,6 @@ #!/usr/bin/env sh set -euo pipefail -model_name=$(cat /proc/cpuinfo | grep "model name") - add_var() { key=$1 value=$2 @@ -49,10 +47,14 @@ read_vars() { function cpu_vars_fill { + model_name=$(cat /proc/cpuinfo | grep "model name") + # Current GitHub default (Q1/2024) # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources if [[ "$model_name" == *"AMD EPYC 7763"* ]]; then + echo "Found EPYC 7763 model" >> $GITHUB_STEP_SUMMARY echo "Found EPYC 7763 model"; + add_var "MODEL_NAME" "EPYC_7763"; add_var "TDP" 280; @@ -71,7 +73,8 @@ function cpu_vars_fill { # we use 4 years - 1*60*60*24*365*4 = add_var "SCI_USAGE_DURATION" 126144000 - # gitlab uses this one (Q1/2024) + # gitlab uses this one https://docs.gitlab.com/ee/ci/runners/hosted_runners/linux.html (Q1/2024) + # https://www.green-coding.io/case-studies/cpu-utilization-usefulness/ elif [[ "$model_name" == *"AMD EPYC 7B12"* ]]; then echo "Found EPYC 7B12 model" add_var "MODEL_NAME" "EPYC_7B12"; @@ -95,7 +98,7 @@ function cpu_vars_fill { else - echo "⚠️ Unknown model $model_name for estimation, will use auto detect ..." # >> $GITHUB_STEP_SUMMARY + echo "⚠️ Unknown model $model_name for estimation, will use auto detect ..." >> $GITHUB_STEP_SUMMARY # we use a default configuration here from https://datavizta.boavizta.org/serversimpact add_var "SCI_M" 800.3; # we use 4 years - 1*60*60*24*365*4 = @@ -105,88 +108,6 @@ function cpu_vars_fill { } -get_geo_ipapi_co() { - response=$(curl -s https://ipapi.co/json || true) - - if [[ -z "$response" ]] || ! echo "$response" | jq empty; then - echo "Failed to retrieve data or received invalid JSON. Exiting" >&2 - return - fi - - if echo "$response" | jq '.latitude, .longitude, .city' | grep -q null; then - echo "Required data is missing. Exiting" >&2 - return - fi - - echo "$response" -} - -get_carbon_intensity() { - latitude=$1 - longitude=$2 - - if [ -z "${ELECTRICITY_MAPS_TOKEN+x}" ]; then - export ELECTRICITY_MAPS_TOKEN='no_token' - fi - - response=$(curl -s -H "auth-token: $ELECTRICITY_MAPS_TOKEN" "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=$latitude&lon=$longitude" || true) - - if [[ -z "$response" ]] || ! echo "$response" | jq empty; then - echo "Failed to retrieve data or received invalid JSON. Exiting" >&2 - return - fi - - if echo "$response" | jq '.carbonIntensity' | grep -q null; then - echo "Required carbonIntensity is missing. Exiting" >&2 - return - fi - - echo "$response" | jq '.carbonIntensity' -} - -get_embodied_co2_val (){ - time=$1 - - if [ -n "$SCI_M" ]; then - co2_value=$(echo "$SCI_M * ($time/$SCI_USAGE_DURATION)" | bc -l) - export CO2EQ_EMBODIED="$co2_value" - else - echo "SCI_M was not set" >&2 - fi - -} - -get_energy_co2_val (){ - total_energy=$1 - - geo_data=$(get_geo_ipapi_co) || true - if [ -n "$geo_data" ]; then - latitude=$(echo "$geo_data" | jq '.latitude') - longitude=$(echo "$geo_data" | jq '.longitude') - city=$(echo "$geo_data" | jq -r '.city') - - export CITY="$city" - export LAT="$latitude" - export LON="$longitude" - - carbon_intensity=$(get_carbon_intensity $latitude $longitude) || true - - if [[ -n "$carbon_intensity" ]]; then - export CO2I="$carbon_intensity" - - value_mJ=$(echo "$total_energy*1000" | bc -l | cut -d '.' -f 1) - value_kWh=$(echo "$value_mJ * 10^-9" | bc -l) - co2_value=$(echo "$value_kWh * $carbon_intensity" | bc -l) - - export CO2EQ_ENERGY="$co2_value" - - else - echo "Failed to get carbon intensity data." >&2 - fi - else - echo "Failed to get geolocation data." >&2 - fi -} # Main script logic if [ $# -eq 0 ]; then @@ -205,14 +126,8 @@ case $option in read_vars) read_vars ;; - get_energy_co2) - get_energy_co2_val $2 - ;; - get_embodied_co2) - get_embodied_co2_val $2 - ;; *) - echo "Invalid option ($option). Please specify an option: cpu_vars, or add_var [key] [value]." + echo "Invalid option ($option). Please specify an option: cpu_vars, read_vars or add_var [key] [value]." exit 1 ;; esac \ No newline at end of file