Running a comparison
A comparison is a job, not a function call. It runs in its own process, reports where it has got to, stops when you tell it to, and cannot take the window down with it.
Where the work happens#
Comparisons are CPU-bound — a line diff over a large pair, a pixel loop, a recursive folder walk — and the window must not freeze while one runs. So the engines run in a separate utility process:
- It is a plain Node process. It has no Electron APIs and no DOM, and it reads any input that was too
large to travel with the request. Engines never import
fsthemselves — filesystem access is injected into them, which is also what lets one engine run somewhere else entirely. - It is spawned on your first comparison and reused afterwards, so the second comparison does not pay the startup cost.
- If it hangs or dies, it can be killed without taking the app with it.
The path a comparison takes:
| Step | Where |
|---|---|
| Compare or ⏎ | The window |
| The request is validated against a schema | Main process, at the IPC boundary |
| The engine is resolved so the UI can name it before work begins | Main process |
| The worker picks the engine and runs it | Utility process |
| Progress, then done or error | Back to the window as events |
One job at a time#
TwinScope compares two things, so it runs one comparison. A second request supersedes the first rather than queueing behind it.
The superseded job is not torn down mid-flight, so its progress events keep arriving for a moment. They are discarded: every event carries the job id it belongs to, and an event from a job that is no longer the current one is dropped rather than allowed to clobber the new job's state. You never see a percentage from a comparison you replaced, or a result that belongs to the previous pair.
Live progress#
While a job runs you get a percentage, a stage label under the bar, and the same percentage in the status bar. The stage names come from the engine, so they describe real work:
| Engine | Stages you will see |
|---|---|
| Text | reading → comparing lines → done |
| JSON | reading → parsing → comparing structure → done |
| Folders | scanning <path> → scanned N files (every 500) → comparing → done |
| Images | reading → decoding → comparing pixels → finding regions → rendering difference |
| Binary | measuring → hashing → done |
Cancelling#
Cancel appears in the toolbar for the whole time a job is running.
Cancellation is cooperative: the host aborts a signal, and each engine checks that signal between units of work — after reading its inputs, between chunks, between bands of pixels. That is what makes a cancel instant in practice without the app having to kill anything.
The workspace then shows Comparison cancelled rather than a failure, because you did not hit a bug — you changed your mind. There is no Copy details button on a cancellation, for the same reason.
If the engine process crashes#
| What happens | What you see |
|---|---|
| The worker exits with a job in flight | The comparison fails with The comparison engine stopped unexpectedly. Try again. |
| You run another comparison | A fresh worker is spawned automatically — no restart, no reload |
| The worker fails to start three times | The comparison engine failed to start repeatedly. Restart TwinScope to try again. |
The third case is deliberate: a spawn loop that keeps retrying forever produces an app that looks broken and says nothing. Three attempts, then a sentence you can act on.
A failure that is not a crash gets the engine's own message. A file that was deleted between loading it and comparing it, for instance, fails with a message naming that file — and the failure panel has a Copy details button that puts the reason, both input names and the engine on your clipboard. See Reading a result.
Answers that come back immediately#
Some comparisons do not need diffing at all, and pretending otherwise wastes your time.
| Case | What happens |
|---|---|
| The same content on both sides | The text engine compares the two strings, finds them equal and returns without diffing: 0 changes, plus the line count |
| Two empty files | The same short-circuit: 0 changes, 0 lines |
| Two identical binaries | A size match, then one hash per side — These files are byte-for-byte identical |
| Two binaries of different sizes | No hashing at all: a size difference is already conclusive |
| Two identical images | These images are identical at this threshold |
A large text pair asks first#
Over 10 MB on either side, the Compare button does not run the comparison — it shows the size, warns that this will take a few seconds, and waits for Compare anyway. A progress bar that sits still for eight seconds is indistinguishable from a hang, and being asked is better than guessing.
Folder and image comparisons skip the question: their cost has nothing to do with input byte size.
Limits#
Every limit here stops instead of hanging, and says which limit it hit.
| Limit | Value | Past it |
|---|---|---|
| Input carried over IPC | 10 MB | Only the path travels; the worker reads the file |
| Bytes handed to the image engine | 64 MB | Refused, with the file name and its size |
| Text lines, both sides together | 400,000 | The text engine stops and says the pair is too large for the line-by-line view |
| Image dimensions | 4096 px on the longest side | Scaled down before comparing, and the result says it was |
| Folder entries scanned | 50,000 per side | The scan goes partial, the summary strip carries a partial scan chip, and the result says so |
| Folder depth | 40 levels | Stops descending, and says so in the result |
| JSON nodes | 500,000 | The tree stops growing rather than exhausting memory |
| JSON depth | 100 levels | Deeper nodes are marked not compared |