Monday 3 November 2008
Grumble Grumble
I've been hacking up an implementation of interruptible reflow. Laying out large pages is sometimes a source of unacceptable delays. It would be a better user experience if, during a long layout, we could detect that there's pending user input, get out of the layout code, handle the user input, and then resume the layout. In principle this isn't that hard now that we use dirty bits to track what needs to be laid out. It's simply a matter of making sure the right dirty bits are set as we bail out of reflow, and then making sure we enter reflow again eventually to complete the unfinished work. We also have to make sure we accurately distinguish "interruptible" reflows from "uninterruptible" reflows. For example, if a reflow is triggered by script asking for the geometry of some element, that's uninterruptible since returning with some work undone would give incorrect results and break things.
I have a patch that basically works, at least for pages that are dominated by block layout. But I've run into a severe problem: I don't know how to detect pending user input in Mac OS X or GTK :-(. On Windows, for about 20 years there's been a function called GetInputState which does exactly what we need. OS X and GTK/X11 just don't have anything like it. I've tried Appkit's nextEventMatchingMask; it sometimes processes some events, which is unacceptable. X11 doesn't seem to provide a way to peek the event queue without blocking; the only nonblocking event queue reading APIs always remove the event from the queue if they find one.
OS X and GTK suck, Windows rules. Prove me wrong, fanboys!
Comments
[1] http://library.gnome.org/devel/gdk/stable/gdk-Events.html#gdk-events-pending
[2] http://www.sbin.org/doc/Xlib/chapt_08.html
Could you use nsIIdleService's idleTime to determine if there is pending user input? If idleTime is small, there might be pending user input?
-Seth
nsIdleService doesn't work since it doesn't detect pending input that hasn't been processed by Gecko yet.
pkasting, returning to the event loop periodically kinda works but it's a heck of a performance penalty because bailing out of reflow and restarting is really inefficient. It could even starve us indefinitely in some situations. And, unless we bail out *really often*, like every 100ms, we won't be responsive enough.
Looks like gdk_events_pending calls XPending or XEventsQueued or something. Anyway, it seems to work OK, although using that alone means we interrupt on mouse moves, which I don't really want. Then again so would GetInputState. I wonder how fast gdk_events_pending is, though.
You might want to take a look at IsEventInQueue or AcquireFirstMatchingEventInQueue, if multi-threaded is not the way you can go. See the Carbon Event Manager Reference [1]
For X11, XPeekEvent is your friend. Or, if you need more than the head of the queue, go for XCheckIfEvent
As for GTK - who knows or cares? ;)
Like all things slightly out of the ordinary for OSX programs, you need to drop out of Cocoa and into Carbon to achieve things.
[1] http://developer.apple.com/documentation/Carbon/Reference/Carbon_Event_Manager_Ref/index.html
A dirty trick would be to call XCheckIfEvent with a predicate that always returns false (thus leaving the event queue intact) and that uses the closure argument to indicate whether anything was actually found. That should be safe, as long as there isn't any problem with flushing the output queue.
Without it the code gets much more complicated but it looks to me like they are using XNextRequest, XEventsQueued, and XNextEvent.
Are you passing [[NSRunLoop currentRunLoop] currentMode] for the mode parameter? Are you passing NO for dequeue flag?
Post your code :)
What I mean is that that call to nextEventMatchingMask is actually dispatching events that reenter Gecko (and cause havoc).
I actually see the same thing for a call to nextEventMatchingMask:dequeue:NO that's already in the tree:
http://mxr.mozilla.org/mozilla-central/source/widget/src/cocoa/nsAppShell.mm#603
If you however want to decide on a per-event basis if you need to interrupt reflow, the only option I can come up with is to call gdk_display_get_event() until you got all queued events - when it returns NULL (otherwise you mess up the ordering of events), then decide based on all pulled events if you need to interrupt, and afterwards stuff them back using gdk_display_put_event(); gdk_event_free().
If you go that route, you definitely want to file an enhancement bug with your implementation so Gdk gets support for it in the next release.
Your whole app will appear frozen if you don't return (no redraws, animations will stop, progressbars won't advance etc) and I can't imagine that'd be intended.
So it seems to me that you should return to the main loop after X ms (95% confidence interval of a normal page being rendered I'd say, but that's just gut feeling) and return unconditionally, then process all events and resume layouting.