SQUAD : Adding a memory footprint gathering script launched from ci
This patch sets up the TFM memory footprint tracking using SQUAD and CI.
A python script is launched by the ci build-config pipeline if it has
been started by a nightly build. This python script sends bl2.axf and
tfm_s.axf sizes to a SQUAD interface.
Signed-off-by: Hugo L'Hostis <hugo.lhostis@arm.com>
Change-Id: Ied2df3f82b9f56589777829ec793c3260c75d374
diff --git a/jenkins/build-config.jpl b/jenkins/build-config.jpl
index 21e7ea0..665f939 100644
--- a/jenkins/build-config.jpl
+++ b/jenkins/build-config.jpl
@@ -1,6 +1,6 @@
#!/usr/bin/env groovy
//-------------------------------------------------------------------------------
-// Copyright (c) 2020, Arm Limited and Contributors. All rights reserved.
+// Copyright (c) 2020-2021, Arm Limited and Contributors. All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause
//
@@ -100,6 +100,16 @@
stage("Post") {
archiveArtifacts 'trusted-firmware-m/build/bin/**'
archiveArtifacts 'trusted-firmware-m/build/install/interface/**'
+ if (env.SQUAD_CONFIGURATIONS != ""){
+ //Creating a folder to store memory footprint artifacts and launching the memory footprint script.
+ sh "mkdir tf-m-ci-scripts/Memory_footprint/"
+ withCredentials([string(credentialsId: 'QA_REPORTS_TOKEN', variable: 'TOKEN')]) {
+ sh(script: "python3 tf-m-ci-scripts/memory_footprint.py ${env.WORKSPACE}/trusted-firmware-m/ ${env.CONFIG_NAME} \'${env.SQUAD_CONFIGURATIONS}\' ${TOKEN}", returnStdout: true)
+ }
+ if (fileExists('tf-m-ci-scripts/Memory_footprint/filesize.json')) {
+ archiveArtifacts 'tf-m-ci-scripts/Memory_footprint/filesize.json'
+ }
+ }
}
} catch (Exception e) {
manager.buildFailure()
diff --git a/jenkins/ci.jpl b/jenkins/ci.jpl
index 25e6eb6..41eb6c9 100644
--- a/jenkins/ci.jpl
+++ b/jenkins/ci.jpl
@@ -1,6 +1,6 @@
#!/usr/bin/env groovy
//-------------------------------------------------------------------------------
-// Copyright (c) 2020, Arm Limited and Contributors. All rights reserved.
+// Copyright (c) 2020-2021, Arm Limited and Contributors. All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause
//
@@ -158,6 +158,9 @@
params += string(name: 'CODE_REPO', value: env.CODE_REPO)
params += string(name: 'CODE_COVERAGE_EN', value: env.CODE_COVERAGE_EN)
params += string(name: 'CI_SCRIPTS_BRANCH', value: env.CI_SCRIPTS_BRANCH)
+ if (env.JOB_NAME.equals("tf-m-nightly")) { //Setting the Memory footprint gathering.
+ params += string(name: 'SQUAD_CONFIGURATIONS', value: env.SQUAD_CONFIGURATIONS)
+ }
return { -> results
def build_res = build(job: 'tf-m-build-config', parameters: params, propagate: false)
def build_info = [build_res, config, params_collection]
diff --git a/memory_footprint.py b/memory_footprint.py
new file mode 100644
index 0000000..dca011c
--- /dev/null
+++ b/memory_footprint.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python
+
+#memory_footprint.py : Script for sending memory footprint data from the TFM CI
+#to a SQUAD web interface
+#
+#Copyright (c) 2020-2021, Arm Limited. All rights reserved.
+#
+#SPDX-License-Identifier: BSD-3-Clause
+
+
+import argparse
+import os
+import re
+import sys
+import json
+import requests
+import subprocess
+from tfm_ci_pylib import utils
+
+# Arguments/parameters given by CI
+PATH_TO_TFM = sys.argv[1]
+CI_CONFIG = sys.argv[2]
+REFERENCE_CONFIGS = sys.argv[3].split(",")
+SQUAD_TOKEN = sys.argv[4]
+
+# local constant
+SQUAD_BASE_PROJECT_URL = ("https://qa-reports.linaro.org/api/submit/tf/tf-m/")
+
+# This function uses arm_non_eabi_size to get the sizes of a file
+# in the build directory of tfm
+def get_file_size(filename):
+ f_path = os.path.join(PATH_TO_TFM, "build", "bin", filename)
+ if os.path.exists(f_path) :
+ bl2_sizes = utils.arm_non_eabi_size(f_path)[0]
+ return bl2_sizes
+ else :
+ print(f_path + "Not found")
+ return -1
+
+# This function creates a json file containing all the data about
+# memory footprint and sends this data to SQUAD
+def send_file_size(change_id, config_name, bl2_sizes, tfms_sizes):
+ url = SQUAD_BASE_PROJECT_URL + change_id + '/' + config_name
+
+ try:
+ metrics = json.dumps({ "bl2_size" : bl2_sizes["dec"],
+ "bl2_data" : bl2_sizes["data"],
+ "bl2_bss" : bl2_sizes["bss"],
+ "tfms_size" : tfms_sizes["dec"],
+ "tfms_data" : tfms_sizes["data"],
+ "tfms_bss" : tfms_sizes["bss"]})
+ except:
+ return -1
+ headers = {"Auth-Token": SQUAD_TOKEN}
+ data= {"metrics": metrics}
+
+ try:
+ #Sending the data to SQUAD, 40s timeout
+ result = requests.post(url, headers=headers, data=data, timeout=40)
+ except:
+ return -1
+
+ with open(os.path.join(PATH_TO_TFM,
+ "..",
+ "tf-m-ci-scripts",
+ "Memory_footprint",
+ "filesize.json"), "w") as F:
+ #Storing the json file
+ F.write(metrics)
+
+ if not result.ok:
+ print(f"Error submitting to qa-reports: {result.reason}: {result.text}")
+ return -1
+ else :
+ print ("POST request sent to project " + config_name )
+ return 0
+
+#Function used to launch the configs.py script and get the printed output
+def get_config(config_string):
+ script_directory = os.path.join(PATH_TO_TFM, "..", "tf-m-ci-scripts")
+ directory = os.path.abspath(script_directory)
+ cur_dir = os.path.abspath(os.getcwd())
+ os.chdir(directory) # Going to the repo's directory
+
+ cmd = "python3 configs.py " + config_string
+
+ rex = re.compile(r'(?P<config>^[\s\S]*?)((?:TFM_PLATFORM=)'
+ r'(?P<platform>.*)\n)((?:TOOLCHAIN_FILE=)'
+ r'(?P<toolchain>.*)\n)((?:PSA_API=)'
+ r'(?P<psa>.*)\n)((?:ISOLATION_LEVEL=)'
+ r'(?P<isolation_level>.*)\n)((?:TEST_REGRESSION=)'
+ r'(?P<regression>.*)\n)((?:TEST_PSA_API=)'
+ r'(?P<psa_test>.*)\n)((?:CMAKE_BUILD_TYPE=)'
+ r'(?P<build_type>.*)\n)((?:OTP=)'
+ r'(?P<otp>.*)\n)((?:BL2=)'
+ r'(?P<bl2>.*)\n)((?:NS=)'
+ r'(?P<ns>.*)\n)((?:PROFILE=)'
+ r'(?P<profile>.*)\n)((?:PARTITION_PS=)'
+ r'(?P<partition_ps>.*)?)', re.MULTILINE)
+
+ r, e = subprocess.Popen(cmd,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE).communicate()
+ if e:
+ print("Error", e)
+ return -1
+ else:
+ try:
+ txt_body = r.decode('ascii')
+ except UnicodeDecodeError as E:
+ txt_body = r.decode('utf-8')
+
+ m = rex.search(txt_body)
+ match_dict = m.groupdict()
+
+ match_dict = {k: simple_filter(v) for k, v in match_dict.items()}
+
+ os.chdir(cur_dir)
+
+ return match_dict
+
+def simple_filter(A):
+ try:
+ a = A.lower()
+ if a in ["true", "on"]:
+ return True
+ elif a in ["false", "off"]:
+ return False
+ else:
+ return A
+ except Exception:
+ return A
+
+# This funcion manipulates the format of the config given
+# as entry to extract the name of the configuration used
+def identify_config(ci_config):
+ arguments = get_config(CI_CONFIG)
+ name_config = "Unknown"
+ try :
+ if (arguments["psa"] and arguments["isolation_level"] == "1" and
+ not arguments["regression"] and not arguments["psa_test"] and
+ arguments["build_type"] == "Release" and not arguments["otp"] and
+ arguments["bl2"] and arguments["ns"] and
+ arguments["profile"] == "N.A" and arguments["partition_ps"]):
+ name_config = "CoreIPC"
+ elif (not arguments["psa"] and arguments["isolation_level"] == "1" and
+ not arguments["regression"] and not arguments["psa_test"] and
+ arguments["build_type"] == "Release" and not arguments["otp"] and
+ arguments["bl2"] and arguments["ns"] and
+ arguments["profile"] == "N.A" and arguments["partition_ps"]):
+ name_config = "Default"
+ elif (arguments["psa"] and arguments["isolation_level"] == "2" and
+ not arguments["regression"] and not arguments["psa_test"] and
+ arguments["build_type"] == "Release" and not arguments["otp"] and
+ arguments["bl2"] and arguments["ns"] and
+ arguments["profile"] == "N.A" and arguments["partition_ps"]):
+ name_config = "CoreIPCTfmLevel2"
+ elif (not arguments["psa"] and arguments["isolation_level"] == "1" and
+ not arguments["regression"] and not arguments["psa_test"] and
+ arguments["build_type"] == "Release" and not arguments["otp"] and
+ arguments["bl2"] and arguments["ns"] and
+ arguments["profile"] == "SMALL" and arguments["partition_ps"]):
+ name_config = "DefaultProfileS"
+ elif (not arguments["psa"] and arguments["isolation_level"] == "2" and
+ not arguments["regression"] and not arguments["psa_test"] and
+ arguments["build_type"] == "Release" and not arguments["otp"] and
+ arguments["bl2"] and arguments["ns"] and
+ arguments["profile"] == "MEDIUM" and arguments["partition_ps"]):
+ name_config = "DefaultProfileM"
+ ret = [arguments["platform"],arguments["toolchain"], name_config]
+ except:
+ ret = ["Unknown", "Unknown", "Unknown"]
+ return ret
+
+# Function based on get_local_git_info() from utils, getting change id for the tfm repo
+def get_change_id(directory):
+ directory = os.path.abspath(directory)
+ cur_dir = os.path.abspath(os.getcwd())
+ os.chdir(directory) # Going to the repo's directory
+ cmd = "git log HEAD -n 1 --pretty=format:'%b'"
+
+ git_info_rex = re.compile(r'(?P<body>^[\s\S]*?)((?:Change-Id:\s)'
+ r'(?P<change_id>.*)\n?)', re.MULTILINE)
+
+ r, e = subprocess.Popen(cmd,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE).communicate()
+ if e:
+ print("Error", e)
+ return
+ else:
+ try:
+ txt_body = r.decode('ascii')
+ except UnicodeDecodeError as E:
+ txt_body = r.decode('utf-8')
+ result = txt_body.rstrip()
+
+ change_id = git_info_rex.search(result).groupdict()["change_id"].strip()
+ os.chdir(cur_dir) #Going back to the initial directory
+ return change_id
+
+if __name__ == "__main__":
+ for i in range(len(REFERENCE_CONFIGS)):
+ REFERENCE_CONFIGS[i] = REFERENCE_CONFIGS[i].strip().lower()
+ config = identify_config(CI_CONFIG)
+ if (config[2].lower() in REFERENCE_CONFIGS
+ and config[0] == "mps2/an521"
+ and config[1] == "toolchain_GNUARM.cmake"):
+ # Pushing data for AN521 and GNUARM
+ print("Configuration " + config[2] + " is a reference")
+ try :
+ change_id = get_change_id(PATH_TO_TFM)
+ except :
+ change_id = -1
+ bl2_sizes = get_file_size("bl2.axf")
+ tfms_sizes = get_file_size("tfm_s.axf")
+ if (bl2_sizes != -1 and change_id != -1) :
+ send_file_size(change_id, config[2], bl2_sizes, tfms_sizes)
+ else :
+ #Directory or file weren't found
+ if change_id == -1 :
+ print("Error : trusted-firmware-m repo not found")
+ else :
+ print("Error : file not found")