GUI design thoughts, from both sides of the fence
Posted: Sat Oct 20, 2007 2:15 pm
I don't know if I'm mentioned this before, but there was a time when I wrote a small GUI toolkit for X11. In the process I learned quite a few things about GUI toolkits and event-based processing.
On the other side of the fence, I've since seen people misuse GUI APIs in ways which result in applications that generally work fine, but start lagging and "not responding" once there's enough things happening at the same time.
So, I've been thinking of writing an article or two about how to write GUI code that behaves well even on a slow CPU or when resources are running low, and that does stall because it needs to do IO or whatever.
But then the idea hit me, why not share some thoughts here as well, since while most OS probably aren't quite far enough that GUIs are really relevant, people here should be interested in systems programming, and maybe this whole mess will be helpful in some sense.
SO..
The absolute basic of a event-driven GUI ofcourse is the event-loop. That's some sort of a queue, where incoming events from whatever source (keyboard, mouse, windowing system, the application itself) get dumped, and then processed one at a time. Not much interesting in the loop, other than to mention, that there is one way to shoot yourself in the foot with the loop, and that's to write another special case loop. And the problem with special case loops is that you typically do that in order to not process all events in the normal fashion. And you want to always process any events, especially those you don't understand. Right? So the only exception I can see is modal dialogs, where you might want to drop user input (keyboard/mouse) going into the parent of the modal dialog, but even then you have to process any other events going into the parent (say redraws, internal messaging, ..).
Anyway, that's easy. The things I really want to talk about is stuff like drawing, which seems surprisingly hard to get right. Namely, every other programmer seems to think that when the application wants to draw, it asks the system for a handle to some graphics context, draws in there, and everything is fine. Which ofcourse is a seriously stupid thing to do in general, because drawing is typically expensive, and while drawing, you might get tons of other requests in the queue, which all require drawing, and your queue grows and grows and the feedback to user actions on screen lags more and more, and you either have to start dropping events or the whole thing crashes in an Out-Of-Memory condition.
The solution then, involves delaying the drawing in form of window invalidation. Which involves telling the system to send yourself a request to draw the window. Kinda. Except when you invalidate a window (or a region of a window) multiple times before actually handling the resulting request to draw, you won't get more than one such request, because multiple invalidations only make the window logically invalid once, and a single drawing operation is enough to make it valid again. Which means that as long as you can process any other messages faster than they arrive, you'll be able to clean up the queue after the drawing operations, independent of how long your drawing took.
And ofcourse you might then want to do other possibly expensive things related to drawing (complex layout calculations in response to window resizing come to mind) using a similar invalidation logic. With the same idea.
That's how a GUI can do very expensive drawing, yet behave acceptably on a 486 and Vesa framebuffer. Well, up to the point where drawing takes so long that it's essentially a synchronous operation and must be done in a separate thread with some double-buffering scheme (discussed below).
And then there those APIs which don't support the concept. Which means one has to build that on top of the API. And then people that don't understand the details will write apps on top of the API that freeze like it's not even funny when you trigger too much drawing too fast.
So mm.. if you design a GUI API, provide for proper invalidation of windows, and PLEASE PLEASE make it as hard as possible (IMHO impossible) to get a raw drawing context outside a repaint event handler. Thank you.
The other thing, that I wanted to talk about, is the fundamental incompability with an event-based GUI and synchronous IO and how the Windows Explorer in Vista freezes in response to SMB operations all the time. Not permanently, but until it timeouts or gets the operation done. Not nice.
So, mm.. you don't do synchronous IO in the GUI processing thread, ever. You shouldn't anyway. You schedule a background threads, tell it what you need done, it goes doing the stuff, sends an event notifying you that it finished (or failed) and you normal GUI event processing notices that such and such thing happened and acts accordingly. That way while the synchronous process blocks, the GUI thread can keep doing things like redrawing invalidated regions of the window, and waiting for the user to press "cancel".
And the reason I mention this, is because all too often one sees stupid APIs and make it totally impossible to send a result-nofication-event from the worker thread into the normal GUI threads event-queue. That's like.. a joke. No really, please, in an API design, make sure that the GUI thread never needs to block on anything. Some mutex locking for O(1) critical sections is totally fine, but just no unbounded waiting. It's impossible to design well behaving GUI apps then.
As for the user hitting cancel, well, sometimes the background thread could still keep blocking, and nobody would be any wiser, but in operations like transferring long files over the wire, it's trivial to check some cancel indicator ever once in a while to see if there's any point to keep going.. But that's ofcourse normal multi-threaded programming, and really hard to get right, so mm.. I'll not start discussing that now.
..
Was there something else? If you can think of other ways to shoot oneself in the foot with event-based GUI programming, please share your thoughts. If you disagree, share that as well, and I'll tell you that you are wrong.
Thank you.
On the other side of the fence, I've since seen people misuse GUI APIs in ways which result in applications that generally work fine, but start lagging and "not responding" once there's enough things happening at the same time.
So, I've been thinking of writing an article or two about how to write GUI code that behaves well even on a slow CPU or when resources are running low, and that does stall because it needs to do IO or whatever.
But then the idea hit me, why not share some thoughts here as well, since while most OS probably aren't quite far enough that GUIs are really relevant, people here should be interested in systems programming, and maybe this whole mess will be helpful in some sense.
SO..
The absolute basic of a event-driven GUI ofcourse is the event-loop. That's some sort of a queue, where incoming events from whatever source (keyboard, mouse, windowing system, the application itself) get dumped, and then processed one at a time. Not much interesting in the loop, other than to mention, that there is one way to shoot yourself in the foot with the loop, and that's to write another special case loop. And the problem with special case loops is that you typically do that in order to not process all events in the normal fashion. And you want to always process any events, especially those you don't understand. Right? So the only exception I can see is modal dialogs, where you might want to drop user input (keyboard/mouse) going into the parent of the modal dialog, but even then you have to process any other events going into the parent (say redraws, internal messaging, ..).
Anyway, that's easy. The things I really want to talk about is stuff like drawing, which seems surprisingly hard to get right. Namely, every other programmer seems to think that when the application wants to draw, it asks the system for a handle to some graphics context, draws in there, and everything is fine. Which ofcourse is a seriously stupid thing to do in general, because drawing is typically expensive, and while drawing, you might get tons of other requests in the queue, which all require drawing, and your queue grows and grows and the feedback to user actions on screen lags more and more, and you either have to start dropping events or the whole thing crashes in an Out-Of-Memory condition.
The solution then, involves delaying the drawing in form of window invalidation. Which involves telling the system to send yourself a request to draw the window. Kinda. Except when you invalidate a window (or a region of a window) multiple times before actually handling the resulting request to draw, you won't get more than one such request, because multiple invalidations only make the window logically invalid once, and a single drawing operation is enough to make it valid again. Which means that as long as you can process any other messages faster than they arrive, you'll be able to clean up the queue after the drawing operations, independent of how long your drawing took.
And ofcourse you might then want to do other possibly expensive things related to drawing (complex layout calculations in response to window resizing come to mind) using a similar invalidation logic. With the same idea.
That's how a GUI can do very expensive drawing, yet behave acceptably on a 486 and Vesa framebuffer. Well, up to the point where drawing takes so long that it's essentially a synchronous operation and must be done in a separate thread with some double-buffering scheme (discussed below).
And then there those APIs which don't support the concept. Which means one has to build that on top of the API. And then people that don't understand the details will write apps on top of the API that freeze like it's not even funny when you trigger too much drawing too fast.
So mm.. if you design a GUI API, provide for proper invalidation of windows, and PLEASE PLEASE make it as hard as possible (IMHO impossible) to get a raw drawing context outside a repaint event handler. Thank you.
The other thing, that I wanted to talk about, is the fundamental incompability with an event-based GUI and synchronous IO and how the Windows Explorer in Vista freezes in response to SMB operations all the time. Not permanently, but until it timeouts or gets the operation done. Not nice.
So, mm.. you don't do synchronous IO in the GUI processing thread, ever. You shouldn't anyway. You schedule a background threads, tell it what you need done, it goes doing the stuff, sends an event notifying you that it finished (or failed) and you normal GUI event processing notices that such and such thing happened and acts accordingly. That way while the synchronous process blocks, the GUI thread can keep doing things like redrawing invalidated regions of the window, and waiting for the user to press "cancel".
And the reason I mention this, is because all too often one sees stupid APIs and make it totally impossible to send a result-nofication-event from the worker thread into the normal GUI threads event-queue. That's like.. a joke. No really, please, in an API design, make sure that the GUI thread never needs to block on anything. Some mutex locking for O(1) critical sections is totally fine, but just no unbounded waiting. It's impossible to design well behaving GUI apps then.
As for the user hitting cancel, well, sometimes the background thread could still keep blocking, and nobody would be any wiser, but in operations like transferring long files over the wire, it's trivial to check some cancel indicator ever once in a while to see if there's any point to keep going.. But that's ofcourse normal multi-threaded programming, and really hard to get right, so mm.. I'll not start discussing that now.
..
Was there something else? If you can think of other ways to shoot oneself in the foot with event-based GUI programming, please share your thoughts. If you disagree, share that as well, and I'll tell you that you are wrong.
Thank you.