Skip to content

Commit 913bf91

Browse files
committed
Add ability for libraries actions to report changes in memory usage introduced by a PR
Changes in the flash and dynamic memory for global variables usage of an example sketch are determined during the pull request build by the compile-examples action. The action displays the memory usage data in the log and also saves it to a JSON formatted file. The actions/upload-artifact action may be used to create a workflow artifact containing the memory usage data files. The newly added arduino/actions/libraries/report-size-deltas action may be used to provide a table summarizing the memory usage changes as a comment in the pull request thread.
1 parent 574e392 commit 913bf91

File tree

3 files changed

+316
-6
lines changed

3 files changed

+316
-6
lines changed

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,40 @@ For 3rd party boards, also specify the Boards Manager URL:
2020

2121
List of library dependencies to install (space separated). Default `""`.
2222

23+
### `github-token`
24+
25+
GitHub access token used to get information from the GitHub API. Only needed if you're using the size report features with private repositories. It will be convenient to use [`${{ secrets.GITHUB_TOKEN }}`](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token). Default `""`.
26+
27+
### `size-report-sketch`
28+
29+
Name of the sketch used to compare memory usage change. Default `""`.
30+
31+
### `enable-size-deltas-report`
32+
33+
Set to `true` to cause the action to determine the change in memory usage for the [`size-reports-sketch`](#size-reports-sketch) between the pull request branch and the tip of the pull request's base branch. This may be used with the [`arduino/actions/libraries/report-size-deltas` action](https://github.com/arduino/actions/tree/master/libraries/report-size-deltas). Default `false`.
34+
35+
### `size-deltas-report-folder-name`
36+
37+
Folder to save the JSON formatted memory usage change reports to. Should be used only to store reports. It will be created under [`GITHUB_WORKSPACE`](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables). The folder will be created if it doesn't already exist. Default `"size-deltas-reports"`.
38+
2339
## Example usage
2440

41+
Only compiling examples:
42+
```yaml
43+
- uses: arduino/actions/libraries/compile-examples@master
44+
with:
45+
fqbn: 'arduino:avr:uno'
46+
```
47+
48+
Storing the memory usage change report as a [workflow artifact](https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts):
2549
```yaml
26-
uses: arduino/actions/libraries/compile-examples@master
27-
with:
28-
fqbn: 'arduino:avr:uno'
50+
- uses: arduino/actions/libraries/compile-examples@master
51+
with:
52+
size-report-sketch: Foobar
53+
enable-size-deltas-report: true
54+
- if: github.event_name == 'pull_request'
55+
uses: actions/upload-artifact@v1
56+
with:
57+
name: size-deltas-reports
58+
path: size-delta-reports
2959
```

action.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,26 @@ inputs:
1010
libraries:
1111
description: 'List of library dependencies to install (space separated)'
1212
default: ''
13+
github-token:
14+
description: 'GitHub access token used to get information from the GitHub API. Only needed if you are using the size report features with private repositories.'
15+
default: ''
16+
size-report-sketch:
17+
description: 'Name of the sketch used to compare memory usage change'
18+
default: ''
19+
enable-size-deltas-report:
20+
description: 'Set to true to cause the action to determine the change in memory usage for the size-reports-sketch'
21+
default: false
22+
size-deltas-report-folder-name:
23+
description: 'Folder to save the memory usage change report to'
24+
default: 'size-deltas-reports'
1325
runs:
1426
using: 'docker'
1527
image: 'Dockerfile'
1628
args:
1729
- ${{ inputs.cli-version }}
1830
- ${{ inputs.fqbn }}
1931
- ${{ inputs.libraries }}
32+
- ${{ inputs.github-token }}
33+
- ${{ inputs.size-report-sketch }}
34+
- ${{ inputs.enable-size-deltas-report }}
35+
- ${{ inputs.size-deltas-report-folder-name }}

entrypoint.sh

Lines changed: 267 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
readonly CLI_VERSION="$1"
44
readonly FQBN_ARG="$2"
55
readonly LIBRARIES="$3"
6+
readonly GH_TOKEN="$4"
7+
readonly SIZE_REPORT_SKETCH="$5"
8+
ENABLE_SIZE_DELTAS_REPORT="$6"
9+
readonly SIZE_DELTAS_REPORT_FOLDER_NAME="$7"
10+
11+
readonly SIZE_NOT_APPLICABLE_INDICATOR='"N/A"'
612

713
# Determine cli archive
814
readonly CLI_ARCHIVE="arduino-cli_${CLI_VERSION}_Linux_64bit.tar.gz"
@@ -16,6 +22,132 @@ readonly CORE="$(echo "$FQBN" | cut --delimiter=':' --fields=1,2)"
1622
# Additional Boards Manager URL
1723
readonly ADDITIONAL_URL="${FQBN_ARRAY[1]}"
1824

25+
# Get value from a key=value properties list file
26+
function get_property_value() {
27+
local -r filePath="$1"
28+
local -r key="$2"
29+
30+
local propertyValue
31+
propertyValue="$(grep --regex='^[[:blank:]]*'"$key"'[[:blank:]]*=' "$filePath" | cut --delimiter='=' --fields=2)"
32+
# Strip leading spaces
33+
propertyValue="${propertyValue#"${propertyValue%%[! ]*}"}"
34+
# Strip trailing spaces
35+
propertyValue="${propertyValue%"${propertyValue##*[! ]}"}"
36+
37+
echo "$propertyValue"
38+
}
39+
40+
function compile_example() {
41+
local -r examplePath="$1"
42+
arduino-cli compile --verbose --warnings all --fqbn "$FQBN" --output "${OUTPUT_FOLDER_PATH}/${OUTPUT_NAME}" "$examplePath" || {
43+
return $?
44+
}
45+
}
46+
47+
# Provide a more meaningful indicator in the report when a size could not be determined
48+
function check_sizes() {
49+
if [[ "$FLASH_SIZE" == "" ]]; then
50+
FLASH_SIZE="$SIZE_NOT_APPLICABLE_INDICATOR"
51+
fi
52+
if [[ "$RAM_SIZE" == "" ]]; then
53+
RAM_SIZE="$SIZE_NOT_APPLICABLE_INDICATOR"
54+
fi
55+
}
56+
57+
# Get the memory usage from the compilation output
58+
function compile_example_get_size_from_output() {
59+
local -r examplePath="$1"
60+
local compilationOutput
61+
62+
FLASH_SIZE=""
63+
RAM_SIZE=""
64+
65+
compilationOutput=$(compile_example "$EXAMPLE" 2>&1)
66+
local -r compileExampleExitStatus=$?
67+
# Display the compilation output
68+
echo "$compilationOutput"
69+
if [[ $compileExampleExitStatus -ne 0 ]]; then
70+
return $compileExampleExitStatus
71+
fi
72+
73+
while read -r outputLine; do
74+
# Determine program storage memory usage
75+
programStorageRegex="Sketch uses ([0-9,]+) *"
76+
if [[ "$outputLine" =~ $programStorageRegex ]]; then
77+
FLASH_SIZE="${BASH_REMATCH[1]}"
78+
# Remove commas
79+
FLASH_SIZE="${FLASH_SIZE//,/}"
80+
fi
81+
82+
# Determine dynamic memory usage
83+
dynamicMemoryRegex="Global variables use ([0-9,]+) *"
84+
if [[ "$outputLine" =~ $dynamicMemoryRegex ]]; then
85+
RAM_SIZE="${BASH_REMATCH[1]}"
86+
# Remove commas
87+
RAM_SIZE="${RAM_SIZE//,/}"
88+
fi
89+
done <<<"$compilationOutput"
90+
91+
# Some hardware cores aren't configured to output RAM usage by global variables, but the flash usage should at least be in the output
92+
if [[ "$FLASH_SIZE" == "" && "$RAM_SIZE" == "" ]]; then
93+
echo "::error::Something went wrong while while determining memory usage of the size-report-sketch"
94+
exit 1
95+
fi
96+
}
97+
98+
# Parse the compiler size command to determine memory usage
99+
function get_size_from_size_output() {
100+
local -r sizeOutput="$1"
101+
local -r sizeRegex="$2"
102+
103+
local -r sizeOutputLines="$(echo "$sizeOutput" | grep --perl-regexp --regex="$sizeRegex")"
104+
105+
local totalSize=0
106+
while read -r -a replyArray; do
107+
local replyValue="${replyArray[1]}"
108+
totalSize="$((totalSize + replyValue))"
109+
done <<<"$sizeOutputLines"
110+
111+
if [[ "$totalSize" == "" ]]; then
112+
echo "::error::Something went wrong while while determining memory usage of the size-report-sketch"
113+
exit 1
114+
fi
115+
116+
echo "$totalSize"
117+
}
118+
119+
# Use the compiler size command to determine memory usage
120+
function compile_example_get_size_from_size_cmd() {
121+
local -r examplePath="$1"
122+
123+
FLASH_SIZE=""
124+
RAM_SIZE=""
125+
126+
compile_example "$EXAMPLE" || {
127+
return $?
128+
}
129+
130+
local -r size_output="$("$COMPILER_SIZE_CMD_PATH" -A "${OUTPUT_FOLDER_PATH}/${OUTPUT_NAME}.elf")"
131+
FLASH_SIZE="$(get_size_from_size_output "$size_output" "$RECIPE_SIZE_REGEX")"
132+
RAM_SIZE="$(get_size_from_size_output "$size_output" "$RECIPE_SIZE_REGEX_DATA")"
133+
}
134+
135+
# If the enable-size-deltas-report argument is set to true, the size-report-sketch argument must also be defined
136+
if [[ "$ENABLE_SIZE_DELTAS_REPORT" == "true" && "$SIZE_REPORT_SKETCH" == "" ]]; then
137+
echo "::error::size-report-sketch argument was not defined"
138+
exit 1
139+
fi
140+
141+
# If the enable-size-deltas-report argument is set to true, the size-deltas-report-folder-path argument must also be defined
142+
if [[ "$ENABLE_SIZE_DELTAS_REPORT" == "true" && "$SIZE_DELTAS_REPORT_FOLDER_NAME" == "" ]]; then
143+
echo "::error::size-deltas-report-folder-path argument was not defined"
144+
exit 1
145+
fi
146+
147+
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
148+
ENABLE_SIZE_DELTAS_REPORT='false'
149+
fi
150+
19151
# Download the arduino-cli
20152
wget --no-verbose --directory-prefix="$HOME" "https://downloads.arduino.cc/arduino-cli/$CLI_ARCHIVE" || {
21153
exit 1
@@ -59,6 +191,46 @@ fi
59191
mkdir --parents "$HOME/Arduino/libraries"
60192
ln --symbolic "$PWD" "$HOME/Arduino/libraries/."
61193

194+
if [[ "$ENABLE_SIZE_DELTAS_REPORT" == "true" ]]; then
195+
# https://stedolan.github.io/jq/
196+
apt-get install --quiet=2 --assume-yes jq >/dev/null || {
197+
echo "::error::Failed to install jq"
198+
exit 1
199+
}
200+
fi
201+
202+
GET_SIZE_FROM_OUTPUT=true
203+
if [[ "$ENABLE_SIZE_DELTAS_REPORT" == "true" && ("$CORE" == "arduino:sam" || "$CORE" == "arduino:samd") ]]; then
204+
# arduino-cli doesn't report RAM usage for Arduino SAM Boards or Arduino SAMD Boards and doesn't include the data section in the flash usage report, so it's necessary to determine the sizes independently
205+
GET_SIZE_FROM_OUTPUT=false
206+
207+
DATA_DIRECTORY_PATH="$(arduino-cli config dump --format json | jq --raw-output '.directories.data')"
208+
DATA_DIRECTORY_PATH="${DATA_DIRECTORY_PATH//\"/}"
209+
210+
readonly VENDOR="$(echo "$FQBN" | cut --delimiter=':' --fields=1)"
211+
readonly ARCHITECTURE="$(echo "$FQBN" | cut --delimiter=':' --fields=2)"
212+
readonly PLATFORM_TXT_PATH="$(find "${DATA_DIRECTORY_PATH}/packages/${VENDOR}/hardware/${ARCHITECTURE}" -name platform.txt)"
213+
if [[ "$PLATFORM_TXT_PATH" == "" ]]; then
214+
echo "::error::Unable to find platform folder"
215+
exit 1
216+
fi
217+
218+
readonly COMPILER_SIZE_CMD="$(get_property_value "$PLATFORM_TXT_PATH" 'compiler\.size\.cmd')"
219+
readonly COMPILER_SIZE_CMD_PATH="$(find "$DATA_DIRECTORY_PATH/packages/$VENDOR/tools" -name "$COMPILER_SIZE_CMD")"
220+
if [[ "$COMPILER_SIZE_CMD_PATH" == "" ]]; then
221+
echo "::error::Unable to find compiler size tool"
222+
exit 1
223+
fi
224+
225+
readonly RECIPE_SIZE_REGEX='(?:\.text|\.data)\s+[0-9]'
226+
readonly RECIPE_SIZE_REGEX_DATA='(?:\.data|\.bss)\s+[0-9]'
227+
fi
228+
229+
# Create a folder for the compilation output files
230+
readonly OUTPUT_FOLDER_PATH="$(mktemp -d)"
231+
# The name of the output files. arduino-cli adds the file extensions.
232+
readonly OUTPUT_NAME='output'
233+
62234
# Find all the examples and loop build each
63235
readonly EXAMPLES="$(find "examples/" -name '*.ino' -print0 | xargs --null dirname | uniq)"
64236
if [[ "$EXAMPLES" == "" ]]; then
@@ -68,9 +240,101 @@ fi
68240
SCRIPT_EXIT_STATUS=0
69241
for EXAMPLE in $EXAMPLES; do
70242
echo "Building example $EXAMPLE"
71-
arduino-cli compile --verbose --warnings all --fqbn "$FQBN" "$EXAMPLE" || {
72-
SCRIPT_EXIT_STATUS="$?"
73-
}
243+
244+
if [[ "$ENABLE_SIZE_DELTAS_REPORT" != "true" || "${EXAMPLE##*/}" != "$SIZE_REPORT_SKETCH" ]]; then
245+
# Don't determine size
246+
compile_example "$EXAMPLE" || {
247+
SCRIPT_EXIT_STATUS="$?"
248+
}
249+
continue
250+
elif [[ "$ENABLE_SIZE_DELTAS_REPORT" == "true" && "${EXAMPLE##*/}" == "$SIZE_REPORT_SKETCH" ]]; then
251+
# Do determine size
252+
253+
# Determine memory usage of the sketch at the tip of the pull request branch
254+
if [[ "$GET_SIZE_FROM_OUTPUT" == "true" ]]; then
255+
compile_example_get_size_from_output "$EXAMPLE" || {
256+
SCRIPT_EXIT_STATUS="$?"
257+
continue
258+
}
259+
else
260+
compile_example_get_size_from_size_cmd "$EXAMPLE" || {
261+
SCRIPT_EXIT_STATUS="$?"
262+
continue
263+
}
264+
fi
265+
check_sizes
266+
267+
CURRENT_FLASH_SIZE="$FLASH_SIZE"
268+
CURRENT_RAM_SIZE="$RAM_SIZE"
269+
270+
# Determine memory usage of the sketch at the tip of the target repository's default branch
271+
apt-get install --quiet=2 --assume-yes git >/dev/null || {
272+
echo "::error::Failed to install git"
273+
exit 1
274+
}
275+
276+
# Save the commit hash for the tip of the pull request branch
277+
readonly CURRENT_COMMIT="$(git rev-parse HEAD)"
278+
279+
# checkout the tip of the pull request's base branch
280+
apt-get install --quiet=2 --assume-yes curl >/dev/null || {
281+
echo "::error::Failed to install curl"
282+
exit 1
283+
}
284+
285+
# Determine the pull request number, to use for the GitHub API request
286+
readonly PULL_REQUEST_NUMBER="$(jq --raw-output '.pull_request.number' "$GITHUB_EVENT_PATH")"
287+
if [[ "$GH_TOKEN" == "" ]]; then
288+
# Access token is not needed for public repositories
289+
readonly BASE_BRANCH_NAME="$(curl "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PULL_REQUEST_NUMBER}" | jq --raw-output .base.ref)"
290+
else
291+
readonly BASE_BRANCH_NAME="$(curl --header "Authorization: token ${GH_TOKEN}" "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PULL_REQUEST_NUMBER}" | jq --raw-output .base.ref)"
292+
fi
293+
if [[ "$BASE_BRANCH_NAME" == "null" ]]; then
294+
echo "::error::Unable to determine base branch name. Please specify the github-token argument in your workflow configuration."
295+
exit 1
296+
fi
297+
git checkout "$BASE_BRANCH_NAME" || {
298+
echo "::error::Failed to checkout base branch"
299+
exit 1
300+
}
301+
302+
# Compile the example sketch and get the sizes
303+
if [[ "$GET_SIZE_FROM_OUTPUT" == "true" ]]; then
304+
compile_example_get_size_from_output "$EXAMPLE"
305+
else
306+
compile_example_get_size_from_size_cmd "$EXAMPLE"
307+
fi
308+
check_sizes
309+
310+
if [[ "$CURRENT_FLASH_SIZE" == "$SIZE_NOT_APPLICABLE_INDICATOR" || "$FLASH_SIZE" == "$SIZE_NOT_APPLICABLE_INDICATOR" ]]; then
311+
FLASH_DELTA="$SIZE_NOT_APPLICABLE_INDICATOR"
312+
else
313+
FLASH_DELTA="$((CURRENT_FLASH_SIZE - FLASH_SIZE))"
314+
fi
315+
echo "Change in flash memory usage: $FLASH_DELTA"
316+
if [[ "$CURRENT_RAM_SIZE" == "$SIZE_NOT_APPLICABLE_INDICATOR" || "$RAM_SIZE" == "$SIZE_NOT_APPLICABLE_INDICATOR" ]]; then
317+
RAM_DELTA="$SIZE_NOT_APPLICABLE_INDICATOR"
318+
else
319+
RAM_DELTA="$((CURRENT_RAM_SIZE - RAM_SIZE))"
320+
fi
321+
echo "Change in RAM used by globals: $RAM_DELTA"
322+
323+
# Create the report folder
324+
readonly SIZE_REPORT_FOLDER_PATH="${GITHUB_WORKSPACE}/${SIZE_DELTAS_REPORT_FOLDER_NAME}"
325+
if ! [[ -d "$SIZE_REPORT_FOLDER_PATH" ]]; then
326+
mkdir --parents "$SIZE_REPORT_FOLDER_PATH"
327+
fi
328+
# Create the report file
329+
readonly SIZE_REPORT_FILE_PATH="${SIZE_REPORT_FOLDER_PATH}/${FQBN//:/-}.json"
330+
echo "{\"fqbn\": \"${FQBN}\", \"sketch\": \"${EXAMPLE}\", \"previous_flash\": ${FLASH_SIZE}, \"flash\": ${CURRENT_FLASH_SIZE}, \"flash_delta\": ${FLASH_DELTA}, \"previous_ram\": ${RAM_SIZE}, \"ram\": ${CURRENT_RAM_SIZE}, \"ram_delta\": ${RAM_DELTA}}" | jq . >"$SIZE_REPORT_FILE_PATH"
331+
332+
# Switch back to the previous commit in the repository
333+
git checkout "$CURRENT_COMMIT" || {
334+
echo "::error::Could not checkout the PR's head branch"
335+
exit 1
336+
}
337+
fi
74338
done
75339

76340
exit $SCRIPT_EXIT_STATUS

0 commit comments

Comments
 (0)