Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) - #123

Open
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master
Open

Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
gampixi wants to merge 3 commits into
CabbageDevelopment:masterfrom
gampixi:master

Conversation

@gampixi

@gampixigampixi commented Jul 17, 2024

Copy link
Copy Markdown

Summary

Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.

It's reasonable to assume that custom event loops would adhere to the contract established in the asyncio docs. Quoting the asyncio docs:

Callbacks are called in the order in which they are registered.

This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.

Issue was observed on Windows.

I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.

Some background

Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.

Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.

We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use call_soon to queue the processing of the value.

As observed, each update roughly goes through these stages:

  1. Bleak calls call_soon_threadsafe, which makes Qt emit the passed callback as a signal
  2. The signal callback calls call_soon (from a different thread than Bleak used)
  3. qasync redirects call_soon to call_later with delay=0
  4. call_later builds an asyncio.Handle and passes it to _add_callback
  5. _add_callback redirects to the _SimpleTimer, which registers a timer with a timeout of 0
  6. A short while later, the registered timer timeouts, and is handled by the timerEvent method

The out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.

This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.

After applying the workaround from this PR, the issue no longer happens.

@gampixi

gampixi commented Jul 17, 2024

Copy link
Copy Markdown
Author

Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though.

Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains.

gampixiand others added 3 commits July 24, 2025 15:12
…them independently of delayed callbacks
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
@hosaka

Copy link
Copy Markdown
Collaborator

Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the tests/test_qeventloop.py::test_exception_handler the handler is never executed. Maybe we can also benchmark the CallSoonQueue agains existing SimpleTimer class to see what sort of performance we might be getting/losing here.

@hosakahosaka added enhancement New feature or request help wanted Extra attention is needed labels Jul 24, 2025
@gampixi

Copy link
Copy Markdown
Author

Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed.

Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming).

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gampixi@hosaka