Tuesday, 4 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!



19 comments:

  1. I don't know at which level you hook into the gtk stack, but probably something like gdk_events_pending() [1] or XEventsQueued()/XPending() [2] might help.
    [1] http://library.gnome.org/devel/gdk/stable/gdk-Events.html#gdk-events-pending
    [2] http://www.sbin.org/doc/Xlib/chapt_08.html

    ReplyDelete
  2. No, you are right - GTK does suck.

    ReplyDelete
  3. Use Qt. Win32 blows.

    ReplyDelete
  4. Is gtk_events_pending() not what you're after (for gtk)?

    ReplyDelete
  5. For Gtk+ I believe you want g_main_context_pending() (see http://library.gnome.org/devel/glib/stable/glib-The-Main-Event-Loop.html). I poked around in the OSX documentation but couldn't find anything.

    ReplyDelete
  6. Do you have to actually detect pending input? If you're in the midst of interruptible reflow that's "taking too long", could you just return to the event loop regardless? In the case where input isn't pending this presumably adds a small amount of overhead, but if the event loop is written well, I'd imagine it wouldn't be noticeable (alas, I'm unfamiliar with Gecko's event loop).

    ReplyDelete
  7. Roc,
    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

    ReplyDelete
  8. Ka-Hing Cheung4 November 2008 21:19

    g_main_context_pending?

    ReplyDelete
  9. Robert O'Callahan4 November 2008 23:18

    gtk_events_pending and g_main_context_pending don't work because we often have pending GTK events like I/O that we don't want to interrupt on.
    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.

    ReplyDelete
  10. It's probably not easily accessible on the Mac because the first rule of UI programming is to keep event handling and time-consuming actions on different tasks. (Hey, Apple is opinionated if nothing else ;)
    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

    ReplyDelete
  11. Robert Blum: I think XIfEvent/XCheckIfEvent are the APIs that roc was referring to at the beginning when he said "the only nonblocking event queue reading APIs always remove the event from the queue if they find one." XPeekIfEvent doesn't remove the event from the queue, but -- if the manpage is to be believed -- it can, on occasion, block, which would also be unacceptable.
    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.

    ReplyDelete
  12. Looking at Qt, it seems they try to use g_main_context_pending when built with GLib support.
    Without it the code gets much more complicated but it looks to me like they are using XNextRequest, XEventsQueued, and XNextEvent.

    ReplyDelete
  13. What do you mean when you say "[nextEventMatchingMask] sometimes processes some events"?
    Are you passing [[NSRunLoop currentRunLoop] currentMode] for the mode parameter? Are you passing NO for dequeue flag?
    Post your code :)

    ReplyDelete
  14. Robert O'Callahan5 November 2008 09:58

    Passing NO for the dequeue flag and passing NSDefaultRunLoopMode for the mode parameter, IIRC.
    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

    ReplyDelete
  15. Depending on what you want, I can see 2 possibilities with Gtk: If you just want to know if events are pending, you can use gdk_events_pending () or - if you want it per-display - gdk_display_peek_event(). This will however get you all X events, including property change notifications (like switching the active window) or repaints you triggered through Gtk.
    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.

    ReplyDelete
  16. Upon further thinking about that problem, why do you only want to return to the main loop upon user input, but not unconditionally?
    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.

    ReplyDelete
  17. Robert O'Callahan6 November 2008 00:32

    It's a tradeoff between responsiveness and throughput. Yes, it would be best if animations etc kept playing at full speed, but that's not possible --- animations in the page content will usually depend on reflow anyway. And we don't want to hurt page load times by interrupting reflow any more than absolutely necessary.

    ReplyDelete
  18. You should also provide a pair of freeze/thaw operations if you let user interrupt so we can do some batch operation between reflow.

    ReplyDelete
  19. I don't think we want to be providing web-accessible freeze+thaw. Too much abuse potential there.

    ReplyDelete