diff --git a/LICENSE b/LICENSE index 386d3bea6..61fb28ab4 100644 --- a/LICENSE +++ b/LICENSE @@ -2217,6 +2217,7 @@ library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. apps/testing/sd_stress +apps/testing/sd_bench =================== Copyright (c) 2016-2021 PX4 Development Team. All rights reserved. diff --git a/testing/sd_bench/Kconfig b/testing/sd_bench/Kconfig new file mode 100644 index 000000000..00ab74e14 --- /dev/null +++ b/testing/sd_bench/Kconfig @@ -0,0 +1,40 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config TESTING_SD_BENCH + tristate "SD benchmark program" + depends on ALLOW_BSD_COMPONENTS + default n + ---help--- + SD benchmark application based on https://github.com/PX4/PX4-Autopilot/blob/main/src/systemcmds/sd_bench/sd_bench.cpp + +if TESTING_SD_BENCH + +config TESTING_SD_BENCH_PROGNAME + string "Program name" + default "sdbench" + ---help--- + This is the name of the program that will be used when the NSH ELF + program is installed. + +config TESTING_SD_BENCH_PRIORITY + int "SD bench task priority" + default 100 + +config TESTING_SD_BENCH_STACKSIZE + int "SD bench stack size" + default DEFAULT_TASK_STACKSIZE + +config TESTING_SD_BENCH_DEVICE + string "SD / MMC mount point" + default "/mnt" + +config TESTING_SD_MEM_ALIGN_BYTES + int "Allocated memory alignment" + ---help--- + Specifies the memory alignment (bytes) used for allocated buffers, when enabled with the -a argument. + default 512 + +endif diff --git a/testing/sd_bench/Make.defs b/testing/sd_bench/Make.defs new file mode 100644 index 000000000..8ff52e6f0 --- /dev/null +++ b/testing/sd_bench/Make.defs @@ -0,0 +1,3 @@ +ifneq ($(CONFIG_TESTING_SD_BENCH),) +CONFIGURED_APPS += $(APPDIR)/testing/sd_bench +endif diff --git a/testing/sd_bench/Makefile b/testing/sd_bench/Makefile new file mode 100644 index 000000000..fddcf2ddc --- /dev/null +++ b/testing/sd_bench/Makefile @@ -0,0 +1,10 @@ +include $(APPDIR)/Make.defs + +PROGNAME = $(CONFIG_TESTING_SD_BENCH_PROGNAME) +PRIORITY = $(CONFIG_TESTING_SD_BENCH_PRIORITY) +STACKSIZE = $(CONFIG_TESTING_SD_BENCH_STACKSIZE) +MODULE = $(CONFIG_TESTING_SD_BENCH) + +MAINSRC = sd_bench_main.c + +include $(APPDIR)/Application.mk diff --git a/testing/sd_bench/sd_bench_main.c b/testing/sd_bench/sd_bench_main.c new file mode 100644 index 000000000..7e67fb9de --- /dev/null +++ b/testing/sd_bench/sd_bench_main.c @@ -0,0 +1,503 @@ +/**************************************************************************** + * apps/testing/sd_bench/sd_bench_main.c + * + * Original Licence: + * + * Copyright (c) 2016-2021 PX4 Development Team. 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 PX4 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 OWNER 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. + * + ****************************************************************************/ + +/* Originally ported from PX4 https://github.com/PX4/PX4-Autopilot, + * with the following additions: + * + * - Refactoring for NuttX code style. + * - Test result output has been modified to display total MB written. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +/**************************************************************************** + * Private Definitions + ****************************************************************************/ + +#define BUFFER_ALIGN CONFIG_TESTING_SD_MEM_ALIGN_BYTES +/**************************************************************************** + * Private Types + ****************************************************************************/ + +typedef struct sdb_config +{ + int num_runs; + int run_duration; + bool synchronized; + bool aligned; + size_t total_blocks_written; +} sdb_config_t; + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const char *BENCHMARK_FILE = + CONFIG_TESTING_SD_BENCH_DEVICE "/sd_bench"; + +const size_t max_block = 65536; +const size_t min_block = 1; +const size_t default_block = 512; + +const size_t max_runs = 10000; +const size_t min_runs = 1; +const size_t default_runs = 5; + +const size_t max_duration = 60000; +const size_t min_duration = 1; +const size_t default_duration = 2000; + +const bool default_keep_test = false; +const bool default_fsync = false; +const bool default_verify = true; +const bool default_aligned = false; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static void write_test(int fd, sdb_config_t *cfg, uint8_t *block, + int block_size); +static int read_test(int fd, sdb_config_t *cfg, uint8_t *block, + int block_size); + +static uint64_t time_fsync_us(int fd); +struct timespec get_abs_time(void); +uint64_t get_elapsed_time_us(const struct timespec *start); +uint64_t time_fsync_us(int fd); +float ts_to_kb(uint64_t bytes, uint64_t elapsed); +float block_count_to_mb(size_t blocks, size_t block_size); +static const char *print_bool(const bool value); +static void usage(void); + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +struct timespec get_abs_time(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts; +} + +uint64_t get_time_delta_us(const struct timespec *start, + const struct timespec *end) +{ + uint64_t elapsed; + elapsed = (((uint64_t)end->tv_sec * NSEC_PER_SEC) + end->tv_nsec); + elapsed -= (((uint64_t)start->tv_sec * NSEC_PER_SEC) + start->tv_nsec); + return elapsed / 1000.; +} + +uint64_t get_elapsed_time_us(const struct timespec *start) +{ + struct timespec now = get_abs_time(); + return get_time_delta_us(start, &now); +} + +uint64_t time_fsync_us(int fd) +{ + struct timespec start = get_abs_time(); + fsync(fd); + return get_elapsed_time_us(&start); +} + +float ts_to_kb(uint64_t bytes, uint64_t elapsed) +{ + return (bytes / 1024.) / (elapsed / 1e6); +} + +float block_count_to_mb(size_t blocks, size_t block_size) +{ + return blocks * block_size / (float)(1024 * 1024); +} + +static const char *print_bool(const bool value) +{ + return value ? "true" : "false"; +} + +void write_test(int fd, sdb_config_t *cfg, uint8_t *block, int block_size) +{ + struct timespec start; + struct timespec write_start; + size_t written; + size_t num_blocks; + uint64_t max_write_time; + uint64_t fsync_time; + uint64_t write_time; + uint64_t elapsed; + uint64_t total_elapsed = 0.; + size_t total_blocks = 0; + size_t *blocknumber = (unsigned int *)(void *)&block[0]; + + printf("\n"); + printf("Testing Sequential Write Speed...\n"); + + cfg->total_blocks_written = 0; + + for (int run = 0; run < cfg->num_runs; ++run) + { + start = get_abs_time(); + num_blocks = 0; + max_write_time = 0; + fsync_time = 0; + + while (get_elapsed_time_us(&start) < cfg->run_duration) + { + *blocknumber = total_blocks + num_blocks; + write_start = get_abs_time(); + written = write(fd, block, block_size); + write_time = get_elapsed_time_us(&write_start); + + if (write_time > max_write_time) + { + max_write_time = write_time; + } + + if ((int)written != block_size) + { + printf("Write error: %d\n", errno); + return; + } + + if (cfg->synchronized) + { + fsync_time += time_fsync_us(fd); + } + + ++num_blocks; + } + + /* Note: if testing a slow device (SD Card) and the OS buffers a lot, + * fsync can take really long, and it looks like the process hangs. + * But it does not and the reported result will still be correct. + */ + + if (!cfg->synchronized) + { + fsync_time += time_fsync_us(fd); + } + + elapsed = get_elapsed_time_us(&start); + printf(" Run %2i: %8.1f KB/s, max write time: %4.3f ms (%.1f KB/s), " + "fsync: %4.3f ms\n", run + 1, + ts_to_kb(block_size * num_blocks, elapsed), + max_write_time / 1.e3, + ts_to_kb(block_size, max_write_time), fsync_time / 1e3); + + total_elapsed += elapsed; + total_blocks += num_blocks; + } + + cfg->total_blocks_written = total_blocks; + printf(" Avg : %8.1f KB/s, %3.3f MB written.\n", + ts_to_kb(block_size * total_blocks, total_elapsed), + block_count_to_mb(total_blocks, block_size)); +} + +int read_test(int fd, sdb_config_t *cfg, uint8_t *block, int block_size) +{ + uint8_t *read_block; + uint64_t total_elapsed; + size_t total_blocks; + struct timespec start; + size_t num_blocks; + uint64_t max_read_time; + uint64_t read_time; + uint64_t elapsed; + struct timespec read_start; + size_t nread; + + printf("\n"); + printf("Testing Sequential Read Speed...\n"); + + if (cfg->aligned) + { + read_block = (uint8_t *)memalign(BUFFER_ALIGN, block_size); + } + else + { + read_block = (uint8_t *)malloc(block_size); + } + + if (!read_block) + { + printf("Failed to allocate memory block\n"); + return -1; + } + + total_elapsed = 0.; + total_blocks = 0; + size_t *blocknumber = (unsigned int *)(void *) &read_block[0]; + + for (int run = 0; run < cfg->num_runs; ++run) + { + start = get_abs_time(); + num_blocks = 0; + max_read_time = 0; + + while (get_elapsed_time_us(&start) < cfg->run_duration + && total_blocks + num_blocks < cfg->total_blocks_written) + { + read_start = get_abs_time(); + nread = read(fd, read_block, block_size); + read_time = get_elapsed_time_us(&read_start); + + if (read_time > max_read_time) + { + max_read_time = read_time; + } + + if ((int)nread != block_size) + { + printf("Read error\n"); + free(read_block); + return -1; + } + + if (*blocknumber != total_blocks + num_blocks) + { + printf("Read data error at block: %d wrote:0x%04x read:0x%04x", + (total_blocks + num_blocks), + total_blocks + num_blocks, *blocknumber); + } + + for (unsigned int i = sizeof(*blocknumber); + i < (block_size - sizeof(*blocknumber)); ++i) + { + if (block[i] != read_block[i]) + { + printf("Read data error at offset: %d wrote:0x%02x " + "read:0x%02x", total_blocks + num_blocks + i, + block[i], read_block[i]); + } + } + + ++num_blocks; + } + + elapsed = get_elapsed_time_us(&start); + + if (num_blocks) + { + printf(" Run %2i: %8.1f KB/s, max read/verify time: %3.4f ms " + "(%.1f KB/s)\n", run + 1, + ts_to_kb(block_size * num_blocks, elapsed), + max_read_time / 1e3, + ts_to_kb(block_size, max_read_time)); + + total_elapsed += elapsed; + total_blocks += num_blocks; + } + } + + printf(" Avg : %8.1f KB/s, %3.3f MB and verified\n", + ts_to_kb(block_size * total_blocks, total_elapsed), + block_count_to_mb(total_blocks, block_size)); + + free(read_block); + return 0; +} + +static void usage(void) +{ + printf("Test the speed of an SD card or mount point\n"); + printf(CONFIG_TESTING_SD_BENCH_PROGNAME + ": [-b] [-r] [-d] [-k] [-s] [-a] [-v]\n"); + printf(" -b Block size per write (%u-%u), default %u\n", + min_block, max_block, default_block); + printf(" -r Number of runs (%u-%u), default %u\n", + min_runs, max_runs, default_runs); + printf(" -d Max duration of a test (ms) (%u-%u), default %u\n", + min_duration, max_duration, default_duration); + printf(" -k Keep test file when finished, default %s\n", + print_bool(default_keep_test)); + printf(" -s Call fsync after each block, false calls fsync\n" + " only at the end of each run, default %s\n", + print_bool(default_fsync)); + printf(" -a Test performance on aligned data, default %s\n", + print_bool(default_aligned)); + printf(" -v Verify data and block number, default %s\n", + print_bool(default_verify)); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int main(int argc, char *argv[]) +{ + size_t block_size = default_block; + bool verify = default_verify; + bool keep = default_keep_test; + int ch; + int bench_fd; + sdb_config_t cfg; + uint8_t *block = NULL; + + cfg.synchronized = default_fsync; + cfg.num_runs = default_runs; + cfg.run_duration = default_duration; + cfg.aligned = default_aligned; + + while ((ch = getopt(argc, argv, "b:r:d:ksav")) != EOF) + { + switch (ch) + { + case 'b': + block_size = strtol(optarg, NULL, 0); + break; + + case 'r': + cfg.num_runs = strtol(optarg, NULL, 0); + break; + + case 'd': + cfg.run_duration = strtol(optarg, NULL, 0); + break; + + case 'k': + keep = !default_keep_test; + break; + + case 's': + cfg.synchronized = !default_fsync; + break; + + case 'a': + cfg.aligned = !default_aligned; + break; + + case 'v': + verify = !default_verify; + break; + + default: + usage(); + return -1; + break; + } + } + + if (cfg.run_duration > max_duration || cfg.run_duration < min_duration) + { + printf("Duration outside of allowable range.\n"); + usage(); + exit(EXIT_FAILURE); + } + + if (block_size > max_block || block_size < min_block) + { + printf("Bytes outside allowable range.\n"); + usage(); + exit(EXIT_FAILURE); + } + + if (cfg.num_runs > max_runs || cfg.num_runs < min_runs) + { + printf("Runs outside allowable range.\n"); + usage(); + exit(EXIT_FAILURE); + } + + cfg.run_duration *= 1000; + bench_fd = open(BENCHMARK_FILE, + O_CREAT | (verify ? O_RDWR : O_WRONLY) | O_TRUNC); + + if (bench_fd < 0) + { + printf("Can't open benchmark file %s (%d)\n", + BENCHMARK_FILE, bench_fd); + exit(EXIT_FAILURE); + } + + if (cfg.aligned) + { + block = (uint8_t *)memalign(BUFFER_ALIGN, block_size); + } + else + { + block = (uint8_t *)malloc(block_size); + } + + if (!block) + { + printf("Failed to allocate memory block\n"); + close(bench_fd); + exit(EXIT_FAILURE); + } + + for (int j = 0; j < block_size; ++j) + { + block[j] = (uint8_t)j; + } + + printf("Using block size = %u bytes, sync = %s\n", block_size, + print_bool(cfg.synchronized)); + + write_test(bench_fd, &cfg, block, block_size); + + if (verify) + { + fsync(bench_fd); + lseek(bench_fd, 0, SEEK_SET); + read_test(bench_fd, &cfg, block, block_size); + } + + free(block); + close(bench_fd); + + if (!keep) + { + unlink(BENCHMARK_FILE); + } + + return 0; +}