diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 00000000..8ae20415 --- /dev/null +++ b/benchmark/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/fixtures diff --git a/benchmark/create-fixture.js b/benchmark/create-fixture.js new file mode 100644 index 00000000..69f67779 --- /dev/null +++ b/benchmark/create-fixture.js @@ -0,0 +1,24 @@ +const { writeFile: writeFile_ } = require('fs') +const writeFile = async (path, data) => new Promise((res, rej) => + writeFile_(path, data, er => er ? rej(er) : res())) +const mkdirp = require('mkdirp') +const { resolve } = require('path') + +const create = async (path, start, end, maxDepth, depth = 0) => { + await mkdirp(path) + const promises = [] + for (let i = start; i <= end; i++) { + const c = String.fromCharCode(i) + if (depth < maxDepth && (i-start >= depth)) + await create(resolve(path, c), start, end, maxDepth, depth + 1) + else + promises.push(writeFile(resolve(path, c), c)) + } + await Promise.all(promises) + return path +} + +module.exports = async ({ start, end, depth, name }) => { + const path = resolve(__dirname, 'fixtures', name, 'test') + return await create(path, start.charCodeAt(0), end.charCodeAt(0), depth) +} diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 00000000..90928a20 --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,18 @@ +const cases = require('./rimrafs.js') +const runTest = require('./run-test.js') +const print = require('./print-results.js') + +const rimraf = require('../') +const main = async () => { + // cleanup first. since the windows impl works on all platforms, + // use that. it's only relevant if the folder exists anyway. + rimraf.sync(__dirname + '/fixtures') + const results = {} + for (const name of Object.keys(cases)) { + results[name] = await runTest(name) + } + rimraf.sync(__dirname + '/fixtures') + return results +} + +main().then(print) diff --git a/benchmark/old-rimraf/CHANGELOG.md b/benchmark/old-rimraf/CHANGELOG.md new file mode 100644 index 00000000..f116f141 --- /dev/null +++ b/benchmark/old-rimraf/CHANGELOG.md @@ -0,0 +1,65 @@ +# v3.0 + +- Add `--preserve-root` option to executable (default true) +- Drop support for Node.js below version 6 + +# v2.7 + +- Make `glob` an optional dependency + +# 2.6 + +- Retry on EBUSY on non-windows platforms as well +- Make `rimraf.sync` 10000% more reliable on Windows + +# 2.5 + +- Handle Windows EPERM when lstat-ing read-only dirs +- Add glob option to pass options to glob + +# 2.4 + +- Add EPERM to delay/retry loop +- Add `disableGlob` option + +# 2.3 + +- Make maxBusyTries and emfileWait configurable +- Handle weird SunOS unlink-dir issue +- Glob the CLI arg for better Windows support + +# 2.2 + +- Handle ENOENT properly on Windows +- Allow overriding fs methods +- Treat EPERM as indicative of non-empty dir +- Remove optional graceful-fs dep +- Consistently return null error instead of undefined on success +- win32: Treat ENOTEMPTY the same as EBUSY +- Add `rimraf` binary + +# 2.1 + +- Fix SunOS error code for a non-empty directory +- Try rmdir before readdir +- Treat EISDIR like EPERM +- Remove chmod +- Remove lstat polyfill, node 0.7 is not supported + +# 2.0 + +- Fix myGid call to check process.getgid +- Simplify the EBUSY backoff logic. +- Use fs.lstat in node >= 0.7.9 +- Remove gently option +- remove fiber implementation +- Delete files that are marked read-only + +# 1.0 + +- Allow ENOENT in sync method +- Throw when no callback is provided +- Make opts.gently an absolute path +- use 'stat' if 'lstat' is not available +- Consistent error naming, and rethrow non-ENOENT stat errors +- add fiber implementation diff --git a/benchmark/old-rimraf/LICENSE b/benchmark/old-rimraf/LICENSE new file mode 100644 index 00000000..19129e31 --- /dev/null +++ b/benchmark/old-rimraf/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/benchmark/old-rimraf/README.md b/benchmark/old-rimraf/README.md new file mode 100644 index 00000000..423b8cf8 --- /dev/null +++ b/benchmark/old-rimraf/README.md @@ -0,0 +1,101 @@ +[![Build Status](https://travis-ci.org/isaacs/rimraf.svg?branch=master)](https://travis-ci.org/isaacs/rimraf) [![Dependency Status](https://david-dm.org/isaacs/rimraf.svg)](https://david-dm.org/isaacs/rimraf) [![devDependency Status](https://david-dm.org/isaacs/rimraf/dev-status.svg)](https://david-dm.org/isaacs/rimraf#info=devDependencies) + +The [UNIX command](http://en.wikipedia.org/wiki/Rm_(Unix)) `rm -rf` for node. + +Install with `npm install rimraf`, or just drop rimraf.js somewhere. + +## API + +`rimraf(f, [opts], callback)` + +The first parameter will be interpreted as a globbing pattern for files. If you +want to disable globbing you can do so with `opts.disableGlob` (defaults to +`false`). This might be handy, for instance, if you have filenames that contain +globbing wildcard characters. + +The callback will be called with an error if there is one. Certain +errors are handled for you: + +* Windows: `EBUSY` and `ENOTEMPTY` - rimraf will back off a maximum of + `opts.maxBusyTries` times before giving up, adding 100ms of wait + between each attempt. The default `maxBusyTries` is 3. +* `ENOENT` - If the file doesn't exist, rimraf will return + successfully, since your desired outcome is already the case. +* `EMFILE` - Since `readdir` requires opening a file descriptor, it's + possible to hit `EMFILE` if too many file descriptors are in use. + In the sync case, there's nothing to be done for this. But in the + async case, rimraf will gradually back off with timeouts up to + `opts.emfileWait` ms, which defaults to 1000. + +## options + +* unlink, chmod, stat, lstat, rmdir, readdir, + unlinkSync, chmodSync, statSync, lstatSync, rmdirSync, readdirSync + + In order to use a custom file system library, you can override + specific fs functions on the options object. + + If any of these functions are present on the options object, then + the supplied function will be used instead of the default fs + method. + + Sync methods are only relevant for `rimraf.sync()`, of course. + + For example: + + ```javascript + var myCustomFS = require('some-custom-fs') + + rimraf('some-thing', myCustomFS, callback) + ``` + +* maxBusyTries + + If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error code is encountered + on Windows systems, then rimraf will retry with a linear backoff + wait of 100ms longer on each try. The default maxBusyTries is 3. + + Only relevant for async usage. + +* emfileWait + + If an `EMFILE` error is encountered, then rimraf will retry + repeatedly with a linear backoff of 1ms longer on each try, until + the timeout counter hits this max. The default limit is 1000. + + If you repeatedly encounter `EMFILE` errors, then consider using + [graceful-fs](http://npm.im/graceful-fs) in your program. + + Only relevant for async usage. + +* glob + + Set to `false` to disable [glob](http://npm.im/glob) pattern + matching. + + Set to an object to pass options to the glob module. The default + glob options are `{ nosort: true, silent: true }`. + + Glob version 6 is used in this module. + + Relevant for both sync and async usage. + +* disableGlob + + Set to any non-falsey value to disable globbing entirely. + (Equivalent to setting `glob: false`.) + +## rimraf.sync + +It can remove stuff synchronously, too. But that's not so good. Use +the async API. It's better. + +## CLI + +If installed with `npm install rimraf -g` it can be used as a global +command `rimraf [ ...]` which is useful for cross platform support. + +## mkdirp + +If you need to create a directory recursively, check out +[mkdirp](https://github.com/substack/node-mkdirp). diff --git a/benchmark/old-rimraf/bin.js b/benchmark/old-rimraf/bin.js new file mode 100755 index 00000000..023814cc --- /dev/null +++ b/benchmark/old-rimraf/bin.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +const rimraf = require('./') + +const path = require('path') + +const isRoot = arg => /^(\/|[a-zA-Z]:\\)$/.test(path.resolve(arg)) +const filterOutRoot = arg => { + const ok = preserveRoot === false || !isRoot(arg) + if (!ok) { + console.error(`refusing to remove ${arg}`) + console.error('Set --no-preserve-root to allow this') + } + return ok +} + +let help = false +let dashdash = false +let noglob = false +let preserveRoot = true +const args = process.argv.slice(2).filter(arg => { + if (dashdash) + return !!arg + else if (arg === '--') + dashdash = true + else if (arg === '--no-glob' || arg === '-G') + noglob = true + else if (arg === '--glob' || arg === '-g') + noglob = false + else if (arg.match(/^(-+|\/)(h(elp)?|\?)$/)) + help = true + else if (arg === '--preserve-root') + preserveRoot = true + else if (arg === '--no-preserve-root') + preserveRoot = false + else + return !!arg +}).filter(arg => !preserveRoot || filterOutRoot(arg)) + +const go = n => { + if (n >= args.length) + return + const options = noglob ? { glob: false } : {} + rimraf(args[n], options, er => { + if (er) + throw er + go(n+1) + }) +} + +if (help || args.length === 0) { + // If they didn't ask for help, then this is not a "success" + const log = help ? console.log : console.error + log('Usage: rimraf [ ...]') + log('') + log(' Deletes all files and folders at "path" recursively.') + log('') + log('Options:') + log('') + log(' -h, --help Display this usage info') + log(' -G, --no-glob Do not expand glob patterns in arguments') + log(' -g, --glob Expand glob patterns in arguments (default)') + log(' --preserve-root Do not remove \'/\' (default)') + log(' --no-preserve-root Do not treat \'/\' specially') + log(' -- Stop parsing flags') + process.exit(help ? 0 : 1) +} else + go(0) diff --git a/benchmark/old-rimraf/package.json b/benchmark/old-rimraf/package.json new file mode 100644 index 00000000..1bf8d5e3 --- /dev/null +++ b/benchmark/old-rimraf/package.json @@ -0,0 +1,32 @@ +{ + "name": "rimraf", + "version": "3.0.2", + "main": "rimraf.js", + "description": "A deep deletion module for node (like `rm -rf`)", + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "license": "ISC", + "repository": "git://github.com/isaacs/rimraf.git", + "scripts": { + "preversion": "npm test", + "postversion": "npm publish", + "postpublish": "git push origin --follow-tags", + "test": "tap test/*.js" + }, + "bin": "./bin.js", + "dependencies": { + "glob": "^7.1.3" + }, + "files": [ + "LICENSE", + "README.md", + "bin.js", + "rimraf.js" + ], + "devDependencies": { + "mkdirp": "^0.5.1", + "tap": "^12.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } +} diff --git a/benchmark/old-rimraf/rimraf.js b/benchmark/old-rimraf/rimraf.js new file mode 100644 index 00000000..34da4171 --- /dev/null +++ b/benchmark/old-rimraf/rimraf.js @@ -0,0 +1,360 @@ +const assert = require("assert") +const path = require("path") +const fs = require("fs") +let glob = undefined +try { + glob = require("glob") +} catch (_err) { + // treat glob as optional. +} + +const defaultGlobOpts = { + nosort: true, + silent: true +} + +// for EMFILE handling +let timeout = 0 + +const isWindows = (process.platform === "win32") + +const defaults = options => { + const methods = [ + 'unlink', + 'chmod', + 'stat', + 'lstat', + 'rmdir', + 'readdir' + ] + methods.forEach(m => { + options[m] = options[m] || fs[m] + m = m + 'Sync' + options[m] = options[m] || fs[m] + }) + + options.maxBusyTries = options.maxBusyTries || 3 + options.emfileWait = options.emfileWait || 1000 + if (options.glob === false) { + options.disableGlob = true + } + if (options.disableGlob !== true && glob === undefined) { + throw Error('glob dependency not found, set `options.disableGlob = true` if intentional') + } + options.disableGlob = options.disableGlob || false + options.glob = options.glob || defaultGlobOpts +} + +const rimraf = (p, options, cb) => { + if (typeof options === 'function') { + cb = options + options = {} + } + + assert(p, 'rimraf: missing path') + assert.equal(typeof p, 'string', 'rimraf: path should be a string') + assert.equal(typeof cb, 'function', 'rimraf: callback function required') + assert(options, 'rimraf: invalid options argument provided') + assert.equal(typeof options, 'object', 'rimraf: options should be object') + + defaults(options) + + let busyTries = 0 + let errState = null + let n = 0 + + const next = (er) => { + errState = errState || er + if (--n === 0) + cb(errState) + } + + const afterGlob = (er, results) => { + if (er) + return cb(er) + + n = results.length + if (n === 0) + return cb() + + results.forEach(p => { + const CB = (er) => { + if (er) { + if ((er.code === "EBUSY" || er.code === "ENOTEMPTY" || er.code === "EPERM") && + busyTries < options.maxBusyTries) { + busyTries ++ + // try again, with the same exact callback as this one. + return setTimeout(() => rimraf_(p, options, CB), busyTries * 100) + } + + // this one won't happen if graceful-fs is used. + if (er.code === "EMFILE" && timeout < options.emfileWait) { + return setTimeout(() => rimraf_(p, options, CB), timeout ++) + } + + // already gone + if (er.code === "ENOENT") er = null + } + + timeout = 0 + next(er) + } + rimraf_(p, options, CB) + }) + } + + if (options.disableGlob || !glob.hasMagic(p)) + return afterGlob(null, [p]) + + options.lstat(p, (er, stat) => { + if (!er) + return afterGlob(null, [p]) + + glob(p, options.glob, afterGlob) + }) + +} + +// Two possible strategies. +// 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR +// 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR +// +// Both result in an extra syscall when you guess wrong. However, there +// are likely far more normal files in the world than directories. This +// is based on the assumption that a the average number of files per +// directory is >= 1. +// +// If anyone ever complains about this, then I guess the strategy could +// be made configurable somehow. But until then, YAGNI. +const rimraf_ = (p, options, cb) => { + assert(p) + assert(options) + assert(typeof cb === 'function') + + // sunos lets the root user unlink directories, which is... weird. + // so we have to lstat here and make sure it's not a dir. + options.lstat(p, (er, st) => { + if (er && er.code === "ENOENT") + return cb(null) + + // Windows can EPERM on stat. Life is suffering. + if (er && er.code === "EPERM" && isWindows) + fixWinEPERM(p, options, er, cb) + + if (st && st.isDirectory()) + return rmdir(p, options, er, cb) + + options.unlink(p, er => { + if (er) { + if (er.code === "ENOENT") + return cb(null) + if (er.code === "EPERM") + return (isWindows) + ? fixWinEPERM(p, options, er, cb) + : rmdir(p, options, er, cb) + if (er.code === "EISDIR") + return rmdir(p, options, er, cb) + } + return cb(er) + }) + }) +} + +const fixWinEPERM = (p, options, er, cb) => { + assert(p) + assert(options) + assert(typeof cb === 'function') + + options.chmod(p, 0o666, er2 => { + if (er2) + cb(er2.code === "ENOENT" ? null : er) + else + options.stat(p, (er3, stats) => { + if (er3) + cb(er3.code === "ENOENT" ? null : er) + else if (stats.isDirectory()) + rmdir(p, options, er, cb) + else + options.unlink(p, cb) + }) + }) +} + +const fixWinEPERMSync = (p, options, er) => { + assert(p) + assert(options) + + try { + options.chmodSync(p, 0o666) + } catch (er2) { + if (er2.code === "ENOENT") + return + else + throw er + } + + let stats + try { + stats = options.statSync(p) + } catch (er3) { + if (er3.code === "ENOENT") + return + else + throw er + } + + if (stats.isDirectory()) + rmdirSync(p, options, er) + else + options.unlinkSync(p) +} + +const rmdir = (p, options, originalEr, cb) => { + assert(p) + assert(options) + assert(typeof cb === 'function') + + // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS) + // if we guessed wrong, and it's not a directory, then + // raise the original error. + options.rmdir(p, er => { + if (er && (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM")) + rmkids(p, options, cb) + else if (er && er.code === "ENOTDIR") + cb(originalEr) + else + cb(er) + }) +} + +const rmkids = (p, options, cb) => { + assert(p) + assert(options) + assert(typeof cb === 'function') + + options.readdir(p, (er, files) => { + if (er) + return cb(er) + let n = files.length + if (n === 0) + return options.rmdir(p, cb) + let errState + files.forEach(f => { + rimraf(path.join(p, f), options, er => { + if (errState) + return + if (er) + return cb(errState = er) + if (--n === 0) + options.rmdir(p, cb) + }) + }) + }) +} + +// this looks simpler, and is strictly *faster*, but will +// tie up the JavaScript thread and fail on excessively +// deep directory trees. +const rimrafSync = (p, options) => { + options = options || {} + defaults(options) + + assert(p, 'rimraf: missing path') + assert.equal(typeof p, 'string', 'rimraf: path should be a string') + assert(options, 'rimraf: missing options') + assert.equal(typeof options, 'object', 'rimraf: options should be object') + + let results + + if (options.disableGlob || !glob.hasMagic(p)) { + results = [p] + } else { + try { + options.lstatSync(p) + results = [p] + } catch (er) { + results = glob.sync(p, options.glob) + } + } + + if (!results.length) + return + + for (let i = 0; i < results.length; i++) { + const p = results[i] + + let st + try { + st = options.lstatSync(p) + } catch (er) { + if (er.code === "ENOENT") + return + + // Windows can EPERM on stat. Life is suffering. + if (er.code === "EPERM" && isWindows) + fixWinEPERMSync(p, options, er) + } + + try { + // sunos lets the root user unlink directories, which is... weird. + if (st && st.isDirectory()) + rmdirSync(p, options, null) + else + options.unlinkSync(p) + } catch (er) { + if (er.code === "ENOENT") + return + if (er.code === "EPERM") + return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er) + if (er.code !== "EISDIR") + throw er + + rmdirSync(p, options, er) + } + } +} + +const rmdirSync = (p, options, originalEr) => { + assert(p) + assert(options) + + try { + options.rmdirSync(p) + } catch (er) { + if (er.code === "ENOENT") + return + if (er.code === "ENOTDIR") + throw originalEr + if (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM") + rmkidsSync(p, options) + } +} + +const rmkidsSync = (p, options) => { + assert(p) + assert(options) + options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options)) + + // We only end up here once we got ENOTEMPTY at least once, and + // at this point, we are guaranteed to have removed all the kids. + // So, we know that it won't be ENOENT or ENOTDIR or anything else. + // try really hard to delete stuff on windows, because it has a + // PROFOUNDLY annoying habit of not closing handles promptly when + // files are deleted, resulting in spurious ENOTEMPTY errors. + const retries = isWindows ? 100 : 1 + let i = 0 + do { + let threw = true + try { + const ret = options.rmdirSync(p, options) + threw = false + return ret + } finally { + if (++i < retries && threw) + continue + } + } while (true) +} + +module.exports = rimraf +rimraf.sync = rimrafSync diff --git a/benchmark/print-results.js b/benchmark/print-results.js new file mode 100644 index 00000000..831e3309 --- /dev/null +++ b/benchmark/print-results.js @@ -0,0 +1,65 @@ +const sum = list => list.reduce((a, b) => a + b) +const mean = list => sum(list) / list.length +const median = list => list.sort()[Math.floor(list.length / 2)] +const max = list => list.sort()[list.length - 1] +const min = list => list.sort()[0] +const sqrt = n => Math.pow(n, 0.5) +const variance = list => { + const m = mean(list) + return mean(list.map(n => Math.pow(n - m, 2))) +} +const stddev = list => { + const v = variance(list) + if (isNaN(v)) { + console.error({list, v}) + throw new Error('wat?') + } + return sqrt(variance(list)) +} + +const round = n => Math.round(n * 1e3) / 1e3 + +const nums = list => ({ + mean: round(mean(list)), + median: round(median(list)), + stddev: round(stddev(list)), + max: round(max(list)), + min: round(min(list)), +}) + +const printEr = er => `${er.code ? er.code + ': ' : ''}${er.message}` +const failures = list => list.length === 0 ? {} + : { failures: list.map(er => printEr(er)).join('\n') } + +const table = results => { + const table = {} + for (const [type, data] of Object.entries(results)) { + table[`${type} sync`] = { + ...nums(data.syncTimes), + ...failures(data.syncFails), + } + table[`${type} async`] = { + ...nums(data.asyncTimes), + ...failures(data.asyncFails), + } + table[`${type} parallel`] = { + ...nums(data.paraTimes), + ...failures(data.paraFails), + } + } + // sort by mean time + return Object.entries(table) + .sort(([, {mean:a}], [, {mean:b}]) => a - b) + .reduce((set, [key, val]) => { + set[key] = val + return set + }, {}) +} + +const print = results => { + console.log(JSON.stringify(results, 0, 2)) + console.log('Results sorted by fastest mean value') + console.table(table(results)) +} + +module.exports = print diff --git a/benchmark/rimrafs.js b/benchmark/rimrafs.js new file mode 100644 index 00000000..aefbabfe --- /dev/null +++ b/benchmark/rimrafs.js @@ -0,0 +1,40 @@ +// just disable the glob option, and promisify it, for apples-to-apples comp +const oldRimraf = () => { + const {promisify} = require('util') + const oldRimraf = require('./old-rimraf') + const pOldRimraf = promisify(oldRimraf) + const rimraf = path => pOldRimraf(path, { disableGlob: true }) + const sync = path => oldRimraf.sync(path, { disableGlob: true }) + return Object.assign(rimraf, { sync }) +} + +const { spawn, spawnSync } = require('child_process') +const systemRmRf = () => { + const rimraf = path => new Promise((res, rej) => { + const proc = spawn('rm', ['-rf', path]) + proc.on('close', (code, signal) => { + if (code || signal) + rej(Object.assign(new Error('command failed'), { code, signal })) + else + res() + }) + }) + rimraf.sync = path => { + const result = spawnSync('rm', ['-rf', path]) + if (result.status || result.signal) { + throw Object.assign(new Error('command failed'), { + code: result.status, + signal: result.signal, + }) + } + } + return rimraf +} + +module.exports = { + native: require('../').native, + posix: require('../').posix, + windows: require('../').windows, + old: oldRimraf(), + system: systemRmRf(), +} diff --git a/benchmark/run-test.js b/benchmark/run-test.js new file mode 100644 index 00000000..a9fe6da4 --- /dev/null +++ b/benchmark/run-test.js @@ -0,0 +1,100 @@ +const START = process.env.RIMRAF_TEST_START_CHAR || 'a' +const END = process.env.RIMRAF_TEST_END_CHAR || 'f' +const DEPTH = +process.env.RIMRAF_TEST_DEPTH || 5 +const N = +process.env.RIMRAF_TEST_ITERATIONS || 7 + +const cases = require('./rimrafs.js') + +const create = require('./create-fixture.js') + +const hrToMS = hr => Math.round(hr[0]*1e9 + hr[1]) / 1e6 + +const runTest = async (type) => { + const rimraf = cases[type] + if (!rimraf) + throw new Error('unknown rimraf type: ' + type) + + const opt = { + start: START, + end: END, + depth: DEPTH, + } + console.error(`\nrunning test for ${type}, iterations=${N} %j...`, opt) + + // first, create all fixtures + const syncPaths = [] + const asyncPaths = [] + const paraPaths = [] + process.stderr.write('creating fixtures...') + for (let i = 0; i < N; i++) { + const [syncPath, asyncPath, paraPath] = await Promise.all([ + create({ name: `${type}/sync/${i}`, ...opt }), + create({ name: `${type}/async/${i}`, ...opt }), + create({ name: `${type}/para/${i}`, ...opt }), + ]) + syncPaths.push(syncPath) + asyncPaths.push(asyncPath) + paraPaths.push(paraPath) + process.stderr.write('.') + } + console.error('done!') + + const syncTimes = [] + const syncFails = [] + process.stderr.write('running sync tests...') + const startSync = process.hrtime() + for (const path of syncPaths) { + const start = process.hrtime() + try { + rimraf.sync(path) + syncTimes.push(hrToMS(process.hrtime(start))) + } catch (er) { + syncFails.push(er) + } + process.stderr.write('.') + } + const syncTotal = hrToMS(process.hrtime(startSync)) + console.error('done! (%j ms, %j failed)', syncTotal, syncFails.length) + + const asyncTimes = [] + const asyncFails = [] + process.stderr.write('running async tests...') + const startAsync = process.hrtime() + for (const path of asyncPaths) { + const start = process.hrtime() + await rimraf(path).then( + () => asyncTimes.push(hrToMS(process.hrtime(start))), + er => asyncFails.push(er) + ).then(() => process.stderr.write('.')) + } + const asyncTotal = hrToMS(process.hrtime(startAsync)) + console.error('done! (%j ms, %j failed)', asyncTotal, asyncFails.length) + + const paraTimes = [] + const paraFails = [] + process.stderr.write('running parallel tests...') + const startPara = process.hrtime() + const paraRuns = [] + for (const path of paraPaths) { + const start = process.hrtime() + paraRuns.push(rimraf(path).then( + () => paraTimes.push(hrToMS(process.hrtime(start))), + er => paraFails.push(er) + ).then(() => process.stderr.write('.'))) + } + await Promise.all(paraRuns) + const paraTotal = hrToMS(process.hrtime(startPara)) + console.error('done! (%j ms, %j failed)', paraTotal, paraFails.length) + + // wait a tick to let stderr to clear + return Promise.resolve().then(() => ({ + syncTimes, + syncFails, + asyncTimes, + asyncFails, + paraTimes, + paraFails, + })) +} + +module.exports = runTest diff --git a/package.json b/package.json index 341ce792..905b9daa 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "lint": "npm run npmclilint -- libtap-settings.js map.js lib/**/*.*js test/**/*.*js", "lintfix": "npm run lint -- --fix", "posttest": "npm run lint --", - "postsnap": "npm run lintfix --" + "postsnap": "npm run lintfix --", + "benchmark": "node benchmark/index.js" }, "files": [ "lib"