Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Run git commands in dedicated renderer process #688

Merged
merged 82 commits into from
Apr 27, 2017
Merged

Conversation

kuychaco
Copy link
Contributor

@kuychaco kuychaco commented Apr 20, 2017

Fixes #386.
Fixes #643.

This PR addresses the issue of Atom freezing due to multiple blocking spawn calls when shelling out to Git in process (#386).

The Options

The solution is to make Git calls in a separate process, options being a Node child process, Electron renderer process, or Chromium web worker.

This PR implements a dedicated renderer process for shelling out to Git.

Basic testing with a Node child process revealed quadratic growth in IPC time relative to message size so this approach was dismissed due to the negative performance implications. The fix for this won't be released until Node v7.5.0.

Web workers are the ideal solution, but will have to wait until Atom is upgraded to Electron v1.6.4 which adds Node integration to web workers.

Current Approach

This PR introduces a WorkerManager which creates a Worker that wraps a RendererProcess which shells out to Git and sends results back over IPC. If the renderer process is not yet ready, we fall back to shelling out in process.

The renderer process keeps a running average of the time it takes to make a spawn call and if this exceeds 20ms (and the minimum number of operations specified have been completed), the WorkerManager creates a new RendererProcess and routes all new Git data requests to it. Once the "sick" renderer process finishes its operations it will be destroyed. The new renderer process may have a higher operationCountLimit, so that if the spawn calls continue to take longer than 20ms, we'll wait longer before creating a new RendererProcess in order to prevent process thrashing on slow machines.

If a renderer process crashes, the WorkerManager will create a new RendererProcess with the same operationCountLimit and send all incomplete operations to the new process.

If the WorkerManager process is destroyed, all RendererProcess instances will immediately destroy themselves.

If a Worker has it's destroy method called and there are remaining operations to be completed, we wait until completion before destroying the associated RendererProcess. This is in the event that there is a write operation that is still in progress and we want to allow it to reach disk.

Things to consider for future PRs:

  • Avoid duplicate write operations when a renderer process crashes mid-write and a new one is created that picks up the unfinished operation. This can be done by marking an Operation as "in-progress" by sending an ipc message after GitProcess.exec has been called.

kuychaco and others added 30 commits April 12, 2017 18:39
Only need one RendererProcessManager for each Atom window. Also reset
RendererProcessManager in afterEach.
Since we're using a renderer process for spawning git processes right
now
... to go along with hostWebContentsId
Copy link
Contributor

@smashwilson smashwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really, really cool. I'll install this branch and give it a shot over the next few days but I'd ❤️ to get this merged and out to our staff users as soon as we can 😁

});
}, {parallel: !writeOperation});
/* eslint-enable no-console */
}

executeGitCommand(args, options, marker = null) {
if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC || !WorkerManager.getInstance().isReady()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for offering an env var to opt out of the workers for when someone runs this on their Raspberry Pi 😉

If I'm reading it right it'll prevent the first worker from spawning too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading it right it'll prevent the first worker from spawning too?

Yup 👍 . Second expression after || doesn't get evaluated if first expression evaluates to true

@@ -220,6 +239,7 @@ export default class GitShellOutStrategy {

async isGitRepository() {
try {
await fsStat(this.workingDir); // fails if folder doesn't exist
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, cool. This fixes that uncaught-exception-in-Promise from the test suite I presume.

<body>
<h1>GitHub Package Git Execution Window</h1>
<p>
Hi there! I'm a window used by the GitHub pacakge to execute Git commands in the background. My PID is <script>document.write(process.pid)</script>.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✍️ "GitHub pacakge"

(... Nobody will ever see this 😛 )

static status = {
PENDING: Symbol('pending'),
INPROGRESS: Symbol('in-progress'),
COMPLETE: Symbol('complete'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hooray for Symbols 🎉

this.startTime = performance.now();
return execFn({...this.data, id: this.id});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This WorkerManager/Operation setup is downright elegant 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙇‍♀️

// For now we won't do this to avoid clogging up ipc channel
// event.sender.sendTo(managerWebContentsId, channelName, {sourceWebContentsId, type: 'exec-started', data: {id}});

if (averageTracker.enoughData() && averageTracker.getAverage() > 20) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be sure to give this a shot on the Windows box to see what the spawn latency is like here. If 20ms is enough here it should be enough for anywhere.

smashwilson's computer irl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks yo

@@ -158,6 +164,16 @@ export function createRenderer() {
return renderer;
}

export function isProcessAlive(pid) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest: before I clicked that link, I really expected "Stayin' Alive." Should have known better 😆

workerManager.request();
workerManager.request();
const worker1OperationsInFlight = worker1.getRemainingOperations();
assert.equal(worker1OperationsInFlight.length, 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an assert.lengthOf(...) too... I think it shows you the array contents if the length is wrong but it isn't too long?

assert.equal(worker1OperationsInFlight.length, 3);

const worker1Pid = worker1.getPid();
process.kill(worker1Pid, 'SIGKILL');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔪

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, to clarify, this was my attempt at a SIGKILL emoji, not an indication that this line shouldn't be here or anything.

const headerStyle = 'font-weight: bold; color: blue;';

console.groupCollapsed(`git:${formattedArgs}`);
console.log('%cexit status%c %d', headerStyle, 'font-weight: normal; color: black;', exitCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The day I discovered how %c works in console.log() was a dangerous day.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
3 participants