| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <atomic> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <unistd.h> |
| |
| #include <fuchsia/hardware/block/c/fidl.h> |
| #include <lib/fit/defer.h> |
| #include <lib/fzl/fdio.h> |
| #include <lib/sync/completion.h> |
| #include <lib/zircon-internal/xorshiftrand.h> |
| #include <perftest/results.h> |
| #include <zircon/device/block.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/time.h> |
| #include <zircon/types.h> |
| |
| static uint64_t number(const char* str) { |
| char* end; |
| uint64_t n = strtoull(str, &end, 10); |
| |
| uint64_t m = 1; |
| switch (*end) { |
| case 'G': |
| case 'g': |
| m = 1024 * 1024 * 1024; |
| break; |
| case 'M': |
| case 'm': |
| m = 1024 * 1024; |
| break; |
| case 'K': |
| case 'k': |
| m = 1024; |
| break; |
| } |
| return m * n; |
| } |
| |
| static void bytes_per_second(uint64_t bytes, uint64_t nanos) { |
| double s = ((double)nanos) / ((double)1000000000); |
| double rate = ((double)bytes) / s; |
| |
| const char* unit = "B"; |
| if (rate > 1024 * 1024) { |
| unit = "MB"; |
| rate /= 1024 * 1024; |
| } else if (rate > 1024) { |
| unit = "KB"; |
| rate /= 1024; |
| } |
| fprintf(stderr, "%g %s/s\n", rate, unit); |
| } |
| |
| static void ops_per_second(uint64_t count, uint64_t nanos) { |
| double s = ((double)nanos) / ((double)1000000000); |
| double rate = ((double)count) / s; |
| fprintf(stderr, "%g %s/s\n", rate, "ops"); |
| } |
| |
| typedef struct { |
| int fd; |
| zx_handle_t vmo; |
| zx_handle_t fifo; |
| reqid_t reqid; |
| fuchsia_hardware_block_VmoID vmoid; |
| size_t bufsz; |
| fuchsia_hardware_block_BlockInfo info; |
| } blkdev_t; |
| |
| static void blkdev_close(blkdev_t* blk) { |
| if (blk->fd >= 0) { |
| close(blk->fd); |
| } |
| zx_handle_close(blk->vmo); |
| zx_handle_close(blk->fifo); |
| memset(blk, 0, sizeof(blkdev_t)); |
| blk->fd = -1; |
| } |
| |
| static zx_status_t blkdev_open(int fd, const char* dev, size_t bufsz, blkdev_t* blk) { |
| memset(blk, 0, sizeof(blkdev_t)); |
| blk->fd = fd; |
| blk->bufsz = bufsz; |
| |
| auto cleanup = fit::defer([blk]() { blkdev_close(blk); }); |
| |
| fzl::UnownedFdioCaller caller(fd); |
| zx_status_t status; |
| zx_status_t io_status = |
| fuchsia_hardware_block_BlockGetInfo(caller.borrow_channel(), &status, &blk->info); |
| if (io_status != ZX_OK || status != ZX_OK) { |
| fprintf(stderr, "error: cannot get block device info for '%s'\n", dev); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| io_status = fuchsia_hardware_block_BlockGetFifo(caller.borrow_channel(), &status, &blk->fifo); |
| if (io_status != ZX_OK || status != ZX_OK) { |
| fprintf(stderr, "error: cannot get fifo for '%s'\n", dev); |
| return ZX_ERR_INTERNAL; |
| } |
| if ((status = zx_vmo_create(bufsz, 0, &blk->vmo)) != ZX_OK) { |
| fprintf(stderr, "error: out of memory %d\n", status); |
| return status; |
| } |
| |
| zx_handle_t dup; |
| if ((status = zx_handle_duplicate(blk->vmo, ZX_RIGHT_SAME_RIGHTS, &dup)) != ZX_OK) { |
| fprintf(stderr, "error: cannot duplicate handle %d\n", status); |
| return status; |
| } |
| |
| io_status = |
| fuchsia_hardware_block_BlockAttachVmo(caller.borrow_channel(), dup, &status, &blk->vmoid); |
| if (io_status != ZX_OK || status != ZX_OK) { |
| fprintf(stderr, "error: cannot attach vmo for '%s'\n", dev); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| cleanup.cancel(); |
| return ZX_OK; |
| } |
| |
| typedef struct { |
| blkdev_t* blk; |
| size_t count; |
| size_t xfer; |
| uint64_t seed; |
| int max_pending; |
| bool write; |
| bool linear; |
| |
| std::atomic<int> pending; |
| sync_completion_t signal; |
| } bio_random_args_t; |
| |
| std::atomic<reqid_t> next_reqid(0); |
| |
| static int bio_random_thread(void* arg) { |
| auto* a = reinterpret_cast<bio_random_args_t*>(arg); |
| |
| size_t off = 0; |
| size_t count = a->count; |
| size_t xfer = a->xfer; |
| |
| size_t blksize = a->blk->info.block_size; |
| size_t blkcount = ((count * xfer) / blksize) - (xfer / blksize); |
| |
| rand64_t r64 = RAND63SEED(a->seed); |
| |
| zx_handle_t fifo = a->blk->fifo; |
| size_t dev_off = 0; |
| |
| while (count > 0) { |
| while (a->pending.load() == a->max_pending) { |
| sync_completion_wait(&a->signal, ZX_TIME_INFINITE); |
| sync_completion_reset(&a->signal); |
| } |
| |
| block_fifo_request_t req = {}; |
| req.reqid = next_reqid.fetch_add(1); |
| req.vmoid = a->blk->vmoid.id; |
| req.opcode = a->write ? BLOCKIO_WRITE : BLOCKIO_READ; |
| req.length = static_cast<uint32_t>(xfer); |
| req.vmo_offset = off; |
| |
| if (a->linear) { |
| req.dev_offset = dev_off; |
| dev_off += xfer; |
| } else { |
| req.dev_offset = (rand64(&r64) % blkcount) * blksize; |
| } |
| off += xfer; |
| if ((off + xfer) > a->blk->bufsz) { |
| off = 0; |
| } |
| |
| req.length /= static_cast<uint32_t>(blksize); |
| req.dev_offset /= blksize; |
| req.vmo_offset /= blksize; |
| |
| #if 0 |
| fprintf(stderr, "IO tid=%u vid=%u op=%x len=%zu vof=%zu dof=%zu\n", |
| req.reqid, req.vmoid.id, req.opcode, req.length, req.vmo_offset, req.dev_offset); |
| #endif |
| zx_status_t r = zx_fifo_write(fifo, sizeof(req), &req, 1, NULL); |
| if (r == ZX_ERR_SHOULD_WAIT) { |
| r = zx_object_wait_one(fifo, ZX_FIFO_WRITABLE | ZX_FIFO_PEER_CLOSED, ZX_TIME_INFINITE, NULL); |
| if (r != ZX_OK) { |
| fprintf(stderr, "failed waiting for fifo\n"); |
| zx_handle_close(fifo); |
| return -1; |
| } |
| continue; |
| } else if (r < 0) { |
| fprintf(stderr, "error: failed writing fifo\n"); |
| zx_handle_close(fifo); |
| return -1; |
| } |
| |
| a->pending.fetch_add(1); |
| count--; |
| } |
| return 0; |
| } |
| |
| static zx_status_t bio_random(bio_random_args_t* a, uint64_t* _total, zx_duration_t* _res) { |
| thrd_t t; |
| int r; |
| |
| size_t count = a->count; |
| zx_handle_t fifo = a->blk->fifo; |
| |
| zx_time_t t0 = zx_clock_get_monotonic(); |
| thrd_create(&t, bio_random_thread, a); |
| |
| auto cleanup = fit::defer([&a, &t]() { |
| int r; |
| zx_handle_close(a->blk->fifo); |
| thrd_join(t, &r); |
| }); |
| |
| while (count > 0) { |
| block_fifo_response_t resp; |
| zx_status_t r = zx_fifo_read(fifo, sizeof(resp), &resp, 1, NULL); |
| if (r == ZX_ERR_SHOULD_WAIT) { |
| r = zx_object_wait_one(fifo, ZX_FIFO_READABLE | ZX_FIFO_PEER_CLOSED, ZX_TIME_INFINITE, NULL); |
| if (r != ZX_OK) { |
| fprintf(stderr, "failed waiting for fifo: %d\n", r); |
| return r; |
| } |
| continue; |
| } else if (r < 0) { |
| fprintf(stderr, "error: failed reading fifo: %d\n", r); |
| return r; |
| } |
| if (resp.status != ZX_OK) { |
| fprintf(stderr, "error: io txn failed %d (%zu remaining)\n", resp.status, count); |
| return resp.status; |
| } |
| count--; |
| if (a->pending.fetch_sub(1) == a->max_pending) { |
| sync_completion_signal(&a->signal); |
| } |
| } |
| |
| cleanup.cancel(); |
| |
| zx_time_t t1; |
| t1 = zx_clock_get_monotonic(); |
| |
| fprintf(stderr, "waiting for thread to exit...\n"); |
| thrd_join(t, &r); |
| |
| *_res = zx_time_sub_time(t1, t0); |
| *_total = a->count * a->xfer; |
| return ZX_OK; |
| } |
| |
| void usage(void) { |
| fprintf(stderr, |
| "usage: biotime <option>* <device>\n" |
| "\n" |
| "args: -bs <num> transfer block size (multiple of 4K)\n" |
| " -total-bytes-to-transfer <num> total amount to read or write\n" |
| " -iter <num-iterations> total number of iterations (0 stands for infinite)\n" |
| " -mo <num> maximum outstanding ops (1..128)\n" |
| " -read test reading from the block device (default)\n" |
| " -write test writing to the block device\n" |
| " -live-dangerously required if using \"-write\"\n" |
| " -linear transfers in linear order (default)\n" |
| " -random random transfers across total range\n" |
| " -output-file <filename> destination file for " |
| "writing results in JSON format\n"); |
| } |
| |
| #define needparam() \ |
| do { \ |
| argc--; \ |
| argv++; \ |
| if (argc == 0) { \ |
| fprintf(stderr, "error: option %s needs a parameter\n", argv[-1]); \ |
| return -1; \ |
| } \ |
| } while (0) |
| #define nextarg() \ |
| do { \ |
| argc--; \ |
| argv++; \ |
| } while (0) |
| #define error(x...) \ |
| do { \ |
| fprintf(stderr, x); \ |
| usage(); \ |
| return -1; \ |
| } while (0) |
| |
| int main(int argc, char** argv) { |
| blkdev_t blk; |
| |
| bool live_dangerously = false; |
| bio_random_args_t a = {}; |
| bool opt_write = false; |
| bool opt_linear = true; |
| int opt_max_pending = 128; |
| size_t opt_xfer_size = 32768; |
| uint64_t opt_num_iter = 1; |
| bool loop_forever = false; |
| |
| a.blk = &blk; |
| a.seed = 7891263897612ULL; |
| const char* output_file = nullptr; |
| |
| size_t total = 0; |
| |
| nextarg(); |
| while (argc > 0) { |
| if (argv[0][0] != '-') { |
| break; |
| } |
| if (!strcmp(argv[0], "-bs")) { |
| needparam(); |
| opt_xfer_size = number(argv[0]); |
| if ((opt_xfer_size == 0) || (opt_xfer_size % 4096)) { |
| error("error: block size must be multiple of 4K\n"); |
| } |
| } else if (!strcmp(argv[0], "-total-bytes-to-transfer")) { |
| needparam(); |
| total = number(argv[0]); |
| } else if (!strcmp(argv[0], "-mo")) { |
| needparam(); |
| size_t n = number(argv[0]); |
| if ((n < 1) || (n > 128)) { |
| error("error: max pending must be between 1 and 128\n"); |
| } |
| opt_max_pending = static_cast<int>(n); |
| } else if (!strcmp(argv[0], "-read")) { |
| opt_write = false; |
| } else if (!strcmp(argv[0], "-write")) { |
| opt_write = true; |
| } else if (!strcmp(argv[0], "-live-dangerously")) { |
| live_dangerously = true; |
| } else if (!strcmp(argv[0], "-linear")) { |
| opt_linear = true; |
| } else if (!strcmp(argv[0], "-random")) { |
| opt_linear = false; |
| } else if (!strcmp(argv[0], "-output-file")) { |
| needparam(); |
| output_file = argv[0]; |
| } else if (!strcmp(argv[0], "-h")) { |
| usage(); |
| return 0; |
| } else if (!strcmp(argv[0], "-iter")) { |
| needparam(); |
| opt_num_iter = strtoull(argv[0], NULL, 10); |
| if (opt_num_iter == 0) |
| loop_forever = true; |
| } else { |
| error("error: unknown option: %s\n", argv[0]); |
| } |
| nextarg(); |
| } |
| if (argc == 0) { |
| error("error: no device specified\n"); |
| } |
| if (argc > 1) { |
| error("error: unexpected arguments\n"); |
| } |
| if (a.write && !live_dangerously) { |
| error( |
| "error: the option \"-live-dangerously\" is required when using" |
| " \"-write\"\n"); |
| } |
| const char* device_filename = argv[0]; |
| |
| do { |
| int fd; |
| a.xfer = opt_xfer_size; |
| a.max_pending = opt_max_pending; |
| a.write = opt_write; |
| a.linear = opt_linear; |
| if ((fd = open(device_filename, O_RDONLY)) < 0) { |
| fprintf(stderr, "error: cannot open '%s'\n", device_filename); |
| return -1; |
| } |
| if (blkdev_open(fd, device_filename, 8 * 1024 * 1024, &blk) != ZX_OK) { |
| return -1; |
| } |
| |
| size_t devtotal = blk.info.block_count * blk.info.block_size; |
| |
| // default to entire device |
| if ((total == 0) || (total > devtotal)) { |
| total = devtotal; |
| } |
| a.count = total / a.xfer; |
| |
| zx_duration_t res = 0; |
| total = 0; |
| if (bio_random(&a, &total, &res) != ZX_OK) { |
| return -1; |
| } |
| |
| fprintf(stderr, "%zu bytes in %zu ns: ", total, res); |
| bytes_per_second(total, res); |
| fprintf(stderr, "%zu ops in %zu ns: ", a.count, res); |
| ops_per_second(a.count, res); |
| |
| if (output_file) { |
| perftest::ResultsSet results; |
| auto* test_case = |
| results.AddTestCase("fuchsia.zircon", "BlockDeviceThroughput", "bytes/second"); |
| double time_in_seconds = static_cast<double>(res) / 1e9; |
| test_case->AppendValue(static_cast<double>(total) / time_in_seconds); |
| if (!results.WriteJSONFile(output_file)) { |
| return 1; |
| } |
| } |
| blkdev_close(&blk); |
| } while (loop_forever || (--opt_num_iter > 0)); |
| |
| return 0; |
| } |