fcoin/bench/blockstore.js
2019-04-08 13:18:55 -07:00

477 lines
12 KiB
JavaScript

/*!
* bench/blockstore.js - benchmark blockstore 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=<bytes>] [--total=<bytes>]
* [--location=<path>] [--store=<name>]
* [--output=<name>] [--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", "bench" or "benchjson".
* - `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' || a === 'benchjson'),
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 === 'benchjson') {
output = new BenchJSONOutput();
} 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 = hrToMicro(since);
const emicro = hrToMicro(elapsed);
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 = hrToMicro(elapsed);
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 cal = calculate(this.results[type][length]);
this.stdout.write(`${format(length)}`);
this.stdout.write(`${format(cal.operations.toString())}`);
this.stdout.write(`${format(cal.min)}`);
this.stdout.write(`${format(cal.max)}`);
this.stdout.write(`${format(cal.average)}`);
this.stdout.write(`${format(cal.median)}`);
this.stdout.write('\n');
}
this.stdout.write('\n');
}
this.stdout.write('\n');
}
}
class BenchJSONOutput {
constructor() {
this.time = null;
this.results = {};
this.stdout = process.stdout;
}
start() {
this.time = process.hrtime();
}
result(type, start, elapsed, length) {
const micro = hrToMicro(elapsed);
if (!this.results[type])
this.results[type] = {};
if (!this.results[type][length])
this.results[type][length] = [];
this.results[type][length].push(micro);
}
end() {
const report = {
summary: [],
time: hrToMicro(process.hrtime(this.time)),
elapsed: 0
};
for (const type in this.results) {
for (const length in this.results[type]) {
const cal = calculate(this.results[type][length]);
report.elapsed += cal.total;
report.summary.push({
type: type,
length: length,
operations: cal.operations,
min: cal.min,
max: cal.max,
average: cal.average,
median: cal.median
});
}
}
this.stdout.write(JSON.stringify(report, null, 2));
this.stdout.write('\n');
}
}
function hrToMicro(time) {
return (time[0] * 1000000) + (time[1] / 1000);
}
function calculate(times) {
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];
const operations = times.length;
return {
total,
operations,
min,
max,
average,
median
};
}
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;
}