diff --git a/.github/ISSUE_TEMPLATE/04-performance.yml b/.github/ISSUE_TEMPLATE/04-performance.yml new file mode 100644 index 0000000000..590508cfd0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04-performance.yml @@ -0,0 +1,51 @@ +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a performance improvement to Mocha! Please note that Mocha is a widely used library with many millions of weekly downloads, many thousands of dependents, and many years of longstanding behavior. Every change to it is inherently risky and may break user edge cases that we have no way to learn of otherwise. + + So, to accept a performance improvement, we'd need to see _measurable, non-negligible improvement_ in Mocha's real-world usage. That requires providing at least: + + - Exhaustive comparison with other alternatives that proves this is the winning approach + - Disclosure of any personal affiliation with the proposed solution and alternatives + - For runtime performance: some kind of reproducible benchmark to demonstrate exactly what's different + - For dependency cleanups: + - The specific change to the dependency tree of a project with Mocha and no other dependencies + - The specific change to the dependency tree of a project with Mocha and other common dependencies, such as `chai`, `express`, and `sinon` + + You can always suggest a performance improvement without those validations, but we are unlikely to accept it without them. + For more information, see [mochajs/mocha#5377 🛠️ Repo: Add issue template for performance improvements](https://github.com/mochajs/mocha/issues/5377). + - attributes: + description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you! + label: Performance Suggestion Checklist + options: + - label: I am using the latest version of Mocha. + required: true + - label: I have read and understood the nuances around performance reports. + required: true + - label: I have read and agree to Mocha's [Code of Conduct](https://github.com/mochajs/mocha/blob/main/.github/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/mochajs/mocha/blob/main/.github/CONTRIBUTING.md) + required: true + - label: I have searched for [related issues](https://github.com/mochajs/mocha/issues?q=is%3Aissue) and [issues with the `faq` label](https://github.com/mochajs/mocha/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq%20), but none matched my issue. + required: true + - label: I want to provide a PR to resolve this + type: checkboxes + - attributes: + description: What is your suggestion? + label: Overview + type: textarea + validations: + required: true + - attributes: + description: If you have a suggested implementation, please explain why you believe it's the best one here. + label: Validations + type: textarea + - attributes: + description: Any additional info you'd like to provide. + label: Additional Info + type: textarea +description: Suggest a way to make Mocha faster, more memory- and/or space-efficient, or otherwise improve performance +labels: + - 'area: performance' + - 'status: in triage' +name: ⚡️ Performance +title: '⚡️ Performance: ' diff --git a/.github/ISSUE_TEMPLATE/04-repository-tooling.yml b/.github/ISSUE_TEMPLATE/05-repository-tooling.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/04-repository-tooling.yml rename to .github/ISSUE_TEMPLATE/05-repository-tooling.yml diff --git a/lib/cli/watch-run.js b/lib/cli/watch-run.js index eb794a5631..2c4d9c5f3b 100644 --- a/lib/cli/watch-run.js +++ b/lib/cli/watch-run.js @@ -7,6 +7,7 @@ const chokidar = require('chokidar'); const Context = require('../context'); const collectFiles = require('./collect-files'); const glob = require('glob'); +const fs = require('node:fs'); /** * Exports the `watchRun` function that runs mocha in "watch" mode. @@ -207,31 +208,46 @@ const createWatcher = ( const tracker = new GlobFilesTracker(watchFiles, watchIgnore); tracker.regenerate(); - const watcher = chokidar.watch('.', { - ignoreInitial: true - }); + const watcher = chokidar.watch('.'); - const rerunner = createRerunner(mocha, watcher, { + const rerunner = createRerunner(mocha, watcher, tracker, { beforeRun }); - watcher.on('ready', async () => { - if (!globalFixtureContext) { - debug('triggering global setup'); - globalFixtureContext = await mocha.runGlobalSetup(); - } - rerunner.run(); + // eslint-disable-next-line no-restricted-globals + const startTime = new Date(); + let ready = false; + watcher.on('ready', () => { + ready = true; }); - watcher.on('all', (event, filePath) => { - if (event === 'add') { - tracker.regenerate(); + watcher.on('all', async function handleEvent(event, filePath, stat) { + if (exiting) return; + if (event === 'unlink' || (stat ? stat.mtime > startTime : ready)) { + if (event === 'add') { + tracker.regenerate(); + } + // we don't want to accidentally trigger a run before globalFixtureContext + // has been created. If it hasn't then it's okay to do nothing here because + // the code that creates globalFixtureContext will run the tests afterward + if (tracker.has(filePath) && globalFixtureContext) { + rerunner.scheduleRun(); + } + return; } - if (tracker.has(filePath)) { - rerunner.scheduleRun(); + if (!ready && !stat) { + fs.stat(filePath, (err, stat) => { + if (stat) handleEvent(event, filePath, stat); + }); } }); + debug('triggering global setup'); + mocha.runGlobalSetup().then(context => { + globalFixtureContext = context; + rerunner.run(); + }); + hideCursor(); process.on('exit', () => { showCursor(); @@ -287,13 +303,14 @@ const createWatcher = ( * * @param {Mocha} mocha - Mocha instance * @param {FSWatcher} watcher - chokidar `FSWatcher` instance + * @param {GlobFilesTracker} tracker - GlobFilesTracker instance * @param {Object} [opts] - Options! * @param {BeforeWatchRun} [opts.beforeRun] - Function to call before `mocha.run()` * @returns {Rerunner} * @ignore * @private */ -const createRerunner = (mocha, watcher, {beforeRun} = {}) => { +const createRerunner = (mocha, watcher, tracker, {beforeRun} = {}) => { // Set to a `Runner` when mocha is running. Set to `null` when mocha is not // running. let runner = null; @@ -307,7 +324,7 @@ const createRerunner = (mocha, watcher, {beforeRun} = {}) => { runner = mocha.run(() => { debug('finished watch run'); runner = null; - blastCache(watcher); + blastCache(tracker); if (rerunScheduled) { rerun(); } else { @@ -391,14 +408,14 @@ const eraseLine = () => { /** * Blast all of the watched files out of `require.cache` - * @param {FSWatcher} watcher - chokidar FSWatcher + * @param {GlobFilesTracker} tracker - GlobFilesTracker instance * @ignore * @private */ -const blastCache = watcher => { - const files = getWatchedFiles(watcher); +const blastCache = tracker => { + const files = tracker.watchFilesSet; files.forEach(file => { - delete require.cache[file]; + delete require.cache[path.resolve(file)]; }); debug('deleted %d file(s) from the require cache', files.length); }; diff --git a/test/integration/helpers.js b/test/integration/helpers.js index a6ab867abe..4e478f322e 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -492,8 +492,6 @@ async function runMochaWatchJSONAsync(args, opts, change) { ); } -const touchRef = new Date(); - /** * Synchronously touch a file. Creates * the file and all its parent directories if necessary. @@ -503,6 +501,7 @@ const touchRef = new Date(); function touchFile(filepath) { fs.mkdirSync(path.dirname(filepath), { recursive: true }); try { + const touchRef = new Date(); fs.utimesSync(filepath, touchRef, touchRef); } catch (e) { const fd = fs.openSync(filepath, 'a');