From 55fb73f5b39912243b4b6335ebe5c680ceec0dd2 Mon Sep 17 00:00:00 2001 From: Samuli Leivo Date: Fri, 8 Nov 2024 08:46:59 +0200 Subject: [PATCH] Add keywords and tools for power measurement Prevent power measurement errors failing any tests. Monitor also drops in measurement frequency: A warning of unusually low measurement frequency will be shown in the power plot in case of drop in the measurement frequency. This can help in detecting any problems in the measurement hardware. Signed-off-by: Samuli Leivo --- Robot-Framework/config/variables.robot | 6 + Robot-Framework/lib/parse_power_data.py | 70 ++++++++++ .../resources/power_meas_keywords.resource | 130 ++++++++++++++++++ flake.lock | 2 +- 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 Robot-Framework/lib/parse_power_data.py create mode 100644 Robot-Framework/resources/power_meas_keywords.resource diff --git a/Robot-Framework/config/variables.robot b/Robot-Framework/config/variables.robot index 6c006bf5..bff9c3ea 100644 --- a/Robot-Framework/config/variables.robot +++ b/Robot-Framework/config/variables.robot @@ -62,6 +62,12 @@ Set Variables ${result} Run Process sh -c cat /run/secrets/wifi-password shell=true Set Global Variable ${TEST_WIFI_PSWD} ${result.stdout} + Run Keyword And Ignore Error Set Global Variable ${RPI_IP_ADDRESS} ${config['addresses']['measurement_agent']['device_ip_address']} + ${result} Run Process sh -c cat /run/secrets/pi-login shell=true + Set Global Variable ${LOGIN_PI} ${result.stdout} + ${result} Run Process sh -c cat /run/secrets/pi-pass shell=true + Set Global Variable ${PASSWORD_PI} ${result.stdout} + Set Log Level INFO IF $BUILD_ID != '${EMPTY}' diff --git a/Robot-Framework/lib/parse_power_data.py b/Robot-Framework/lib/parse_power_data.py new file mode 100644 index 00000000..0baa6962 --- /dev/null +++ b/Robot-Framework/lib/parse_power_data.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: 2022-2024 Technology Innovation Institute (TII) +# SPDX-License-Identifier: Apache-2.0 + +import pandas as pd +import logging +import matplotlib.pyplot as plt +import csv + + +def extract_time_interval(csv_file, start_time, end_time): + columns = ['time', 'meas_counter', 'power'] + data = pd.read_csv(csv_file, names=columns) + interval = data.query("{} < time < {}".format(start_time, end_time)) + interval.to_csv('power_interval.csv', index=False) + + # Check if measurement frequency was within normal limits + # Reset the flag file + with open("low_frequency_flag", 'w'): + pass + normal_meas_frequency = 312 + tolerance = 50 + low_frequency = data[data['meas_counter'] < normal_meas_frequency - tolerance] + if not low_frequency.empty: + logging.info("Low measurement frequency detected:") + logging.info(low_frequency) + with open("low_frequency_timestamps.csv", 'a', newline='') as csvfile: + csvwriter = csv.writer(csvfile) + for index, row in low_frequency.iterrows(): + csvwriter.writerow(row) + with open("low_frequency_flag", 'w', newline='') as flag: + flag.write("Warning: unusually low measurement frequency detected") + return False + return True + +def generate_graph(csv_file, test_name): + data = pd.read_csv(csv_file) + start_time = data['time'].values[0] + end_time = data['time'].values[data.index.max()] + plt.figure(figsize=(20, 10)) + plt.set_loglevel('WARNING') + + # Show only hh-mm-ss part of the time at x-axis ticks + data['time'] = data['time'].str[11:19] + + plt.ticklabel_format(axis='y', style='plain') + plt.plot(data['time'], data['power'], marker='o', linestyle='-', color='b') + plt.yticks(fontsize=14) + + # Show full timestamps of the beginning and the end of the plotted time interval + plt.suptitle(f'Device power consumption {start_time} - {end_time}', fontsize=18, fontweight='bold') + + # Add note to plot in case of issues in measurement frequency + with open("low_frequency_flag", 'r') as flag: + low_frequency_note = flag.readline() + plt.title(f'During "{test_name}"\n{low_frequency_note}', loc='center', fontweight="bold", fontsize=16) + plt.ylabel('Power (mW)', fontsize=16) + plt.grid(True) + plt.xticks(data['time'], rotation=45, fontsize=14) + + # Set maximum for tick number + plt.locator_params(axis='x', nbins=40) + + plt.savefig(f'../test-suites/power_test.png') + return + +def mean_power(csv_file): + columns = ['time', 'meas_counter', 'power'] + data = pd.read_csv(csv_file, names=columns) + mean_value = data['power'].mean() + return mean_value diff --git a/Robot-Framework/resources/power_meas_keywords.resource b/Robot-Framework/resources/power_meas_keywords.resource new file mode 100644 index 00000000..640ed670 --- /dev/null +++ b/Robot-Framework/resources/power_meas_keywords.resource @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: 2022-2024 Technology Innovation Institute (TII) +# SPDX-License-Identifier: Apache-2.0 + +*** Settings *** +Resource ../config/variables.robot +Library ../lib/parse_power_data.py +Library SSHLibrary +Library DateTime + +*** Variables *** +${SSH_MEASUREMENT} ${EMPTY} +${start_timestamp} ${EMPTY} +${RPI_IP_ADDRESS} ${EMPTY} + + +*** Keywords *** + +Check variable availability + ${value}= Get Variable Value ${RPI_IP_ADDRESS} + IF $value!='${EMPTY}' + RETURN ${True} + ELSE + RETURN ${False} + END + +Start power measurement + [Documentation] Connect to the measurement agent and run script to start collecting measurement results + [Arguments] ${id}=power_data ${timeout}=200 + ${availability} Check variable availability + IF ${availability}==False + Log To Console Power measurement agent IP address not defined. Ignoring all power measurement related keywords. + Set Global Variable ${SSH_MEASUREMENT} ${EMPTY} + RETURN + END + ${status} ${connection} Run Keyword And Ignore Error Connect to measurement agent + IF '${status}'!='PASS' + Set Global Variable ${SSH_MEASUREMENT} ${EMPTY} + Log To Console Power measurement agent not found. Ignoring all power measurement related keywords. + RETURN + END + # Multiple logging processes not allowed (for now) + Stop recording power + Start recording power ${id} ${timeout} + +Connect to measurement agent + [Documentation] Set up SSH connection to the measurement agent + [Arguments] ${IP}=${RPI_IP_ADDRESS} ${PORT}=22 ${target_output}=ghaf@raspberrypi + # Use existing connection if available + ${status} ${output} Run Keyword And Ignore Error Switch Connection ${SSH_MEASUREMENT} + IF '${status}'=='PASS' + Log To Console Switched connection to measurement agent. + Set Global Variable ${SSH_MEASUREMENT} ${SSH_MEASUREMENT} + RETURN ${SSH_MEASUREMENT} + END + Log To Console Connecting to measurement agent + ${connection}= Open Connection ${IP} port=${PORT} prompt=\$ timeout=15 + ${output}= Login username=${LOGIN_PI} password=${PASSWORD_PI} + Should Contain ${output} ${target_output} + Set Global Variable ${SSH_MEASUREMENT} ${connection} + RETURN ${SSH_MEASUREMENT} + +Start recording power + [Arguments] ${file_name} ${timeout} + Log To Console Starting to record power measurements + Run Keyword And Ignore Error Execute Command nohup python /home/ghaf/ghaf/ghaf-power-measurement/measure_power.py ${file_name}.csv ${timeout} > output.log 2>&1 & timeout=3 + +Stop recording power + IF $SSH_MEASUREMENT=='${EMPTY}' + Log To Console No connection to power measurement device. Ignoring all power measurement related keywords. + RETURN + END + Log To Console Stopping power recording + Run Keyword And Ignore Error Execute Command pkill python timeout=3 + +Get power record + [Arguments] ${file_name}=power_data.csv + IF $SSH_MEASUREMENT=='${EMPTY}' + Log To Console No connection to power measurement device. Ignoring all power measurement related keywords. + RETURN + END + Run Keyword And Ignore Error Connect to measurement agent + Run Keyword And Ignore Error SSHLibrary.Get File /home/ghaf/ghaf/power_data/${file_name} ../../../power_measurements/ + +Save power measurement interval + [Documentation] Extract measurement data within given time interval + [Arguments] ${file_name} ${start_time} ${end_time} + IF $SSH_MEASUREMENT=='${EMPTY}' + Log To Console No connection to power measurement device. Ignoring all power measurement related keywords. + RETURN + END + Log To Console Extract power data from given time interval + ${time_interval} DateTime.Subtract Date From Date ${end_time} ${start_time} exclude_millis=True + IF ${time_interval} < 0 + Log To Console Invalid timestamp critera for extracting power data + RETURN + END + Run Keyword And Ignore Error Extract time interval ../../../power_measurements/${file_name} ${start_time} ${end_time} + +Generate power plot + [Documentation] Extract power data from start_timestamp to current time. + ... Plot power vs time and save to png file. + [Arguments] ${id} ${test_name} + IF $SSH_MEASUREMENT=='${EMPTY}' + Log To Console No connection to power measurement device. Ignoring all power measurement related keywords. + RETURN + END + ${end_timestamp} Get current timestamp + Run Keyword And Ignore Error Connect to measurement agent + Run Keyword And Ignore Error Get power record ${id}.csv + Run Keyword And Ignore Error Save power measurement interval ${id}.csv '${start_timestamp}' '${end_timestamp}' + Run Keyword And Ignore Error Generate graph power_interval.csv ${test_name} + Run Keyword And Ignore Error Log Power plot HTML + +Set start timestamp + ${current_time} DateTime.Get Current Date UTC exclude_millis=yes + Set Global Variable ${start_timestamp} ${current_time} + Log To Console ${start_timestamp} + +Get current timestamp + ${current_time} DateTime.Get Current Date UTC exclude_millis=yes + RETURN ${current_time} + +Log average power + [Arguments] ${file_name} + IF $SSH_MEASUREMENT=='${EMPTY}' + Log To Console No connection to power measurement device. Ignoring all power measurement related keywords. + RETURN + END + ${keyword_status} ${mean_P} Run Keyword And Ignore Error Mean power ${file_name} + # TODO: With the statistics tools of performance testing also average power values could be plotted and monitored diff --git a/flake.lock b/flake.lock index 00db58a8..0326b7a4 100644 --- a/flake.lock +++ b/flake.lock @@ -58,4 +58,4 @@ }, "root": "root", "version": 7 -} +} \ No newline at end of file