From 0609ce72fc76893563ffc21c60d9606c82c00498 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Wed, 20 Feb 2019 18:37:37 -0800 Subject: [PATCH] bench: add benchmarks for blockstore --- bench/blockstore.js | 404 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 bench/blockstore.js diff --git a/bench/blockstore.js b/bench/blockstore.js new file mode 100644 index 00000000..a068cc9e --- /dev/null +++ b/bench/blockstore.js @@ -0,0 +1,404 @@ +/*! + * bench/blockstore.js - benchmark block store for bcoin + * + * This can be run to benchmark the performance of the blockstore + * module for writing, reading and pruning block data. Results are + * written to stdout as JSON or formated bench results. + * + * Usage: + * node ./blockstore.js [--maxfile=] [--total=] + [--location=] [--store=] [--unsafe] + * + * Options: + * - `maxfile` The maximum file size (applies to "file" store). + * - `total` The total number of block bytes to write. + * - `location` The location to store block data. + * - `store` This can be "file" or "level". + * - `output` This can be "json" or "bench". + * - `unsafe` This will allocate block data directly from memory + * instead of random, it is faster. + * + * Copyright (c) 2019, Braydon Fuller (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +process.title = 'blockstore-bench'; + +const {isAbsolute} = require('path'); +const {mkdirp} = require('bfile'); +const random = require('bcrypto/lib/random'); +const {BufferMap} = require('buffer-map'); + +const { + FileBlockStore, + LevelBlockStore +} = require('../lib/blockstore'); + +const config = { + 'maxfile': { + value: true, + parse: a => parseInt(a), + valid: a => Number.isSafeInteger(a), + fallback: 128 * 1024 * 1024 + }, + 'total': { + value: true, + parse: a => parseInt(a), + valid: a => Number.isSafeInteger(a), + fallback: 3 * 1024 * 1024 * 1024 + }, + 'location': { + value: true, + valid: a => isAbsolute(a), + fallback: '/tmp/bcoin-bench-blockstore' + }, + 'store': { + value: true, + valid: a => (a === 'file' || a === 'level'), + fallback: 'file' + }, + 'output': { + value: true, + valid: a => (a === 'json' || a === 'bench'), + fallback: 'bench' + }, + 'unsafe': { + value: false, + valid: a => (a === true || a === false), + fallback: false + } +}; + +/** + * These block sizes were generated from bitcoin mainnet blocks by putting + * sizes into bins of 256 ^ (2 * n) as the upper bound and calculating + * the percentage of each and then distributing to roughly match the + * percentage of the following: + * + * |-------------|------------| + * | percentage | bytes | + * |-------------|------------| + * | 23.4055 | 1048576 | + * | 15.5338 | 256 | + * | 12.2182 | 262144 | + * | 8.4079 | 524288 | + * | 7.1289 | 131072 | + * | 6.9197 | 65536 | + * | 6.7073 | 2097152 | + * | 4.6753 | 32768 | + * | 3.9695 | 4096 | + * | 3.3885 | 16384 | + * | 2.6526 | 8192 | + * | 2.0048 | 512 | + * | 1.587 | 1024 | + * | 1.3976 | 2048 | + * | 0.0032 | 4194304 | + * |-------------|------------| + */ + +const distribution = [ + 1048576, 256, 256, 524288, 262144, 256, 131072, 256, 524288, 256, 131072, + 1048576, 262144, 1048576, 2097152, 256, 1048576, 65536, 256, 262144, 8192, + 32768, 32768, 256, 1048576, 524288, 2097152, 1024, 1048576, 1048576, 131072, + 131072, 262144, 512, 1048576, 1048576, 1024, 1048576, 1048576, 262144, 2048, + 262144, 256, 1048576, 131072, 4096, 524288, 65536, 4096, 65536, 131072, + 2097152, 2097152, 2097152, 256, 524288, 4096, 262144, 65536, 65536, 262144, + 16384, 1048576, 32768, 262144, 1048576, 256, 131072, 1048576, 1048576, + 1048576, 8192, 1048576, 256, 16384, 1048576, 256, 256, 524288, 256, 32768, + 16384, 32768, 1048576, 512, 4096, 1048576, 1048576, 524288, 65536, 2097152, + 512, 262144, 8192, 524288, 131072, 65536, 16384, 2048, 262144, 1048576, + 1048576, 256, 524288, 262144, 4194304, 262144, 2097152 +]; + +(async () => { + let settings = null; + try { + settings = processArgs(process.argv, config); + } catch (err) { + console.log(err.message); + process.exit(1); + } + + await mkdirp(settings.location); + + let store = null; + let output = null; + + if (settings.store === 'file') { + store = new FileBlockStore({ + location: settings.location, + maxFileLength: settings.maxfile + }); + } else if (settings.store === 'level') { + store = new LevelBlockStore({ + location: settings.location + }); + } + + if (settings.output === 'bench') { + output = new BenchOutput(); + } else if (settings.output === 'json') { + output = new JSONOutput(); + } + + await store.open(); + + const hashes = []; + const lengths = new BufferMap(); + + output.start(); + + // 1. Write data to the block store + let written = 0; + + async function write() { + for (const length of distribution) { + const hash = random.randomBytes(32); + let raw = null; + if (settings.unsafe) { + raw = Buffer.allocUnsafe(length); + } else { + raw = random.randomBytes(length); + } + + const start = process.hrtime(); + await store.write(hash, raw); + const elapsed = process.hrtime(start); + + hashes.push(hash); + lengths.set(hash, length); + written += length; + + output.result('write', start, elapsed, length); + + if (written >= settings.total) + break; + } + } + + while (written < settings.total) + await write(); + + // 2. Read data from the block store + for (const hash of hashes) { + const start = process.hrtime(); + const raw = await store.read(hash); + const elapsed = process.hrtime(start); + + output.result('read', start, elapsed, raw.length); + } + + // 3. Read data not in the order it was written (random) + for (let i = 0; i < hashes.length; i++) { + const rand = random.randomInt() / 0xffffffff * (hashes.length - 1) | 0; + const hash = hashes[rand]; + + const start = process.hrtime(); + const raw = await store.read(hash); + const elapsed = process.hrtime(start); + + output.result('randomread', start, elapsed, raw.length); + } + + // 4. Prune data from the block store + for (const hash of hashes) { + const start = process.hrtime(); + await store.prune(hash); + const elapsed = process.hrtime(start); + const length = lengths.get(hash); + + output.result('prune', start, elapsed, length); + } + + output.end(); + + await store.close(); +})().catch((err) => { + console.error(err); + process.exit(1); +}); + +class JSONOutput { + constructor() { + this.time = process.hrtime(); + this.index = 0; + } + + start() { + process.stdout.write('['); + } + + result(type, start, elapsed, length) { + if (this.index > 0) + process.stdout.write(','); + + const since = [start[0] - this.time[0], start[1] - this.time[1]]; + const smicro = (since[0] * 1000000) + (since[1] / 1000); + const emicro = (elapsed[0] * 1000000) + (elapsed[1] / 1000); + + process.stdout.write(`{"type":"${type}","start":${smicro},`); + process.stdout.write(`"elapsed":${emicro},"length":${length},`); + process.stdout.write(`"index":${this.index}}`); + + this.index += 1; + } + + end() { + process.stdout.write(']'); + } +} + +class BenchOutput { + constructor() { + this.time = process.hrtime(); + this.index = 0; + this.results = {}; + this.interval = null; + this.stdout = process.stdout; + } + + start() { + this.stdout.write('Starting benchmark...\n'); + this.interval = setInterval(() => { + this.stdout.write(`Operation count=${this.index}\n`); + }, 5000); + } + + result(type, start, elapsed, length) { + const micro = (elapsed[0] * 1000000) + (elapsed[1] / 1000); + + if (!this.results[type]) + this.results[type] = {}; + + if (!this.results[type][length]) + this.results[type][length] = []; + + this.results[type][length].push(micro); + + this.index += 1; + } + + end() { + clearInterval(this.interval); + + this.stdout.write('Benchmark finished.\n'); + + function format(value) { + if (typeof value === 'number') + value = value.toFixed(2); + + if (typeof value !== 'string') + value = value.toString(); + + while (value.length < 15) + value = `${value} `; + + return value; + } + + function title(value) { + if (typeof value !== 'string') + value = value.toString(); + + while (value.length < 85) + value = ` ${value} `; + + if (value.length > 85) + value = value.slice(0, 85); + + return value; + } + + for (const type in this.results) { + this.stdout.write('\n'); + this.stdout.write(`${title(type)}\n`); + this.stdout.write(`${'='.repeat(85)}\n`); + this.stdout.write(`${format('length')}`); + this.stdout.write(`${format('operations')}`); + this.stdout.write(`${format('min')}`); + this.stdout.write(`${format('max')}`); + this.stdout.write(`${format('average')}`); + this.stdout.write(`${format('median')}`); + this.stdout.write('\n'); + this.stdout.write(`${'-'.repeat(85)}\n`); + + for (const length in this.results[type]) { + const times = this.results[type][length]; + + times.sort((a, b) => a - b); + + let min = Infinity; + let max = 0; + + let total = 0; + + for (const micro of times) { + if (micro < min) + min = micro; + + if (micro > max) + max = micro; + + total += micro; + } + + const average = total / times.length; + const median = times[times.length / 2 | 0]; + + this.stdout.write(`${format(length)}`); + this.stdout.write(`${format(times.length.toString())}`); + this.stdout.write(`${format(min)}`); + this.stdout.write(`${format(max)}`); + this.stdout.write(`${format(average)}`); + this.stdout.write(`${format(median)}`); + this.stdout.write('\n'); + } + this.stdout.write('\n'); + } + this.stdout.write('\n'); + } +} + +function processArgs(argv, config) { + const args = {}; + + for (const key in config) + args[key] = config[key].fallback; + + for (let i = 2; i < process.argv.length; i++) { + const arg = process.argv[i]; + const match = arg.match(/^(\-){1,2}([a-z]+)(\=)?(.*)?$/); + + if (!match) { + throw new Error(`Unexpected argument: ${arg}.`); + } else { + const key = match[2]; + let value = match[4]; + + if (!config[key]) + throw new Error(`Invalid argument: ${arg}.`); + + if (config[key].value && !value) { + value = process.argv[i + 1]; + i++; + } else if (!config[key].value && !value) { + value = true; + } else if (!config[key].value && value) { + throw new Error(`Unexpected value: ${key}=${value}`); + } + + if (config[key].parse) + value = config[key].parse(value); + + if (value) + args[key] = value; + + if (!config[key].valid(args[key])) + throw new Error(`Invalid value: ${key}=${value}`); + } + } + + return args; +}