-
Notifications
You must be signed in to change notification settings - Fork 6
feat(events): add event throttle #30
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
Conversation
Thibault-Pelletier
commented
Sep 17, 2025
- Add event throttle and compression to handle unresponsive server during interaction.
|
Hi @jourdain, This PR adds a event throttling and compression mechanism to avoid overloading the server when it's hanging. My use case is the following : With the event throttling / compression, only the latest mouse hover is sent and others are ignored. As I'm not a JS developper, this is a port of the equivalent Python impl I tested before but which wasn't satisfactory performance wise. Don't hesitate to let me know what needs to be changed for it to be more JS. Let me know what you think of this mechanism and how I can improve this PR ! |
|
In general the code looks great, especially the logic and idea. The only thing that could be optimized would be memory handling to prevent/reduce gc. Those are optimization we may not need, but would not hurt. |
|
Possible code change to test in case it is better... export class EventThrottle {
constructor(processCallback, throttleTimeMs = 100) {
this.eventQueue = [];
this.processing = false;
this.throttleTimeMs = throttleTimeMs;
this.processCallback = processCallback;
this.eventKeysToIgnore = new Set(['x', 'y']);
}
compressEvents(events) {
if (events.length < 2) return [...events];
const compressed = [];
for (let i = 0; i < events.length; i++) {
const current = events[i];
const next = events[i + 1];
if (next && this.canCompressEvents(current, next)) {
continue;
}
compressed.push(current);
}
return compressed;
}
canCompressEvents(prev, next) {
if (prev.type === 'MouseWheel' || next.type === 'MouseWheel') return false;
if (Object.keys(prev).length !== Object.keys(next).length) return false;
for (const key in prev) {
if (this.eventKeysToIgnore.has(key)) continue;
if (prev[key] !== next[key]) return false;
}
return true;
}
sendEvent(event) {
this.eventQueue.push(event);
if (!this.processing) {
this.processing = true;
this._processEventQueue().catch((err) => {
console.error('Error in _processEventQueue:', err);
this.processing = false;
});
}
}
async _processEventQueue() {
const compressedEvents = this.compressEvents(this.eventQueue);
this.eventQueue.length = 0
const t0 = Date.now()
for (const event of compressedEvents) {
await this.processCallback(event);
await new Promise((resolve) => setTimeout(resolve, 0)); // Yield to event loop
}
const dt = this.throttleTimeMs - Date.now() + t0;
if (dt > 0) {
await new Promise((resolve) => setTimeout(resolve, dt));
}
if (this.eventQueue.length > 0) {
await this._processEventQueue();
} else {
this.processing = false;
}
}
} |
253f7de to
0c6eb1a
Compare
|
Thanks for the review and the comments! |
|
Another pass (sorry) function yield() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function sleep(timeMS) {
if (timeMS < 0) {
return;
}
return new Promise((resolve) => setTimeout(resolve, timeMS));
}
export class EventThrottle {
constructor(processCallback, throttleTimeMs = 100) {
this.eventQueue = [];
this.processing = false;
this.throttleTimeMs = throttleTimeMs;
this.processCallback = processCallback;
this.eventKeysToIgnore = new Set(['x', 'y']);
}
compressEvents(events) {
if (events.length < 2) return [...events];
const compressed = [];
for (let i = 1; i < events.length; i++) {
const current = events[i - 1];
const next = events[i];
if (this.canCompressEvents(current, next)) {
continue;
}
compressed.push(current);
}
return compressed;
}
canCompressEvents(prev, next) {
if (prev.type === 'MouseWheel' || next.type === 'MouseWheel') return false;
if (Object.keys(prev).length !== Object.keys(next).length) return false;
for (let key in prev) {
if (this.eventKeysToIgnore.has(key)) continue;
if (prev[key] !== next[key]) return false;
}
return true;
}
sendEvent(event) {
this.eventQueue.push(event);
if (!this.processing) {
this.processing = true;
this._processEventQueue().catch((err) => {
console.error('Error in _processEventQueue:', err);
this.processing = false;
});
}
}
async _processEventQueue() {
const compressedEvents = this.compressEvents(this.eventQueue);
this.eventQueue.length = 0
const t0 = Date.now()
for (const event of compressedEvents) {
await this.processCallback(event);
await yield();
}
await sleep(this.throttleTimeMs - Date.now() + t0);
if (this.eventQueue.length > 0) {
await this._processEventQueue();
} else {
this.processing = false;
}
}
} |
|
Just try my code to make sure I don't introduce issue. |
|
with the adaptive wait time, your default |
|
I'll have a look tomorrow. My implementation is broken somehow at the moment 😅 |
0c6eb1a to
b832162
Compare
b832162 to
1ae1564
Compare
|
I just updated the code and everything seems to be working properly! Just a note, I had to :
I added some unitest and updated the workflow to run them. Let me know if I should move them (wasn't sure which folder structure for tests you use / like best for trame components). |
- Add event throttle and compression to handle unresponsive server during interaction.
1ae1564 to
b648d23
Compare
|
I guess the |
|
Feel free to merge if you are good to go |
Yes that's correct, only the i handling was changed to keep the last event when no compression was done.
PR is good to go on my end, but I don't seem to have the right to merge it. 😅 |
|
Thx for the merge! |