Repository navigation
Yield with MessageChannel instead of setTimeout in async mode #710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ExplodingCabbage
merged 7 commits into
kpdecker:master
from
ruiined:async-mode-without-timer-clamping
Oct 16, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b78154e
Yield with MessageChannel instead of setTimeout in async mode
ruiined 60421a7
Add release notes
ruiined 0becfa3
Cannibalize a couple of @anzal1's useful tests
ExplodingCabbage 9a8d26d
Improve naming & fix Safari web workers
ExplodingCabbage 5330d1b
Improve release notes
ExplodingCabbage dd2dfd5
Merge remote-tracking branch 'origin/master' into async-mode-without-…
ExplodingCabbage 61ed075
Merge remote-tracking branch 'origin/master' into async-mode-without-…
ExplodingCabbage File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| let enqueueTaskCalls = 0; | ||
|
|
||
| // Adds `callback` to the event queue as a macrotask, much like setTimeout(callback, 0) would. | ||
| // Unlike setTimeout(callback, 0), this MessageChannel-based approach isn't delayed by timer | ||
| // clamping (at least 4ms per nested timer in browsers, 1ms per timer in Node). We want to avoid | ||
| // timer clamping because it would make jsdiff's async mode much much slower than sync mode for | ||
| // no good reason. | ||
| export function enqueueTask(callback: () => void): void { | ||
| // The occasional setTimeout calls are a compromise to mitigate | ||
| // https://bugs.webkit.org/show_bug.cgi?id=327531. | ||
| // Without this workaround, async mode would be effectively sync inside a web worker in Safari. | ||
| // Instead, we very occasionally yield just so that really long-running async diffs will | ||
| // occasionally yield in Safari, while still eliminating 90% of the clamping in all environments. | ||
| if (++enqueueTaskCalls % 10 == 0) { | ||
| setTimeout(callback, 0); | ||
| return; | ||
| } | ||
|
|
||
| const {port1, port2} = new MessageChannel(); | ||
| port1.onmessage = () => { | ||
| // An open port would keep a Node process alive | ||
| port1.close(); | ||
| callback(); | ||
| }; | ||
| port2.postMessage(undefined); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anzal1, from the description and comments in your PR, I expected this to fail in Node - but it does not, at least on my machine. Does it fail on yours? I haven't tested in browsers yet but at least in Node I'm simply not seeing the starvation behaviour in the first place that your PR did extra work to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, passes for me too. That test was written for an older version of #715 that reused one channel, and that does starve timers in Node. A fresh channel per yield like this doesn't. My bad for the confusion.
The only place I could get this approach to starve timers was a WebKit Web Worker. But I looked through 41 repos that use jsdiff in workers and none of them use async mode there, so I wouldn't worry about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 thanks!
(I am curious how you found and reviewed those 41 repos. Does "I looked through" here mean "Claude looked through"?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude and I work together now :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some further remarks:
I'll check tomorrow if I can repro the timer starvation inside a worker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both macrotasks, but the spec lets the browser pick which task queue runs next, and timers and messages are separate queues. WebKit workers just keep picking messages. Repro if useful (open in Safari): https://gist.github.com/anzal1/cbcc4255edf861dd62cab4a8158d4115
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to post saying I've tested this now and can't reproduce starvation in a web worker (in either Firefox or Chrome, the browsers I've got installed here on my Ubuntu desktop)... but then realised you specifically said a WebKit web worker, something which I have no ability to test. Bah!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got Claude to assure me that you are correct, and filed a WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=327531