From 72ea857fd404173279306518138a6f1000b19971 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 14 Feb 2017 17:03:53 -0800 Subject: [PATCH 01/14] Create new parallel async queue --- lib/async-queue.js | 90 ++++++++++++++++------ test/async-queue.test.js | 160 +++++++++++++++++++++++++++++---------- 2 files changed, 188 insertions(+), 62 deletions(-) diff --git a/lib/async-queue.js b/lib/async-queue.js index a9e671c7d0..4026b98f29 100644 --- a/lib/async-queue.js +++ b/lib/async-queue.js @@ -1,28 +1,70 @@ +class Task { + constructor(fn, parallel = true) { + this.fn = fn; + this.parallel = parallel; + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + execute() { + return this.fn.call(undefined).then(this.resolve, this.reject); + } + + runsInParallel() { + return this.parallel; + } + + getPromise() { + return this.promise; + } +} + export default class AsyncQueue { - constructor() { - this.commandQueue = []; - this.running = false; - } - - push(fn) { - const p = new Promise((resolve, reject) => this.commandQueue.push({fn, resolve, reject})); - if (!this.running) { this.processQueue(); } - return p; - } - - async processQueue() { - this.running = true; - let lastPromise = Promise.resolve(); - while (this.commandQueue.length) { - const {fn, resolve, reject} = this.commandQueue[0]; - const run = () => fn(); - lastPromise = lastPromise.then(run, run); - lastPromise.then(resolve, reject); - try { - await lastPromise; // eslint-disable-line babel/no-await-in-loop - } catch (e) { /* nothing */ } - this.commandQueue.shift(); + constructor(options = {}) { + this.parallelism = options.parallelism || 1; + this.nonParallelizableOperation = false; + this.threadsInUse = 0; + this.queue = []; + } + + push(fn, {parallel} = {parallel: true}) { + const task = new Task(fn, parallel); + this.queue.push(task); + this.processQueue(); + return task.getPromise(); + } + + processQueue() { + if (!this.queue.length || this.nonParallelizableOperation) { return; } + + const task = this.queue[0]; + const canRunParallelOp = task.runsInParallel() && this.threadsInUse < this.parallelism; + const canRunSerialOp = !task.runsInParallel() && this.threadsInUse === 0; + if (canRunSerialOp || canRunParallelOp) { + this.processTask(task, task.runsInParallel()); + this.queue.shift(); + this.processQueue(); + } + } + + async processTask(task, runsInParallel) { + this.threadsInUse++; + console.warn(this.threadsInUse); + if (!runsInParallel) { + this.nonParallelizableOperation = true; + } + + try { + await task.execute(); + } catch (err) { + // nothing + } finally { + this.threadsInUse--; + console.warn(this.threadsInUse); + this.nonParallelizableOperation = false; + this.processQueue(); } - this.running = false; } } diff --git a/test/async-queue.test.js b/test/async-queue.test.js index b27751cb15..cd095940dd 100644 --- a/test/async-queue.test.js +++ b/test/async-queue.test.js @@ -1,47 +1,131 @@ +import {autobind} from 'core-decorators'; + import AsyncQueue from '../lib/async-queue'; -const timer = n => { - return new Promise(resolve => { - setTimeout(resolve, 10 - n); - }); -}; - -describe('AsyncQueue', function() { - it('runs items in the order queued', async function() { - const queue = new AsyncQueue(); - - const expectedEvents = []; - const actualEvents = []; - const promises = []; - for (let i = 0; i < 10; i++) { - expectedEvents.push(i); - const promise = queue.push(() => timer(i)).then(() => actualEvents.push(i)); - promises.push(promise); +class Task { + constructor(name, error) { + this.name = name; + this.error = error; + this.started = false; + this.finished = false; + } + + @autobind + run() { + this.started = true; + this.finished = false; + return new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + @autobind + finish() { + this.finished = true; + if (this.error) { + this.reject(new Error(this.name)); + } else { + this.resolve(this.name); } + } +} + +describe.only('AsyncQueue', function() { + it('runs items in parallel up to the set max', async function() { + const queue = new AsyncQueue({parallelism: 3}); + + const tasks = [ + new Task('task 1'), + new Task('task 2', true), + new Task('task 3'), + new Task('task 4'), + new Task('task 5'), + ]; + const results = [false, false, false, false, false]; + const errors = [false, false, false, false, false]; + + const p0 = queue.push(() => tasks[0].run()); + const p1 = queue.push(() => tasks[1].run()); + const p2 = queue.push(() => tasks[2].run()); + const p3 = queue.push(() => tasks[3].run()); + const p4 = queue.push(() => tasks[4].run()); + + p0.then(value => { results[0] = value; }).catch(err => { errors[0] = err; }); + p1.then(value => { results[1] = value; }).catch(err => { errors[1] = err; }); + p2.then(value => { results[2] = value; }).catch(err => { errors[2] = err; }); + p3.then(value => { results[3] = value; }).catch(err => { errors[3] = err; }); + p4.then(value => { results[4] = value; }).catch(err => { errors[4] = err; }); + + assert.isTrue(tasks[0].started); + assert.isTrue(tasks[1].started); + assert.isTrue(tasks[2].started); + assert.isFalse(tasks[3].started); + assert.isFalse(tasks[4].started); + + assert.isFalse(results[0]); - await Promise.all(promises); - assert.deepEqual(expectedEvents, actualEvents); + tasks[0].finish(); + assert.isTrue(tasks[0].finished); + await assert.async.equal(results[0], 'task 1'); + assert.isFalse(tasks[1].finished); + assert.isFalse(results[1]); + + assert.isTrue(tasks[3].started); + assert.isFalse(tasks[4].started); + + tasks[1].finish(); + assert.isTrue(tasks[1].finished); + assert.isFalse(tasks[2].finished); + await assert.async.equal(errors[1].message, 'task 2'); + + assert.isTrue(tasks[4].started); }); - it('continues running queued items when one fails', async function() { - const queue = new AsyncQueue(); - - const expectedEvents = []; - const actualEvents = []; - const promises = []; - for (let i = 0; i < 10; i++) { - expectedEvents.push(i === 5 ? 'error' : i); - const promise = queue.push(() => { - if (i === 5) { - throw new Error('omg'); - } else { - return timer(i); - } - }).then(() => actualEvents.push(i), () => actualEvents.push('error')); - promises.push(promise); - } + it('runs non-parallelizable tasks serially', async function() { + const queue = new AsyncQueue({parallelism: 3}); + + const tasks = [ + new Task('task 1'), + new Task('task 2'), + new Task('task 3'), + new Task('task 4'), + new Task('task 5'), + new Task('task 6'), + ]; + + const p0 = queue.push(() => tasks[0].run()); + const p1 = queue.push(() => tasks[1].run()); + const p2 = queue.push(() => tasks[2].run(), {parallel: false}); + const p3 = queue.push(() => tasks[3].run(), {parallel: false}); + queue.push(() => tasks[4].run()); + queue.push(() => tasks[5].run()); + + assert.isTrue(tasks[0].started); + assert.isTrue(tasks[1].started); + assert.isFalse(tasks[2].started); // not parallelizable!! + assert.isFalse(tasks[3].started); + assert.isFalse(tasks[4].started); + assert.isFalse(tasks[5].started); + + tasks[0].finish(); + await p0; + assert.isFalse(tasks[2].started); // still can't be started + assert.isFalse(tasks[3].started); + + tasks[1].finish(); + await p1; + await assert.async.isTrue(tasks[2].started); + assert.isFalse(tasks[3].started); // still can't be started + + tasks[2].finish(); + await p2; + await assert.async.isTrue(tasks[3].started); + assert.isFalse(tasks[4].started); // 3 is non-parallelizable so 4 can't start - await Promise.all(promises); - assert.deepEqual(expectedEvents, actualEvents); + tasks[3].finish(); + await p3; + await assert.async.isTrue(tasks[4].started); // both can start since they are parallelizable + assert.isTrue(tasks[5].started); }); }); From e25b009cb99a0139c1b5d385ff8e56c28e367297 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 14 Feb 2017 17:05:07 -0800 Subject: [PATCH 02/14] Parallelize all non-write Git commands --- lib/git-shell-out-strategy.js | 39 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 5de778cc74..c0559943ae 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -32,11 +32,11 @@ function withGpgScript(args) { export default class GitShellOutStrategy { constructor(workingDir) { this.workingDir = workingDir; - this.commandQueue = new AsyncQueue(); + this.commandQueue = new AsyncQueue({parallelism: 5}); } // Execute a command and read the output using the embedded Git environment - exec(args, stdin = null, useGitPromptServer = false) { + exec(args, stdin = null, useGitPromptServer = false, writeOperation = false) { /* eslint-disable no-console */ const subscriptions = new CompositeDisposable(); @@ -110,6 +110,7 @@ export default class GitShellOutStrategy { return stdout; }); }); + }, {parallel: !writeOperation}); /* eslint-enable no-console */ } @@ -119,7 +120,7 @@ export default class GitShellOutStrategy { */ gpgExec(args, stdin = null) { const gpgArgs = withGpgScript(args); - return this.exec(gpgArgs, stdin).catch(err => { + return this.exec(gpgArgs, stdin, false, true).catch(err => { if (err.code === 128 && /gpg failed/.test(err.stdErr)) { // Retry with a GitPromptServer return this.exec(gpgArgs, stdin, true); @@ -144,33 +145,33 @@ export default class GitShellOutStrategy { stageFiles(paths) { if (paths.length === 0) { return null; } const args = ['add'].concat(paths); - return this.exec(args); + return this.exec(args, null, false, true); } unstageFiles(paths, commit = 'HEAD') { if (paths.length === 0) { return null; } const args = ['reset', commit, '--'].concat(paths); - return this.exec(args); + return this.exec(args, null, false, true); } applyPatch(patch, {index} = {}) { const args = ['apply', '-']; if (index) { args.splice(1, 0, '--cached'); } - return this.exec(args, patch); + return this.exec(args, patch, false, true); } commit(message, {allowEmpty, amend} = {}) { const args = ['commit', '-m', message]; if (amend) { args.push('--amend'); } if (allowEmpty) { args.push('--allow-empty'); } - return this.gpgExec(args); + return this.gpgExec(args, null, false, true); } /** * File Status and Diffs */ async getStatusesForChangedFiles() { - const output = await this.exec(['status', '--untracked-files=all', '-z']); + const output = await this.exec(['status', '--untracked-files=all', '-z'], null, false, true); const statusMap = { 'A': 'added', @@ -280,7 +281,7 @@ export default class GitShellOutStrategy { async isPartiallyStaged(filePath) { const args = ['status', '--short', '--', filePath]; - const output = await this.exec(args); + const output = await this.exec(args, null, false, true); const results = output.trim().split(LINE_ENDING_REGEX); if (results.length === 2) { return true; @@ -325,7 +326,7 @@ export default class GitShellOutStrategy { } abortMerge() { - return this.exec(['merge', '--abort']); + return this.exec(['merge', '--abort'], null, false, true); } /** @@ -334,7 +335,7 @@ export default class GitShellOutStrategy { clone(remoteUrl, options = {}) { const args = ['clone', '--no-local', remoteUrl, this.workingDir]; if (options.bare) { args.push('--bare'); } - return this.exec(args); + return this.exec(args, null, false, true); } async getRemoteForBranch(branchName) { @@ -348,12 +349,12 @@ export default class GitShellOutStrategy { async fetch(branchName) { const remote = await this.getRemoteForBranch(branchName); - return this.exec(['fetch', remote, branchName], null, true); + return this.exec(['fetch', remote, branchName], null, true, true); } async pull(branchName) { const remote = await this.getRemoteForBranch(branchName); - return this.gpgExec(['pull', remote, branchName], null, true); + return this.gpgExec(['pull', remote, branchName], null, true, true); } async push(branchName, options = {}) { @@ -361,7 +362,7 @@ export default class GitShellOutStrategy { const args = ['push', remote || 'origin', branchName]; if (options.setUpstream) { args.push('--set-upstream'); } if (options.force) { args.push('--force'); } - return this.exec(args, null, true); + return this.exec(args, null, true, true); } async getAheadCount(branchName) { @@ -395,7 +396,7 @@ export default class GitShellOutStrategy { checkout(branchName, options = {}) { const args = ['checkout']; if (options.createNew) { args.push('-b'); } - return this.exec(args.concat(branchName)); + return this.exec(args.concat(branchName), null, false, true); } async checkoutFiles(paths, revision) { @@ -403,7 +404,7 @@ export default class GitShellOutStrategy { try { const args = ['checkout']; if (revision) { args.push(revision); } - return await this.exec(args.concat('--', paths)); + return await this.exec(args.concat('--', paths), null, false, true); } catch (error) { const matches = error.stdErr.match(/error: pathspec .* did not match any file\(s\) known to git\./g); if (matches.length) { @@ -449,7 +450,7 @@ export default class GitShellOutStrategy { let args = ['config']; if (replaceAll) { args.push('--replace-all'); } args = args.concat(option, value); - return this.exec(args); + return this.exec(args, null, false, true); } async getRemotes() { @@ -472,9 +473,9 @@ export default class GitShellOutStrategy { async createBlob({filePath, stdin} = {}) { let output; if (filePath) { - output = await this.exec(['hash-object', '-w', filePath]); + output = await this.exec(['hash-object', '-w', filePath], null, false, true); } else if (stdin) { - output = await this.exec(['hash-object', '-w', '--stdin'], stdin); + output = await this.exec(['hash-object', '-w', '--stdin'], stdin, false, true); } else { throw new Error('Must supply file path or stdin'); } From a2282c1335f9dd056f16939e6eb56eca56366c27 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 14 Feb 2017 17:05:33 -0800 Subject: [PATCH 03/14] Capture JS event loop processing time in timing view waterfalls --- lib/git-shell-out-strategy.js | 50 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index c0559943ae..5820171b0f 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -86,30 +86,34 @@ export default class GitShellOutStrategy { if (process.env.PRINT_GIT_TIMES) { console.time(`git:${formattedArgs}`); } - timingMarker.mark('execute'); - return GitProcess.exec(args, this.workingDir, options) - .then(({stdout, stderr, exitCode}) => { - timingMarker.finalize(); - if (process.env.PRINT_GIT_TIMES) { - console.timeEnd(`git:${formattedArgs}`); - } - if (gitPromptServer) { - gitPromptServer.terminate(); - } - subscriptions.dispose(); - if (exitCode) { - const err = new GitError( - `${formattedArgs} exited with code ${exitCode}\nstdout: ${stdout}\nstderr: ${stderr}`, - ); - err.code = exitCode; - err.stdErr = stderr; - err.stdOut = stdout; - err.command = formattedArgs; - return Promise.reject(err); - } - return stdout; + return new Promise(resolve => { + timingMarker.mark('nexttick'); + setImmediate(() => { + timingMarker.mark('execute'); + resolve(GitProcess.exec(args, this.workingDir, options) + .then(({stdout, stderr, exitCode}) => { + timingMarker.finalize(); + if (process.env.PRINT_GIT_TIMES) { + console.timeEnd(`git:${formattedArgs}`); + } + if (gitPromptServer) { + gitPromptServer.terminate(); + } + subscriptions.dispose(); + if (exitCode) { + const err = new GitError( + `${formattedArgs} exited with code ${exitCode}\nstdout: ${stdout}\nstderr: ${stderr}`, + ); + err.code = exitCode; + err.stdErr = stderr; + err.stdOut = stdout; + err.command = formattedArgs; + return Promise.reject(err); + } + return stdout; + })); }); - }); + }); }, {parallel: !writeOperation}); /* eslint-enable no-console */ } From 7f23487b8aae36e2a78d306791e0ca3a190fd88f Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 00:01:18 -0800 Subject: [PATCH 04/14] :fire: console.warn --- lib/async-queue.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/async-queue.js b/lib/async-queue.js index 4026b98f29..9b2b464a03 100644 --- a/lib/async-queue.js +++ b/lib/async-queue.js @@ -51,7 +51,6 @@ export default class AsyncQueue { async processTask(task, runsInParallel) { this.threadsInUse++; - console.warn(this.threadsInUse); if (!runsInParallel) { this.nonParallelizableOperation = true; } @@ -62,7 +61,6 @@ export default class AsyncQueue { // nothing } finally { this.threadsInUse--; - console.warn(this.threadsInUse); this.nonParallelizableOperation = false; this.processQueue(); } From ff96a4d4cd432681aa6b23f8f565883865caa4bc Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 10:07:11 -0800 Subject: [PATCH 05/14] :fire: .only --- test/async-queue.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/async-queue.test.js b/test/async-queue.test.js index cd095940dd..a291bd0cfe 100644 --- a/test/async-queue.test.js +++ b/test/async-queue.test.js @@ -31,7 +31,7 @@ class Task { } } -describe.only('AsyncQueue', function() { +describe('AsyncQueue', function() { it('runs items in parallel up to the set max', async function() { const queue = new AsyncQueue({parallelism: 3}); From 43a17cbb277a5cc23ae4b70b5ffd173d85f2c9cb Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 10:30:11 -0800 Subject: [PATCH 06/14] Make queue resilient to sync errors --- lib/async-queue.js | 22 +++++++++++----------- test/async-queue.test.js | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/async-queue.js b/lib/async-queue.js index 9b2b464a03..5b7c06d8bc 100644 --- a/lib/async-queue.js +++ b/lib/async-queue.js @@ -8,8 +8,13 @@ class Task { }); } - execute() { - return this.fn.call(undefined).then(this.resolve, this.reject); + async execute() { + try { + const value = await this.fn.call(undefined); + this.resolve(value); + } catch (err) { + this.reject(err); + } } runsInParallel() { @@ -55,14 +60,9 @@ export default class AsyncQueue { this.nonParallelizableOperation = true; } - try { - await task.execute(); - } catch (err) { - // nothing - } finally { - this.threadsInUse--; - this.nonParallelizableOperation = false; - this.processQueue(); - } + await task.execute(); + this.threadsInUse--; + this.nonParallelizableOperation = false; + this.processQueue(); } } diff --git a/test/async-queue.test.js b/test/async-queue.test.js index a291bd0cfe..988f88b3a8 100644 --- a/test/async-queue.test.js +++ b/test/async-queue.test.js @@ -128,4 +128,21 @@ describe('AsyncQueue', function() { await assert.async.isTrue(tasks[4].started); // both can start since they are parallelizable assert.isTrue(tasks[5].started); }); + + it('continues to work when tasks throw synchronous errors', async function() { + const queue = new AsyncQueue({parallelism: 1}); + + const p1 = queue.push(() => { + throw new Error('error thrown from task 1'); + }); + const p2 = queue.push(() => { + return new Promise(res => res(2)); + }); + + try { + await p1; + throw new Error('expected p1 to be rejectd'); + } catch (err) {} + assert.equal(await p2, 2); + }); }); From 77dab9ada4ac3842e6b19ac9fafba3c52b02fcaf Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 10:30:33 -0800 Subject: [PATCH 07/14] :art: --- lib/async-queue.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/async-queue.js b/lib/async-queue.js index 5b7c06d8bc..dfdfadf17e 100644 --- a/lib/async-queue.js +++ b/lib/async-queue.js @@ -21,6 +21,10 @@ class Task { return this.parallel; } + runsInSerial() { + return !this.parallel; + } + getPromise() { return this.promise; } @@ -30,7 +34,7 @@ export default class AsyncQueue { constructor(options = {}) { this.parallelism = options.parallelism || 1; this.nonParallelizableOperation = false; - this.threadsInUse = 0; + this.tasksInProgress = 0; this.queue = []; } @@ -45,8 +49,8 @@ export default class AsyncQueue { if (!this.queue.length || this.nonParallelizableOperation) { return; } const task = this.queue[0]; - const canRunParallelOp = task.runsInParallel() && this.threadsInUse < this.parallelism; - const canRunSerialOp = !task.runsInParallel() && this.threadsInUse === 0; + const canRunParallelOp = task.runsInParallel() && this.tasksInProgress < this.parallelism; + const canRunSerialOp = task.runsInSerial() && this.tasksInProgress === 0; if (canRunSerialOp || canRunParallelOp) { this.processTask(task, task.runsInParallel()); this.queue.shift(); @@ -55,13 +59,13 @@ export default class AsyncQueue { } async processTask(task, runsInParallel) { - this.threadsInUse++; + this.tasksInProgress++; if (!runsInParallel) { this.nonParallelizableOperation = true; } await task.execute(); - this.threadsInUse--; + this.tasksInProgress--; this.nonParallelizableOperation = false; this.processQueue(); } From afb363ea08cba9a2c67bf73ff8b107cfa1347117 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 10:30:55 -0800 Subject: [PATCH 08/14] Oops forgot a thing --- lib/views/git-timings-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/git-timings-view.js b/lib/views/git-timings-view.js index 2c1066ade1..b9faa0508b 100644 --- a/lib/views/git-timings-view.js +++ b/lib/views/git-timings-view.js @@ -94,6 +94,7 @@ class MarkerTooltip extends React.Component { const COLORS = { queued: 'red', prepare: 'cyan', + nexttick: 'yellow', execute: 'green', }; class MarkerSpan extends React.Component { From 34e7b82d6c84c38b8ae07c748db48968725d04a4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 12:20:47 -0800 Subject: [PATCH 09/14] Remove GSOS tests covered already by AsyncQueue tests --- lib/git-shell-out-strategy.js | 5 ++-- test/git-shell-out-strategy.test.js | 36 ----------------------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 5820171b0f..6e2ceb6bdd 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -30,9 +30,10 @@ function withGpgScript(args) { } export default class GitShellOutStrategy { - constructor(workingDir) { + constructor(workingDir, options = {parallelism: 5}) { + lastSos = this; this.workingDir = workingDir; - this.commandQueue = new AsyncQueue({parallelism: 5}); + this.commandQueue = new AsyncQueue({parallelism: options.parallelism || 5}); } // Execute a command and read the output using the embedded Git environment diff --git a/test/git-shell-out-strategy.test.js b/test/git-shell-out-strategy.test.js index 8d7fe1b97d..753b6d8634 100644 --- a/test/git-shell-out-strategy.test.js +++ b/test/git-shell-out-strategy.test.js @@ -14,42 +14,6 @@ import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories */ describe('Git commands', function() { - describe('exec', function() { - it('serializes operations', async function() { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - const expectedEvents = []; - const actualEvents = []; - const promises = []; - for (let i = 0; i < 10; i++) { - expectedEvents.push(i); - promises.push(git.getHeadCommit().then(() => actualEvents.push(i))); - } - - await Promise.all(promises); - assert.deepEqual(expectedEvents, actualEvents); - }); - - it('runs operations after one fails', async function() { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - const expectedEvents = []; - const actualEvents = []; - const promises = []; - for (let i = 0; i < 10; i++) { - expectedEvents.push(i); - if (i === 5) { - promises.push(git.exec(['fake', 'command']).catch(() => actualEvents.push(i))); - } else { - promises.push(git.exec(['status']).then(() => actualEvents.push(i))); - } - } - - await Promise.all(promises); - assert.deepEqual(expectedEvents, actualEvents); - }); - }); - describe('isGitRepository(directoryPath)', function() { it('returns true if the path passed is a valid repository, and false if not', async function() { const workingDirPath = await cloneRepository('three-files'); From 609379f3332fc0cc1bf24fa4c04a81006ead5fe7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 12:38:38 -0800 Subject: [PATCH 10/14] Better parallelize Git operations in GitPanelController --- lib/controllers/git-panel-controller.js | 26 +++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/controllers/git-panel-controller.js b/lib/controllers/git-panel-controller.js index 20d2177642..bdcf0d2a90 100644 --- a/lib/controllers/git-panel-controller.js +++ b/lib/controllers/git-panel-controller.js @@ -82,18 +82,28 @@ export default class GitPanelController { @autobind async fetchRepositoryData(repository) { + const dataPromises = { + unstagedChanges: repository.getUnstagedChanges(), + stagedChanges: this.fetchStagedChanges(repository), + mergeConflicts: repository.getMergeConflicts(), + lastCommit: repository.getLastCommit(), + isMerging: repository.isMerging(), + branchName: repository.getCurrentBranch(), + branches: repository.getBranches(), + remoteName: repository.getRemoteForBranch(this.branchName), + }; const data = { - unstagedChanges: await repository.getUnstagedChanges(), - stagedChanges: await this.fetchStagedChanges(repository), - mergeConflicts: await repository.getMergeConflicts(), - lastCommit: await repository.getLastCommit(), - isMerging: await repository.isMerging(), - branchName: await repository.getCurrentBranch(), - branches: await repository.getBranches(), + unstagedChanges: await dataPromises.unstagedChanges, + stagedChanges: await dataPromises.stagedChanges, + mergeConflicts: await dataPromises.mergeConflicts, + lastCommit: await dataPromises.lastCommit, + isMerging: await dataPromises.isMerging, + branchName: await dataPromises.branchName, + branches: await dataPromises.branches, mergeMessage: null, aheadCount: null, behindCount: null, - remoteName: await repository.getRemoteForBranch(this.branchName), + remoteName: await dataPromises.remoteName, }; if (data.remoteName) { From 2b43e07f65e308ccfbcca57621e46ab747f95f52 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 12:38:54 -0800 Subject: [PATCH 11/14] Drop `test-until` default timeout to under the mocha timeout --- test/runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runner.js b/test/runner.js index f74209a61d..02cec57b23 100644 --- a/test/runner.js +++ b/test/runner.js @@ -8,7 +8,7 @@ chai.use(chaiAsPromised); global.assert = chai.assert; // Give tests that rely on filesystem event delivery lots of breathing room. -until.setDefaultTimeout(5000); +until.setDefaultTimeout(3000); module.exports = createRunner({ htmlTitle: `GitHub Package Tests - pid ${process.pid}`, From 3902ad288e155df65a07676e697d3e0458b978bd Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 12:47:47 -0800 Subject: [PATCH 12/14] Experiment with optimizing Git calls --- lib/controllers/git-panel-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/git-panel-controller.js b/lib/controllers/git-panel-controller.js index bdcf0d2a90..450cf97880 100644 --- a/lib/controllers/git-panel-controller.js +++ b/lib/controllers/git-panel-controller.js @@ -83,14 +83,14 @@ export default class GitPanelController { @autobind async fetchRepositoryData(repository) { const dataPromises = { - unstagedChanges: repository.getUnstagedChanges(), - stagedChanges: this.fetchStagedChanges(repository), mergeConflicts: repository.getMergeConflicts(), lastCommit: repository.getLastCommit(), isMerging: repository.isMerging(), branchName: repository.getCurrentBranch(), branches: repository.getBranches(), remoteName: repository.getRemoteForBranch(this.branchName), + unstagedChanges: repository.getUnstagedChanges(), + stagedChanges: this.fetchStagedChanges(repository), }; const data = { unstagedChanges: await dataPromises.unstagedChanges, From b3eb449dea72ad6c23ade4e1d1ffa5c52ec04253 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 12:53:51 -0800 Subject: [PATCH 13/14] Base GSOS queue parallelism on number of CPUs --- lib/git-shell-out-strategy.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 6e2ceb6bdd..ec4b6bf7f4 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -1,4 +1,5 @@ import path from 'path'; +import os from 'os'; import {CompositeDisposable} from 'atom'; @@ -30,10 +31,10 @@ function withGpgScript(args) { } export default class GitShellOutStrategy { - constructor(workingDir, options = {parallelism: 5}) { - lastSos = this; + constructor(workingDir, options = {}) { this.workingDir = workingDir; - this.commandQueue = new AsyncQueue({parallelism: options.parallelism || 5}); + const parallelism = options.parallelism || Math.max(3, os.cpus().length); + this.commandQueue = new AsyncQueue({parallelism}); } // Execute a command and read the output using the embedded Git environment From 98ea7b194b65fea24b6eb8415b34025b7672b6a8 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Feb 2017 13:07:20 -0800 Subject: [PATCH 14/14] Convert all GSOS#exec calls to use options object parameter --- lib/git-shell-out-strategy.js | 42 +++++++++++++++-------------- test/git-shell-out-strategy.test.js | 12 ++++----- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index ec4b6bf7f4..f47a7d59ed 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -31,6 +31,8 @@ function withGpgScript(args) { } export default class GitShellOutStrategy { + static defaultExecArgs = {stdin: null, useGitPromptServer: false, writeOperation: false} + constructor(workingDir, options = {}) { this.workingDir = workingDir; const parallelism = options.parallelism || Math.max(3, os.cpus().length); @@ -38,7 +40,7 @@ export default class GitShellOutStrategy { } // Execute a command and read the output using the embedded Git environment - exec(args, stdin = null, useGitPromptServer = false, writeOperation = false) { + exec(args, {stdin, useGitPromptServer, writeOperation} = GitShellOutStrategy.defaultExecArgs) { /* eslint-disable no-console */ const subscriptions = new CompositeDisposable(); @@ -124,12 +126,12 @@ export default class GitShellOutStrategy { * Execute a git command that may create a commit. If the command fails because the GPG binary was invoked and unable * to acquire a passphrase (because the pinentry program attempted to use a tty), retry with a `GitPromptServer`. */ - gpgExec(args, stdin = null) { + gpgExec(args, {stdin} = {stdin: null}) { const gpgArgs = withGpgScript(args); - return this.exec(gpgArgs, stdin, false, true).catch(err => { + return this.exec(gpgArgs, {stdin, writeOperation: true}).catch(err => { if (err.code === 128 && /gpg failed/.test(err.stdErr)) { // Retry with a GitPromptServer - return this.exec(gpgArgs, stdin, true); + return this.exec(gpgArgs, {stdin, useGitPromptServer: true, writeOperation: true}); } else { throw err; } @@ -151,33 +153,33 @@ export default class GitShellOutStrategy { stageFiles(paths) { if (paths.length === 0) { return null; } const args = ['add'].concat(paths); - return this.exec(args, null, false, true); + return this.exec(args, {writeOperation: true}); } unstageFiles(paths, commit = 'HEAD') { if (paths.length === 0) { return null; } const args = ['reset', commit, '--'].concat(paths); - return this.exec(args, null, false, true); + return this.exec(args, {writeOperation: true}); } applyPatch(patch, {index} = {}) { const args = ['apply', '-']; if (index) { args.splice(1, 0, '--cached'); } - return this.exec(args, patch, false, true); + return this.exec(args, {stdin: patch, writeOperation: true}); } commit(message, {allowEmpty, amend} = {}) { const args = ['commit', '-m', message]; if (amend) { args.push('--amend'); } if (allowEmpty) { args.push('--allow-empty'); } - return this.gpgExec(args, null, false, true); + return this.gpgExec(args, {writeOperation: true}); } /** * File Status and Diffs */ async getStatusesForChangedFiles() { - const output = await this.exec(['status', '--untracked-files=all', '-z'], null, false, true); + const output = await this.exec(['status', '--untracked-files=all', '-z'], {writeOperation: true}); const statusMap = { 'A': 'added', @@ -287,7 +289,7 @@ export default class GitShellOutStrategy { async isPartiallyStaged(filePath) { const args = ['status', '--short', '--', filePath]; - const output = await this.exec(args, null, false, true); + const output = await this.exec(args, {writeOperation: true}); const results = output.trim().split(LINE_ENDING_REGEX); if (results.length === 2) { return true; @@ -332,7 +334,7 @@ export default class GitShellOutStrategy { } abortMerge() { - return this.exec(['merge', '--abort'], null, false, true); + return this.exec(['merge', '--abort'], {writeOperation: true}); } /** @@ -341,7 +343,7 @@ export default class GitShellOutStrategy { clone(remoteUrl, options = {}) { const args = ['clone', '--no-local', remoteUrl, this.workingDir]; if (options.bare) { args.push('--bare'); } - return this.exec(args, null, false, true); + return this.exec(args, {writeOperation: true}); } async getRemoteForBranch(branchName) { @@ -355,12 +357,12 @@ export default class GitShellOutStrategy { async fetch(branchName) { const remote = await this.getRemoteForBranch(branchName); - return this.exec(['fetch', remote, branchName], null, true, true); + return this.exec(['fetch', remote, branchName], {useGitPromptServer: true, writeOperation: true}); } async pull(branchName) { const remote = await this.getRemoteForBranch(branchName); - return this.gpgExec(['pull', remote, branchName], null, true, true); + return this.gpgExec(['pull', remote, branchName], {useGitPromptServer: true, writeOperation: true}); } async push(branchName, options = {}) { @@ -368,7 +370,7 @@ export default class GitShellOutStrategy { const args = ['push', remote || 'origin', branchName]; if (options.setUpstream) { args.push('--set-upstream'); } if (options.force) { args.push('--force'); } - return this.exec(args, null, true, true); + return this.exec(args, {useGitPromptServer: true, writeOperation: true}); } async getAheadCount(branchName) { @@ -402,7 +404,7 @@ export default class GitShellOutStrategy { checkout(branchName, options = {}) { const args = ['checkout']; if (options.createNew) { args.push('-b'); } - return this.exec(args.concat(branchName), null, false, true); + return this.exec(args.concat(branchName), {writeOperation: true}); } async checkoutFiles(paths, revision) { @@ -410,7 +412,7 @@ export default class GitShellOutStrategy { try { const args = ['checkout']; if (revision) { args.push(revision); } - return await this.exec(args.concat('--', paths), null, false, true); + return await this.exec(args.concat('--', paths), {writeOperation: true}); } catch (error) { const matches = error.stdErr.match(/error: pathspec .* did not match any file\(s\) known to git\./g); if (matches.length) { @@ -456,7 +458,7 @@ export default class GitShellOutStrategy { let args = ['config']; if (replaceAll) { args.push('--replace-all'); } args = args.concat(option, value); - return this.exec(args, null, false, true); + return this.exec(args, {writeOperation: true}); } async getRemotes() { @@ -479,9 +481,9 @@ export default class GitShellOutStrategy { async createBlob({filePath, stdin} = {}) { let output; if (filePath) { - output = await this.exec(['hash-object', '-w', filePath], null, false, true); + output = await this.exec(['hash-object', '-w', filePath], {writeOperation: true}); } else if (stdin) { - output = await this.exec(['hash-object', '-w', '--stdin'], stdin, false, true); + output = await this.exec(['hash-object', '-w', '--stdin'], {stdin, writeOperation: true}); } else { throw new Error('Must supply file path or stdin'); } diff --git a/test/git-shell-out-strategy.test.js b/test/git-shell-out-strategy.test.js index 753b6d8634..043c189bb9 100644 --- a/test/git-shell-out-strategy.test.js +++ b/test/git-shell-out-strategy.test.js @@ -614,8 +614,8 @@ describe('Git commands', function() { const execArgs = callArgs[0]; assert.equal(execArgs[0], '-c'); assert.match(execArgs[1], /^gpg\.program=.*gpg-no-tty\.sh$/); - assert.isNull(callArgs[1]); - assert.isNotOk(callArgs[2]); + assert.isNotOk(callArgs[1].stdin); + assert.isNotOk(callArgs[1].useGitPromptServer); }); it(`retries a ${op.verbalNoun} with a GitPromptServer when GPG signing fails`, async () => { @@ -641,15 +641,15 @@ describe('Git commands', function() { const execArgs0 = callArgs0[0]; assert.equal(execArgs0[0], '-c'); assert.match(execArgs0[1], /^gpg\.program=.*gpg-no-tty\.sh$/); - assert.isNull(callArgs0[1]); - assert.isNotOk(callArgs0[2]); + assert.isNotOk(callArgs0[1].stdin); + assert.isNotOk(callArgs0[1].useGitPromptServer); const callArgs1 = execStub.getCall(callIndex + 1).args; const execArgs1 = callArgs1[0]; assert.equal(execArgs1[0], '-c'); assert.match(execArgs1[1], /^gpg\.program=.*gpg-no-tty\.sh$/); - assert.isNull(callArgs1[1]); - assert.isTrue(callArgs1[2]); + assert.isNotOk(callArgs1[1].stdin); + assert.isTrue(callArgs1[1].useGitPromptServer); }); }); });