From 6b0658f68786110f41cd82558bd97447e4b14203 Mon Sep 17 00:00:00 2001 From: Greg Kroah-Hartman Date: Tue, 15 Dec 2015 12:46:22 -0800 Subject: [PATCH] greybus: tools: Add tools directory to greybus repo and add loopback Move the loopback test to the greybus main repo, as we will be adding more tests over time and it doesn't need to be burried in the gbsim repo. This moves the latest version from gbsim to this repo and fixes up the Makefile to be a bit more "smart" when building the code. Signed-off-by: Greg Kroah-Hartman --- drivers/staging/greybus/.gitignore | 1 + drivers/staging/greybus/Makefile | 6 +- drivers/staging/greybus/tools/Android.mk | 10 + drivers/staging/greybus/tools/Makefile | 31 + drivers/staging/greybus/tools/README.loopback | 198 ++++++ drivers/staging/greybus/tools/lbtest | 168 +++++ drivers/staging/greybus/tools/loopback_test.c | 958 ++++++++++++++++++++++++++ 7 files changed, 1371 insertions(+), 1 deletion(-) create mode 100644 drivers/staging/greybus/tools/Android.mk create mode 100644 drivers/staging/greybus/tools/Makefile create mode 100644 drivers/staging/greybus/tools/README.loopback create mode 100755 drivers/staging/greybus/tools/lbtest create mode 100644 drivers/staging/greybus/tools/loopback_test.c diff --git a/drivers/staging/greybus/.gitignore b/drivers/staging/greybus/.gitignore index 8395773..faf45ee 100644 --- a/drivers/staging/greybus/.gitignore +++ b/drivers/staging/greybus/.gitignore @@ -12,3 +12,4 @@ tags cscope.* ncscope.* *.patch +tools/loopback_test diff --git a/drivers/staging/greybus/Makefile b/drivers/staging/greybus/Makefile index e9e1cd0..7fd1fc3 100644 --- a/drivers/staging/greybus/Makefile +++ b/drivers/staging/greybus/Makefile @@ -85,7 +85,10 @@ ccflags-y := -Wall # needed for trace events ccflags-y += -I$(src) -all: module +all: module tools + +tools:: + $(MAKE) -C tools KERNELDIR=$(realpath $(KERNELDIR)) module: $(MAKE) -C $(KERNELDIR) M=$(PWD) @@ -97,6 +100,7 @@ clean: rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c rm -f Module.markers Module.symvers modules.order rm -rf .tmp_versions Modules.symvers + $(MAKE) -C tools clean coccicheck: $(MAKE) -C $(KERNELDIR) M=$(PWD) coccicheck diff --git a/drivers/staging/greybus/tools/Android.mk b/drivers/staging/greybus/tools/Android.mk new file mode 100644 index 0000000..fdadbf6 --- /dev/null +++ b/drivers/staging/greybus/tools/Android.mk @@ -0,0 +1,10 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= loopback_test.c +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE := gb_loopback_test + +include $(BUILD_EXECUTABLE) + diff --git a/drivers/staging/greybus/tools/Makefile b/drivers/staging/greybus/tools/Makefile new file mode 100644 index 0000000..852b12b --- /dev/null +++ b/drivers/staging/greybus/tools/Makefile @@ -0,0 +1,31 @@ +ifeq ($(strip $(V)), 1) + Q = +else + Q = @ +endif + +CFLAGS += -std=gnu99 -Wall -Wextra -g \ + -D_GNU_SOURCE \ + -Wno-unused-parameter \ + -Wmaybe-uninitialized \ + -Wredundant-decls \ + -Wcast-align \ + -Wsign-compare \ + -Wno-missing-field-initializers + +CC := $(CROSS_COMPILE)gcc + +TOOLS = loopback_test + +all: $(TOOLS) + +%.o: %.c ../greybus_protocols.h + @echo ' TARGET_CC $@' + $(Q)$(CC) $(CFLAGS) -c $< -o $@ + +loopback_%: loopback_%.o + @echo ' TARGET_LD $@' + $(Q)$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ + +clean:: + rm -f *.o $(TOOLS) diff --git a/drivers/staging/greybus/tools/README.loopback b/drivers/staging/greybus/tools/README.loopback new file mode 100644 index 0000000..845b08d --- /dev/null +++ b/drivers/staging/greybus/tools/README.loopback @@ -0,0 +1,198 @@ + + + 1 - LOOPBACK DRIVER + +The driver implements the main logic of the loopback test and provides +sysfs files to configure the test and retrieve the results. +A user could run a test without the need of the test application given +that he understands the sysfs interface of the loopback driver. + +The loopback kernel driver needs to be loaded and at least one module +with the loopback feature enabled must be present for the sysfs files to be +created and for the loopback test application to be able to run. + +To load the module: +# modprobe gb-loopback + + +When the module is probed, New files are available on the sysfs +directory of the detected loopback device. +(typically under "/sys/bus/graybus/devices"). + +Here is a short summary of the sysfs interface files that should be visible: + +* Loopback Configuration Files: + async - Use asynchronous operations. + iteration_max - Number of tests iterations to perform. + size - payload size of the transfer. + timeout - The number of microseconds to give an individual + asynchronous request before timing out. + us_wait - Time to wait between 2 messages + type - By writing the test type to this file, the test starts. + Valid tests are: + 0 stop the test + 2 - ping + 3 - transfer + 4 - sink + +* Loopback feedback files: + error - number of errors that have occurred. + iteration_count - Number of iterations performed. + requests_completed - Number of requests successfully completed. + requests_timedout - Number of requests that have timed out. + timeout_max - Max allowed timeout + timeout_min - Min allowed timeout. + +* Loopback result files: + apbridge_unipro_latency_avg + apbridge_unipro_latency_max + apbridge_unipro_latency_min + gpbridge_firmware_latency_avg + gpbridge_firmware_latency_max + gpbridge_firmware_latency_min + requests_per_second_avg + requests_per_second_max + requests_per_second_min + latency_avg + latency_max + latency_min + throughput_avg + throughput_max + throughput_min + + + + 2 - LOOPBACK TEST APPLICATION + +The loopback test application manages and formats the results provided by +the loopback kernel module. The purpose of this application +is to: + - Start and manage multiple loopback device tests concurrently. + - Calculate the aggregate results for multiple devices. + - Gather and format test results (csv or human readable). + +The best way to get up to date usage information for the application is +usually to pass the "-h" parameter. +Here is the summary of the available options: + + Mandatory arguments + -t must be one of the test names - sink, transfer or ping + -i iteration count - the number of iterations to run the test over + Optional arguments + -S sysfs location - location for greybus 'endo' entires default /sys/bus/greybus/devices/ + -D debugfs location - location for loopback debugfs entries default /sys/kernel/debug/gb_loopback/ + -s size of data packet to send during test - defaults to zero + -m mask - a bit mask of connections to include example: -m 8 = 4th connection -m 9 = 1st and 4th connection etc + default is zero which means broadcast to all connections + -v verbose output + -d debug output + -r raw data output - when specified the full list of latency values are included in the output CSV + -p porcelain - when specified printout is in a user-friendly non-CSV format. This option suppresses writing to CSV file + -a aggregate - show aggregation of all enabled devies + -l list found loopback devices and exit. + -x Async - Enable async transfers. + -o Timeout - Timeout in microseconds for async operations. + + + + 3 - REAL WORLD EXAMPLE USAGES + + 3.1 - Using the driver sysfs files to run a test on a single device: + +* Run a 1000 transfers of a 100 byte packet. Each transfer is started only +after the previous one finished successfully: + echo 0 > /sys/bus/greybus/devices/1-2.17/type + echo 0 > /sys/bus/greybus/devices/1-2.17/async + echo 2000 > /sys/bus/greybus/devices/1-2.17/us_wait + echo 100 > /sys/bus/greybus/devices/1-2.17/size + echo 1000 > /sys/bus/greybus/devices/1-2.17/iteration_max + echo 0 > /sys/bus/greybus/devices/1-2.17/mask + echo 200000 > /sys/bus/greybus/devices/1-2.17/timeout + echo 3 > /sys/bus/greybus/devices/1-2.17/type + +* Run a 1000 transfers of a 100 byte packet. Transfers are started without +waiting for the previous one to finish: + echo 0 > /sys/bus/greybus/devices/1-2.17/type + echo 3 > /sys/bus/greybus/devices/1-2.17/async + echo 0 > /sys/bus/greybus/devices/1-2.17/us_wait + echo 100 > /sys/bus/greybus/devices/1-2.17/size + echo 1000 > /sys/bus/greybus/devices/1-2.17/iteration_max + echo 0 > /sys/bus/greybus/devices/1-2.17/mask + echo 200000 > /sys/bus/greybus/devices/1-2.17/timeout + echo 3 > /sys/bus/greybus/devices/1-2.17/type + +* Read the results from sysfs: + cat /sys/bus/greybus/devices/1-2.17/requests_per_second_min + cat /sys/bus/greybus/devices/1-2.17/requests_per_second_max + cat /sys/bus/greybus/devices/1-2.17/requests_per_second_avg + + cat /sys/bus/greybus/devices/1-2.17/latency_min + cat /sys/bus/greybus/devices/1-2.17/latency_max + cat /sys/bus/greybus/devices/1-2.17/latency_avg + + cat /sys/bus/greybus/devices/1-2.17/apbridge_unipro_latency_min + cat /sys/bus/greybus/devices/1-2.17/apbridge_unipro_latency_max + cat /sys/bus/greybus/devices/1-2.17/apbridge_unipro_latency_avg + + cat /sys/bus/greybus/devices/1-2.17/gpbridge_firmware_latency_min + cat /sys/bus/greybus/devices/1-2.17/gpbridge_firmware_latency_max + cat /sys/bus/greybus/devices/1-2.17/gpbridge_firmware_latency_avg + + cat /sys/bus/greybus/devices/1-2.17/error + cat /sys/bus/greybus/devices/1-2.17/requests_completed + cat /sys/bus/greybus/devices/1-2.17/requests_timedout + + +3.2 - using the test application: + +* Run a transfer test 10 iterations of size 100 bytes on all available devices + #/loopback_test -t transfer -i 10 -s 100 + 1970-1-1 0:10:7,transfer,1-4.17,100,10,0,443,509,471.700012,66,1963,2256,2124.600098,293,102776,118088,109318.898438,15312,1620,1998,1894.099976,378,56,57,56.799999,1 + 1970-1-1 0:10:7,transfer,1-5.17,100,10,0,399,542,463.399994,143,1845,2505,2175.800049,660,92568,125744,107393.296875,33176,1469,2305,1806.500000,836,56,57,56.799999,1 + + +* Show the aggregate results of both devices. ("-a") + #/loopback_test -t transfer -i 10 -s 100 -a + 1970-1-1 0:10:35,transfer,1-4.17,100,10,0,448,580,494.100006,132,1722,2230,2039.400024,508,103936,134560,114515.703125,30624,1513,1980,1806.900024,467,56,57,57.299999,1 + 1970-1-1 0:10:35,transfer,1-5.17,100,10,0,383,558,478.600006,175,1791,2606,2115.199951,815,88856,129456,110919.703125,40600,1457,2246,1773.599976,789,56,57,57.099998,1 + 1970-1-1 0:10:35,transfer,aggregate,100,10,0,383,580,486.000000,197,1722,2606,2077.000000,884,88856,134560,112717.000000,45704,1457,2246,1789.000000,789,56,57,57.000000,1 + +* Example usage of the mask option to select which devices will + run the test (1st, 2nd, or both devices): + # /loopback_test -t transfer -i 10 -s 100 -m 1 + 1970-1-1 0:11:56,transfer,1-4.17,100,10,0,514,558,544.900024,44,1791,1943,1836.599976,152,119248,129456,126301.296875,10208,1600,1001609,101613.601562,1000009,56,57,56.900002,1 + # /loopback_test -t transfer -i 10 -s 100 -m 2 + 1970-1-1 0:12:0,transfer,1-5.17,100,10,0,468,554,539.000000,86,1804,2134,1859.500000,330,108576,128528,124932.500000,19952,1606,1626,1619.300049,20,56,57,57.400002,1 + # /loopback_test -t transfer -i 10 -s 100 -m 3 + 1970-1-1 0:12:3,transfer,1-4.17,100,10,0,432,510,469.399994,78,1959,2313,2135.800049,354,100224,118320,108785.296875,18096,1610,2024,1893.500000,414,56,57,57.200001,1 + 1970-1-1 0:12:3,transfer,1-5.17,100,10,0,404,542,468.799988,138,1843,2472,2152.500000,629,93728,125744,108646.101562,32016,1504,2247,1853.099976,743,56,57,57.099998,1 + +* Show output in human readable format ("-p") + # /loopback_test -t transfer -i 10 -s 100 -m 3 -p + + 1970-1-1 0:12:37 + test: transfer + path: 1-4.17 + size: 100 + iterations: 10 + errors: 0 + async: Disabled + requests per-sec: min=390, max=547, average=469.299988, jitter=157 + ap-throughput B/s: min=90480 max=126904 average=108762.101562 jitter=36424 + ap-latency usec: min=1826 max=2560 average=2146.000000 jitter=734 + apbridge-latency usec: min=1620 max=1982 average=1882.099976 jitter=362 + gpbridge-latency usec: min=56 max=57 average=57.099998 jitter=1 + + + 1970-1-1 0:12:37 + test: transfer + path: 1-5.17 + size: 100 + iterations: 10 + errors: 0 + async: Disabled + requests per-sec: min=397, max=538, average=461.700012, jitter=141 + ap-throughput B/s: min=92104 max=124816 average=106998.898438 jitter=32712 + ap-latency usec: min=1856 max=2514 average=2185.699951 jitter=658 + apbridge-latency usec: min=1460 max=2296 average=1828.599976 jitter=836 + gpbridge-latency usec: min=56 max=57 average=57.099998 jitter=1 diff --git a/drivers/staging/greybus/tools/lbtest b/drivers/staging/greybus/tools/lbtest new file mode 100755 index 0000000..d7353f1 --- /dev/null +++ b/drivers/staging/greybus/tools/lbtest @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +# Copyright (c) 2015 Google, Inc. +# Copyright (c) 2015 Linaro, Ltd. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import print_function +import csv +import datetime +import sys +import time + +dict = {'ping': '2', 'transfer': '3', 'sink': '4'} +verbose = 1 + +def abort(): + sys.exit(1) + +def usage(): + print('Usage: looptest TEST SIZE ITERATIONS PATH\n\n' + ' Run TEST for a number of ITERATIONS with operation data SIZE bytes\n' + ' TEST may be \'ping\' \'transfer\' or \'sink\'\n' + ' SIZE indicates the size of transfer <= greybus max payload bytes\n' + ' ITERATIONS indicates the number of times to execute TEST at SIZE bytes\n' + ' Note if ITERATIONS is set to zero then this utility will\n' + ' initiate an infinite (non terminating) test and exit\n' + ' without logging any metrics data\n' + ' PATH indicates the sysfs path for the loopback greybus entries e.g.\n' + ' /sys/bus/greybus/devices/endo0:1:1:1:1/\n' + 'Examples:\n' + ' looptest transfer 128 10000\n' + ' looptest ping 0 128\n' + ' looptest sink 2030 32768\n' + .format(sys.argv[0]), file=sys.stderr) + + abort() + +def read_sysfs_int(path): + try: + f = open(path, "r"); + val = f.read(); + f.close() + return int(val) + except IOError as e: + print("I/O error({0}): {1}".format(e.errno, e.strerror)) + print("Invalid path %s" % path) + +def write_sysfs_val(path, val): + try: + f = open(path, "r+") + f.write(val) + f.close() + except IOError as e: + print("I/O error({0}): {1}".format(e.errno, e.strerror)) + print("Invalid path %s" % path) + +def log_csv(test_name, size, iteration_max, sys_pfx): + # file name will test_name_size_iteration_max.csv + # every time the same test with the same parameters is run we will then + # append to the same CSV with datestamp - representing each test dataset + fname = test_name + '_' + size + '_' + str(iteration_max) + '.csv' + + try: + # gather data set + date = str(datetime.datetime.now()) + error = read_sysfs_int(sys_pfx + 'error') + request_min = read_sysfs_int(sys_pfx + 'requests_per_second_min') + request_max = read_sysfs_int(sys_pfx + 'requests_per_second_max') + request_avg = read_sysfs_int(sys_pfx + 'requests_per_second_avg') + latency_min = read_sysfs_int(sys_pfx + 'latency_min') + latency_max = read_sysfs_int(sys_pfx + 'latency_max') + latency_avg = read_sysfs_int(sys_pfx + 'latency_avg') + throughput_min = read_sysfs_int(sys_pfx + 'throughput_min') + throughput_max = read_sysfs_int(sys_pfx + 'throughput_max') + throughput_avg = read_sysfs_int(sys_pfx + 'throughput_avg') + + # derive jitter + request_jitter = request_max - request_min + latency_jitter = latency_max - latency_min + throughput_jitter = throughput_max - throughput_min + + # append data set to file + with open(fname, 'a') as csvf: + row = csv.writer(csvf, delimiter=",", quotechar="'", + quoting=csv.QUOTE_MINIMAL) + row.writerow([date, test_name, size, iteration_max, error, + request_min, request_max, request_avg, request_jitter, + latency_min, latency_max, latency_avg, latency_jitter, + throughput_min, throughput_max, throughput_avg, throughput_jitter]) + except IOError as e: + print("I/O error({0}): {1}".format(e.errno, e.strerror)) + +def loopback_run(test_name, size, iteration_max, sys_pfx): + test_id = dict[test_name] + try: + # Terminate any currently running test + write_sysfs_val(sys_pfx + 'type', '0') + # Set parameter for no wait between messages + write_sysfs_val(sys_pfx + 'ms_wait', '0') + # Set operation size + write_sysfs_val(sys_pfx + 'size', size) + # Set iterations + write_sysfs_val(sys_pfx + 'iteration_max', str(iteration_max)) + # Initiate by setting loopback operation type + write_sysfs_val(sys_pfx + 'type', test_id) + time.sleep(1) + + if iteration_max == 0: + print ("Infinite test initiated CSV won't be logged\n") + return + + previous = 0 + err = 0 + while True: + # get current count bail out if it hasn't changed + iteration_count = read_sysfs_int(sys_pfx + 'iteration_count') + if previous == iteration_count: + err = 1 + break + elif iteration_count == iteration_max: + break + previous = iteration_count + if verbose: + print('%02d%% complete %d of %d ' % + (100 * iteration_count / iteration_max, + iteration_count, iteration_max)) + time.sleep(1) + if err: + print ('\nError executing test\n') + else: + log_csv(test_name, size, iteration_max, sys_pfx) + except ValueError as ve: + print("Error: %s " % format(e.strerror), file=sys.stderr) + abort() + +def main(): + if len(sys.argv) < 5: + usage() + + if sys.argv[1] in dict.keys(): + loopback_run(sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]) + else: + usage() +if __name__ == '__main__': + main() diff --git a/drivers/staging/greybus/tools/loopback_test.c b/drivers/staging/greybus/tools/loopback_test.c new file mode 100644 index 0000000..55b3102 --- /dev/null +++ b/drivers/staging/greybus/tools/loopback_test.c @@ -0,0 +1,958 @@ +/* + * Loopback test application + * + * Copyright 2015 Google Inc. + * Copyright 2015 Linaro Ltd. + * + * Provided under the three clause BSD license found in the LICENSE file. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_NUM_DEVICES 50 +#define MAX_SYSFS_PATH 0x200 +#define CSV_MAX_LINE 0x1000 +#define SYSFS_MAX_INT 0x20 +#define MAX_STR_LEN 255 +#define MAX_TIMEOUT_COUNT 5 +#define TIMEOUT_SEC 1 +#define DEFAULT_ASYNC_TIMEOUT 200000 + +struct dict { + char *name; + int type; +}; + +static struct dict dict[] = { + {"ping", 2}, + {"transfer", 3}, + {"sink", 4} +}; + +struct loopback_results { + float latency_avg; + uint32_t latency_max; + uint32_t latency_min; + uint32_t latency_jitter; + + float request_avg; + uint32_t request_max; + uint32_t request_min; + uint32_t request_jitter; + + float throughput_avg; + uint32_t throughput_max; + uint32_t throughput_min; + uint32_t throughput_jitter; + + float apbridge_unipro_latency_avg; + uint32_t apbridge_unipro_latency_max; + uint32_t apbridge_unipro_latency_min; + uint32_t apbridge_unipro_latency_jitter; + + float gpbridge_firmware_latency_avg; + uint32_t gpbridge_firmware_latency_max; + uint32_t gpbridge_firmware_latency_min; + uint32_t gpbridge_firmware_latency_jitter; + + uint32_t error; +}; + +struct loopback_device { + char name[MAX_SYSFS_PATH]; + char sysfs_entry[MAX_SYSFS_PATH]; + char debugfs_entry[MAX_SYSFS_PATH]; + int inotify_wd; + struct loopback_results results; +}; + +struct loopback_test { + int verbose; + int debug; + int raw_data_dump; + int porcelain; + int mask; + int size; + int iteration_max; + int aggregate_output; + int test_id; + int device_count; + int inotify_fd; + int list_devices; + int use_async; + int async_timeout; + int async_outstanding_operations; + int us_wait; + char test_name[MAX_STR_LEN]; + char sysfs_prefix[MAX_SYSFS_PATH]; + char debugfs_prefix[MAX_SYSFS_PATH]; + struct loopback_device devices[MAX_NUM_DEVICES]; + struct loopback_results aggregate_results; +}; +struct loopback_test t; + +/* Helper macros to calculate the aggregate results for all devices */ +static inline int device_enabled(struct loopback_test *t, int dev_idx); + +#define GET_MAX(field) \ +static int get_##field##_aggregate(struct loopback_test *t) \ +{ \ + uint32_t max = 0; \ + int i; \ + for (i = 0; i < t->device_count; i++) { \ + if (!device_enabled(t, i)) \ + continue; \ + if (t->devices[i].results.field > max) \ + max = t->devices[i].results.field; \ + } \ + return max; \ +} \ + +#define GET_MIN(field) \ +static int get_##field##_aggregate(struct loopback_test *t) \ +{ \ + uint32_t min = ~0; \ + int i; \ + for (i = 0; i < t->device_count; i++) { \ + if (!device_enabled(t, i)) \ + continue; \ + if (t->devices[i].results.field < min) \ + min = t->devices[i].results.field; \ + } \ + return min; \ +} \ + +#define GET_AVG(field) \ +static int get_##field##_aggregate(struct loopback_test *t) \ +{ \ + uint32_t val = 0; \ + uint32_t count = 0; \ + int i; \ + for (i = 0; i < t->device_count; i++) { \ + if (!device_enabled(t, i)) \ + continue; \ + count++; \ + val += t->devices[i].results.field; \ + } \ + if (count) \ + val /= count; \ + return val; \ +} \ + +GET_MAX(throughput_max); +GET_MAX(request_max); +GET_MAX(latency_max); +GET_MAX(apbridge_unipro_latency_max); +GET_MAX(gpbridge_firmware_latency_max); +GET_MIN(throughput_min); +GET_MIN(request_min); +GET_MIN(latency_min); +GET_MIN(apbridge_unipro_latency_min); +GET_MIN(gpbridge_firmware_latency_min); +GET_AVG(throughput_avg); +GET_AVG(request_avg); +GET_AVG(latency_avg); +GET_AVG(apbridge_unipro_latency_avg); +GET_AVG(gpbridge_firmware_latency_avg); + +void abort() +{ + _exit(1); +} + +void usage(void) +{ + fprintf(stderr, "Usage: loopback_test TEST [SIZE] ITERATIONS [SYSPATH] [DBGPATH]\n\n" + " Run TEST for a number of ITERATIONS with operation data SIZE bytes\n" + " TEST may be \'ping\' \'transfer\' or \'sink\'\n" + " SIZE indicates the size of transfer <= greybus max payload bytes\n" + " ITERATIONS indicates the number of times to execute TEST at SIZE bytes\n" + " Note if ITERATIONS is set to zero then this utility will\n" + " initiate an infinite (non terminating) test and exit\n" + " without logging any metrics data\n" + " SYSPATH indicates the sysfs path for the loopback greybus entries e.g.\n" + " /sys/bus/greybus/devices\n" + " DBGPATH indicates the debugfs path for the loopback greybus entries e.g.\n" + " /sys/kernel/debug/gb_loopback/\n" + " Mandatory arguments\n" + " -t must be one of the test names - sink, transfer or ping\n" + " -i iteration count - the number of iterations to run the test over\n" + " Optional arguments\n" + " -S sysfs location - location for greybus 'endo' entires default /sys/bus/greybus/devices/\n" + " -D debugfs location - location for loopback debugfs entries default /sys/kernel/debug/gb_loopback/\n" + " -s size of data packet to send during test - defaults to zero\n" + " -m mask - a bit mask of connections to include example: -m 8 = 4th connection -m 9 = 1st and 4th connection etc\n" + " default is zero which means broadcast to all connections\n" + " -v verbose output\n" + " -d debug output\n" + " -r raw data output - when specified the full list of latency values are included in the output CSV\n" + " -p porcelain - when specified printout is in a user-friendly non-CSV format. This option suppresses writing to CSV file\n" + " -a aggregate - show aggregation of all enabled devices\n" + " -l list found loopback devices and exit\n" + " -x Async - Enable async transfers\n" + " -o Async Timeout - Timeout in uSec for async operations\n" + " -c Max number of outstanding operations for async operations\n" + " -w Wait in uSec between operations\n" + "Examples:\n" + " Send 10000 transfers with a packet size of 128 bytes to all active connections\n" + " loopback_test -t transfer -s 128 -i 10000 -S /sys/bus/greybus/devices/ -D /sys/kernel/debug/gb_loopback/\n" + " loopback_test -t transfer -s 128 -i 10000 -m 0\n" + " Send 10000 transfers with a packet size of 128 bytes to connection 1 and 4\n" + " loopback_test -t transfer -s 128 -i 10000 -m 9\n" + " loopback_test -t ping -s 0 128 -i -S /sys/bus/greybus/devices/ -D /sys/kernel/debug/gb_loopback/\n" + " loopback_test -t sink -s 2030 -i 32768 -S /sys/bus/greybus/devices/ -D /sys/kernel/debug/gb_loopback/\n"); + abort(); +} + +static inline int device_enabled(struct loopback_test *t, int dev_idx) +{ + if (!t->mask || (t->mask & (1 << dev_idx))) + return 1; + + return 0; +} + +static void show_loopback_devices(struct loopback_test *t) +{ + int i; + + if (t->device_count == 0) { + printf("No loopback devices.\n"); + return; + } + + for (i = 0; i < t->device_count; i++) + printf("device[%d] = %s\n", i, t->devices[i].name); + +} + +int open_sysfs(const char *sys_pfx, const char *node, int flags) +{ + int fd; + char path[MAX_SYSFS_PATH]; + + snprintf(path, sizeof(path), "%s%s", sys_pfx, node); + fd = open(path, flags); + if (fd < 0) { + fprintf(stderr, "unable to open %s\n", path); + abort(); + } + return fd; +} + +int read_sysfs_int_fd(int fd, const char *sys_pfx, const char *node) +{ + char buf[SYSFS_MAX_INT]; + + if (read(fd, buf, sizeof(buf)) < 0) { + fprintf(stderr, "unable to read from %s%s %s\n", sys_pfx, node, + strerror(errno)); + close(fd); + abort(); + } + return atoi(buf); +} + +float read_sysfs_float_fd(int fd, const char *sys_pfx, const char *node) +{ + char buf[SYSFS_MAX_INT]; + + if (read(fd, buf, sizeof(buf)) < 0) { + + fprintf(stderr, "unable to read from %s%s %s\n", sys_pfx, node, + strerror(errno)); + close(fd); + abort(); + } + return atof(buf); +} + +int read_sysfs_int(const char *sys_pfx, const char *node) +{ + int fd, val; + + fd = open_sysfs(sys_pfx, node, O_RDONLY); + val = read_sysfs_int_fd(fd, sys_pfx, node); + close(fd); + return val; +} + +float read_sysfs_float(const char *sys_pfx, const char *node) +{ + int fd; + float val; + + fd = open_sysfs(sys_pfx, node, O_RDONLY); + val = read_sysfs_float_fd(fd, sys_pfx, node); + close(fd); + return val; +} + +void write_sysfs_val(const char *sys_pfx, const char *node, int val) +{ + int fd, len; + char buf[SYSFS_MAX_INT]; + + fd = open_sysfs(sys_pfx, node, O_RDWR); + len = snprintf(buf, sizeof(buf), "%d", val); + if (write(fd, buf, len) < 0) { + fprintf(stderr, "unable to write to %s%s %s\n", sys_pfx, node, + strerror(errno)); + close(fd); + abort(); + } + close(fd); +} + +static int get_results(struct loopback_test *t) +{ + struct loopback_device *d; + struct loopback_results *r; + int i; + + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + d = &t->devices[i]; + r = &d->results; + + r->error = read_sysfs_int(d->sysfs_entry, "error"); + r->request_min = read_sysfs_int(d->sysfs_entry, "requests_per_second_min"); + r->request_max = read_sysfs_int(d->sysfs_entry, "requests_per_second_max"); + r->request_avg = read_sysfs_float(d->sysfs_entry, "requests_per_second_avg"); + + r->latency_min = read_sysfs_int(d->sysfs_entry, "latency_min"); + r->latency_max = read_sysfs_int(d->sysfs_entry, "latency_max"); + r->latency_avg = read_sysfs_float(d->sysfs_entry, "latency_avg"); + + r->throughput_min = read_sysfs_int(d->sysfs_entry, "throughput_min"); + r->throughput_max = read_sysfs_int(d->sysfs_entry, "throughput_max"); + r->throughput_avg = read_sysfs_float(d->sysfs_entry, "throughput_avg"); + + r->apbridge_unipro_latency_min = + read_sysfs_int(d->sysfs_entry, "apbridge_unipro_latency_min"); + r->apbridge_unipro_latency_max = + read_sysfs_int(d->sysfs_entry, "apbridge_unipro_latency_max"); + r->apbridge_unipro_latency_avg = + read_sysfs_float(d->sysfs_entry, "apbridge_unipro_latency_avg"); + + r->gpbridge_firmware_latency_min = + read_sysfs_int(d->sysfs_entry, "gpbridge_firmware_latency_min"); + r->gpbridge_firmware_latency_max = + read_sysfs_int(d->sysfs_entry, "gpbridge_firmware_latency_max"); + r->gpbridge_firmware_latency_avg = + read_sysfs_float(d->sysfs_entry, "gpbridge_firmware_latency_avg"); + + r->request_jitter = r->request_max - r->request_min; + r->latency_jitter = r->latency_max - r->latency_min; + r->throughput_jitter = r->throughput_max - r->throughput_min; + r->apbridge_unipro_latency_jitter = + r->apbridge_unipro_latency_max - r->apbridge_unipro_latency_min; + r->gpbridge_firmware_latency_jitter = + r->gpbridge_firmware_latency_max - r->gpbridge_firmware_latency_min; + + } + + /*calculate the aggregate results of all enabled devices */ + if (t->aggregate_output) { + r = &t->aggregate_results; + + r->request_min = get_request_min_aggregate(t); + r->request_max = get_request_max_aggregate(t); + r->request_avg = get_request_avg_aggregate(t); + + r->latency_min = get_latency_min_aggregate(t); + r->latency_max = get_latency_max_aggregate(t); + r->latency_avg = get_latency_avg_aggregate(t); + + r->throughput_min = get_throughput_min_aggregate(t); + r->throughput_max = get_throughput_max_aggregate(t); + r->throughput_avg = get_throughput_avg_aggregate(t); + + r->apbridge_unipro_latency_min = + get_apbridge_unipro_latency_min_aggregate(t); + r->apbridge_unipro_latency_max = + get_apbridge_unipro_latency_max_aggregate(t); + r->apbridge_unipro_latency_avg = + get_apbridge_unipro_latency_avg_aggregate(t); + + r->gpbridge_firmware_latency_min = + get_gpbridge_firmware_latency_min_aggregate(t); + r->gpbridge_firmware_latency_max = + get_gpbridge_firmware_latency_max_aggregate(t); + r->gpbridge_firmware_latency_avg = + get_gpbridge_firmware_latency_avg_aggregate(t); + + r->request_jitter = r->request_max - r->request_min; + r->latency_jitter = r->latency_max - r->latency_min; + r->throughput_jitter = r->throughput_max - r->throughput_min; + r->apbridge_unipro_latency_jitter = + r->apbridge_unipro_latency_max - r->apbridge_unipro_latency_min; + r->gpbridge_firmware_latency_jitter = + r->gpbridge_firmware_latency_max - r->gpbridge_firmware_latency_min; + + } + + return 0; +} + +void log_csv_error(int len, int err) +{ + fprintf(stderr, "unable to write %d bytes to csv %s\n", len, + strerror(err)); +} + +int format_output(struct loopback_test *t, + struct loopback_results *r, + const char *dev_name, + char *buf, int buf_len, + struct tm *tm) +{ + int len = 0; + + memset(buf, 0x00, buf_len); + len = snprintf(buf, buf_len, "%u-%u-%u %u:%u:%u", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + + if (t->porcelain) { + len += snprintf(&buf[len], buf_len - len, + "\n test:\t\t\t%s\n path:\t\t\t%s\n size:\t\t\t%u\n iterations:\t\t%u\n errors:\t\t%u\n async:\t\t\t%s\n", + t->test_name, + dev_name, + t->size, + t->iteration_max, + r->error, + t->use_async ? "Enabled" : "Disabled"); + + len += snprintf(&buf[len], buf_len - len, + " requests per-sec:\tmin=%u, max=%u, average=%f, jitter=%u\n", + r->request_min, + r->request_max, + r->request_avg, + r->request_jitter); + + len += snprintf(&buf[len], buf_len - len, + " ap-throughput B/s:\tmin=%u max=%u average=%f jitter=%u\n", + r->throughput_min, + r->throughput_max, + r->throughput_avg, + r->throughput_jitter); + len += snprintf(&buf[len], buf_len - len, + " ap-latency usec:\tmin=%u max=%u average=%f jitter=%u\n", + r->latency_min, + r->latency_max, + r->latency_avg, + r->latency_jitter); + len += snprintf(&buf[len], buf_len - len, + " apbridge-latency usec:\tmin=%u max=%u average=%f jitter=%u\n", + r->apbridge_unipro_latency_min, + r->apbridge_unipro_latency_max, + r->apbridge_unipro_latency_avg, + r->apbridge_unipro_latency_jitter); + + len += snprintf(&buf[len], buf_len - len, + " gpbridge-latency usec:\tmin=%u max=%u average=%f jitter=%u\n", + r->gpbridge_firmware_latency_min, + r->gpbridge_firmware_latency_max, + r->gpbridge_firmware_latency_avg, + r->gpbridge_firmware_latency_jitter); + + } else { + len += snprintf(&buf[len], buf_len- len, ",%s,%s,%u,%u,%u", + t->test_name, dev_name, t->size, t->iteration_max, + r->error); + + len += snprintf(&buf[len], buf_len - len, ",%u,%u,%f,%u", + r->request_min, + r->request_max, + r->request_avg, + r->request_jitter); + + len += snprintf(&buf[len], buf_len - len, ",%u,%u,%f,%u", + r->latency_min, + r->latency_max, + r->latency_avg, + r->latency_jitter); + + len += snprintf(&buf[len], buf_len - len, ",%u,%u,%f,%u", + r->throughput_min, + r->throughput_max, + r->throughput_avg, + r->throughput_jitter); + + len += snprintf(&buf[len], buf_len - len, ",%u,%u,%f,%u", + r->apbridge_unipro_latency_min, + r->apbridge_unipro_latency_max, + r->apbridge_unipro_latency_avg, + r->apbridge_unipro_latency_jitter); + + len += snprintf(&buf[len], buf_len - len, ",%u,%u,%f,%u", + r->gpbridge_firmware_latency_min, + r->gpbridge_firmware_latency_max, + r->gpbridge_firmware_latency_avg, + r->gpbridge_firmware_latency_jitter); + } + + printf("\n%s\n", buf); + + return len; +} + +static int log_results(struct loopback_test *t) +{ + int fd, i, len, ret; + struct tm tm; + time_t local_time; + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + char file_name[MAX_SYSFS_PATH]; + char data[CSV_MAX_LINE]; + + local_time = time(NULL); + tm = *localtime(&local_time); + + /* + * file name will test_name_size_iteration_max.csv + * every time the same test with the same parameters is run we will then + * append to the same CSV with datestamp - representing each test + * dataset. + */ + if (!t->porcelain) { + snprintf(file_name, sizeof(file_name), "%s_%d_%d.csv", + t->test_name, t->size, t->iteration_max); + + fd = open(file_name, O_WRONLY | O_CREAT | O_APPEND, mode); + if (fd < 0) { + fprintf(stderr, "unable to open %s for appendation\n", file_name); + abort(); + } + + } + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + len = format_output(t, &t->devices[i].results, + t->devices[i].name, + data, sizeof(data), &tm); + if (!t->porcelain) { + ret = write(fd, data, len); + if (ret == -1) + fprintf(stderr, "unable to write %d bytes to csv.\n", len); + } + + } + + + if (t->aggregate_output) { + len = format_output(t, &t->aggregate_results, "aggregate", + data, sizeof(data), &tm); + if (!t->porcelain) { + ret = write(fd, data, len); + if (ret == -1) + fprintf(stderr, "unable to write %d bytes to csv.\n", len); + } + } + + if (!t->porcelain) + close(fd); + + return 0; +} + +int is_loopback_device(const char *path, const char *node) +{ + char file[MAX_SYSFS_PATH]; + + snprintf(file, MAX_SYSFS_PATH, "%s%s/iteration_count", path, node); + if (access(file, F_OK) == 0) + return 1; + return 0; +} + +int find_loopback_devices(struct loopback_test *t) +{ + struct dirent **namelist; + int i, n, ret; + unsigned int dev_id; + struct loopback_device *d; + + n = scandir(t->sysfs_prefix, &namelist, NULL, alphasort); + if (n < 0) { + perror("scandir"); + ret = -ENODEV; + goto baddir; + } + + /* Don't include '.' and '..' */ + if (n <= 2) { + ret = -ENOMEM; + goto done; + } + + for (i = 0; i < n; i++) { + ret = sscanf(namelist[i]->d_name, "gb_loopback%u", &dev_id); + if (ret != 1) + continue; + + if (!is_loopback_device(t->sysfs_prefix, namelist[i]->d_name)) + continue; + + if (t->device_count == MAX_NUM_DEVICES) { + fprintf(stderr, "max number of devices reached!\n"); + break; + } + + d = &t->devices[t->device_count++]; + snprintf(d->name, MAX_STR_LEN, "gb_loopback%u", dev_id); + + snprintf(d->sysfs_entry, MAX_SYSFS_PATH, "%s%s/", + t->sysfs_prefix, d->name); + + snprintf(d->debugfs_entry, MAX_SYSFS_PATH, "%sraw_latency_%s", + t->debugfs_prefix, d->name); + + if (t->debug) + printf("add %s %s\n", d->sysfs_entry, + d->debugfs_entry); + } + + ret = 0; +done: + for (i = 0; i < n; i++) + free(namelist[n]); + free(namelist); +baddir: + return ret; +} + + +static int register_for_notification(struct loopback_test *t) +{ + char buf[MAX_SYSFS_PATH]; + int i; + + t->inotify_fd = inotify_init(); + if (t->inotify_fd < 0) { + fprintf(stderr, "inotify_init fail %s\n", strerror(errno)); + abort(); + } + + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + snprintf(buf, sizeof(buf), "%s%s", t->devices[i].sysfs_entry, + "iteration_count"); + + t->devices[i].inotify_wd = inotify_add_watch(t->inotify_fd, + buf, IN_MODIFY); + if (t->devices[i].inotify_wd < 0) { + fprintf(stderr, "inotify_add_watch %s fail %s\n", + buf, strerror(errno)); + close(t->inotify_fd); + abort(); + } + } + + return 0; +} + +static int unregister_for_notification(struct loopback_test *t) +{ + int i; + int ret = 0; + + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + ret = inotify_rm_watch(t->inotify_fd, t->devices[i].inotify_wd); + if (ret) { + fprintf(stderr, "inotify_rm_watch error.\n"); + return ret; + } + } + + close(t->inotify_fd); + return 0; +} + +static int is_complete(struct loopback_test *t) +{ + uint32_t iteration_count = 0; + int i; + + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + iteration_count = read_sysfs_int(t->devices[i].sysfs_entry, + "iteration_count"); + + /* at least one device did not finish yet */ + if (iteration_count != t->iteration_max) + return 0; + } + + return 1; +} + +static int wait_for_complete(struct loopback_test *t) +{ + int remaining_timeouts = MAX_TIMEOUT_COUNT; + char buf[MAX_SYSFS_PATH]; + struct timeval timeout; + fd_set read_fds; + int ret; + + while (1) { + /* Wait for change */ + timeout.tv_sec = TIMEOUT_SEC; + timeout.tv_usec = 0; + FD_ZERO(&read_fds); + FD_SET(t->inotify_fd, &read_fds); + ret = select(FD_SETSIZE, &read_fds, NULL, NULL, &timeout); + if (ret < 0) { + fprintf(stderr, "Select error.\n"); + return -1; + } + + /* timeout - test may be finished.*/ + if (!FD_ISSET(t->inotify_fd, &read_fds)) { + remaining_timeouts--; + + if (is_complete(t)) + return 0; + + if (!remaining_timeouts) { + fprintf(stderr, "Too many timeouts\n"); + return -1; + } + } else { + /* read to clear the event */ + ret = read(t->inotify_fd, buf, sizeof(buf)); + } + } + + return 0; +} + +static void prepare_devices(struct loopback_test *t) +{ + int i; + + /* Cancel any running tests */ + for (i = 0; i < t->device_count; i++) + write_sysfs_val(t->devices[i].sysfs_entry, "type", 0); + + + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + write_sysfs_val(t->devices[i].sysfs_entry, "us_wait", + t->us_wait); + + /* Set operation size */ + write_sysfs_val(t->devices[i].sysfs_entry, "size", t->size); + + /* Set iterations */ + write_sysfs_val(t->devices[i].sysfs_entry, "iteration_max", + t->iteration_max); + + if (t->use_async) { + write_sysfs_val(t->devices[i].sysfs_entry, + "async", 1); + write_sysfs_val(t->devices[i].sysfs_entry, + "timeout", t->async_timeout); + write_sysfs_val(t->devices[i].sysfs_entry, + "outstanding_operations_max", + t->async_outstanding_operations); + } else + write_sysfs_val(t->devices[i].sysfs_entry, + "async", 0); + } +} + +static int start(struct loopback_test *t) +{ + int i; + + /* the test starts by writing test_id to the type file. */ + for (i = 0; i < t->device_count; i++) { + if (!device_enabled(t, i)) + continue; + + write_sysfs_val(t->devices[i].sysfs_entry, "type", t->test_id); + } + + return 0; +} + + +void loopback_run(struct loopback_test *t) +{ + int i; + int ret; + + for (i = 0; i < sizeof(dict) / sizeof(struct dict); i++) { + if (strstr(dict[i].name, t->test_name)) + t->test_id = dict[i].type; + } + if (!t->test_id) { + fprintf(stderr, "invalid test %s\n", t->test_name); + usage(); + return; + } + + prepare_devices(t); + + ret = register_for_notification(t); + if (ret) + goto err; + + start(t); + + sleep(1); + + wait_for_complete(t); + + unregister_for_notification(t); + + get_results(t); + + log_results(t); + + return; + +err: + printf("Error running test\n"); + return; +} + +static int sanity_check(struct loopback_test *t) +{ + int i; + + if (t->device_count == 0) { + fprintf(stderr, "No loopback devices found\n"); + return -1; + } + + for (i = 0; i < MAX_NUM_DEVICES; i++) { + if (!device_enabled(t, i)) + continue; + + if (t->mask && !strcmp(t->devices[i].name, "")) { + fprintf(stderr, "Bad device mask %x\n", (1 << i)); + return -1; + } + + } + + + return 0; +} +int main(int argc, char *argv[]) +{ + int o, ret; + char *sysfs_prefix = "/sys/class/gb_loopback/"; + char *debugfs_prefix = "/sys/kernel/debug/gb_loopback/"; + + memset(&t, 0, sizeof(t)); + + while ((o = getopt(argc, argv, + "t:s:i:S:D:m:v::d::r::p::a::l::x::o:c:w:")) != -1) { + switch (o) { + case 't': + snprintf(t.test_name, MAX_STR_LEN, "%s", optarg); + break; + case 's': + t.size = atoi(optarg); + break; + case 'i': + t.iteration_max = atoi(optarg); + break; + case 'S': + snprintf(t.sysfs_prefix, MAX_SYSFS_PATH, "%s", optarg); + break; + case 'D': + snprintf(t.debugfs_prefix, MAX_SYSFS_PATH, "%s", optarg); + break; + case 'm': + t.mask = atol(optarg); + break; + case 'v': + t.verbose = 1; + break; + case 'd': + t.debug = 1; + break; + case 'r': + t.raw_data_dump = 1; + break; + case 'p': + t.porcelain = 1; + break; + case 'a': + t.aggregate_output = 1; + break; + case 'l': + t.list_devices = 1; + break; + case 'x': + t.use_async = 1; + break; + case 'o': + t.async_timeout = atoi(optarg); + break; + case 'c': + t.async_outstanding_operations = atoi(optarg); + break; + case 'w': + t.us_wait = atoi(optarg); + break; + default: + usage(); + return -EINVAL; + } + } + + if (!strcmp(t.sysfs_prefix, "")) + snprintf(t.sysfs_prefix, MAX_SYSFS_PATH, "%s", sysfs_prefix); + + if (!strcmp(t.debugfs_prefix, "")) + snprintf(t.debugfs_prefix, MAX_SYSFS_PATH, "%s", debugfs_prefix); + + ret = find_loopback_devices(&t); + if (ret) + return ret; + ret = sanity_check(&t); + if (ret) + return ret; + + if (t.list_devices) { + show_loopback_devices(&t); + return 0; + } + + if (t.test_name[0] == '\0' || t.iteration_max == 0) + usage(); + + if (t.async_timeout == 0) + t.async_timeout = DEFAULT_ASYNC_TIMEOUT; + + loopback_run(&t); + + return 0; +} -- 2.7.4