Wine takes 0.29 seconds to start git.exe. Not to run it; to start it. A process that imports only kernel32 gets going in 0.16 seconds, anything that also imports user32 takes 0.29, and git, cmd and nearly everything else import user32. A native Linux spawn is under a millisecond. Git Extensions runs git once or more per click, and some clicks run nine.

So the first version (the stock Windows build with the crash patched out and a Windows git in the prefix) worked, and felt like using a computer through treacle. The version the install script gives you doesn’t, and this post is about the difference, which came from giving up on starting Windows processes at all.

The things that didn’t help

The usual Wine tuning doesn’t help here. WINEESYNC and WINEFSYNC, the kernel’s ntsync driver, a prefix on tmpfs, git’s core.fscache: none of them moved the number.

Two things helped a bit, and a third helped more. Every Windows process enumerates fonts at startup, and with a full Noto installation that’s about 2,300 file opens plus a registry read per font per process, so a minimal fontconfig file shaved ten percent off a spawn. The performance CPU profile took twenty percent off, since a powersave profile stretches short bursts. The third was turning off the app’s background status calls: three settings each fire git on every focus change, and a git status walk through Wine’s filesystem layer costs about a second on top.

The seam

Almost every git call the app makes goes through one interface, IExecutable, and its one implementation is what starts the process.

Under Wine, the fork adds a branch at the top of it that sends the call over a socket instead. On the other end of the socket is a small daemon on the Linux side, started by the launcher and gone when the app is, and the daemon runs your Linux git. The code above it still sends arguments and gets output back.

One git command is now a localhost round trip of a few milliseconds. The nine commands the app fires on opening a repository took 56 ms together, which is under the time one of them used to take to start.

Subjectively it now feels snappier than the native app on Windows, the dialogs especially. I haven’t measured Windows, but the mechanism is at least plausible: on Windows every one of those commands is still a real git.exe process start, and here it’s a fork on Linux.

Why not just exec it

The obvious shortcut is to skip the socket and have the Windows app start the Linux binary directly. Wine can do this; wine /usr/bin/true works. But what you get back is a process you can’t wait on, can’t talk to, and can’t kill, because Wine’s loader hands you no handle and wires up none of the pipes. A daemon over a socket is the honest version of the same idea.

Two worlds, one path

Git on the Linux side sees /home/you/src/repo. The app on the Wine side sees Z:\home\you\src\repo, and it puts that everywhere: the working directory, arguments, config overrides, environment variables. So the daemon turns Windows paths into Linux ones on the way in, wherever they appear.

Coming back is harder, because you can’t rewrite git’s output wholesale; a diff of a file that happens to contain a Windows path must arrive untouched. So the daemon translates only the output of the two commands the app uses to learn where things are, and everything else passes through as bytes.

Environment is the same story with one decision that matters more than the rest. The daemon doesn’t forward HOME. That’s what makes native git read your Linux config, your identity, your credential helpers and your hooks, instead of an empty profile inside the prefix. The daemon also turns off the merge tool prompt, because nobody can answer “Hit return to start merge resolution tool” over a socket.

And then the rule that made the rest of the app fall into place: under the bridge, anything that isn’t obviously a Windows program is a Linux program. User scripts, external tools, gitk, the merge tool, the diff tool.

The terminal

The Console tab was the last holdout. It’s hosted in ConEmu, and ConEmu can only run a Windows program. The bridge runs Linux programs. So for a while the tab ran a BusyBox shell inside the prefix, which could not see your prompt, your aliases, your SSH agent, or the native git everything else was using.

The fix is a Windows program that is nothing but a wire. It asks the daemon for a pseudo-terminal running your login shell in the repository, and then copies: terminal output to the console, keystrokes and resizes back to the pty. ConEmu does the emulation and never learns that the shell on the other end is a Linux one.

Once that existed, the progress dialogs got it for free. Rebase, merge, pull, cherry-pick: each one hosts its command in ConEmu through the same relay, so git is on a real pseudo-terminal, an interactive rebase with a terminal editor works, and the progress output is coloured. One detail from that: nano and less switch to the alternate screen, which ConEmu implements by dumping the whole console buffer and writing it back afterwards. ConEmu’s default for that buffer is 32,766 rows and the copy took about three seconds under Wine every time you quit an editor. The app sets it to 1,000 under Wine, which keeps the switch below notice and still leaves a thousand lines of scrollback.

What’s left

Almost nothing runs the Windows git any more; the working tree is owned by native git in practice, and the shell in the Console tab is yours. The bridge can still be switched off, and then the prefix needs a Windows git of its own and all the old pain comes back.

The slow thing was Wine starting Windows programs, and the app starting them constantly. The seam was already there. The bridge just puts something on the other side of it that doesn’t need a third of a second to say hello.