Re: Doubts about GPeriodic
- From: Havoc Pennington <hp pobox com>
- To: Owen Taylor <otaylor redhat com>
- Cc: Ryan Lortie <desrt desrt ca>, gtk-devel-list gnome org
- Subject: Re: Doubts about GPeriodic
- Date: Fri, 22 Oct 2010 13:37:36 -0400
Hi,
On Fri, Oct 22, 2010 at 10:24 AM, Owen Taylor <otaylor redhat com> wrote:
> Is painting well behaved? Inherently - no. We can easily get in
> situations where we can spend all our time painting and no time doing
> anything else.
That's the point of the up-to-5ms-of-dispatch thing (or the
wait-for-frame-complete thing), though, right? We can force painting
to be well behaved by having a period during each frame where painting
is not ready for dispatch or has a lower priority. During that period
anything else can run - even idles can run. (Or if we like, even
default idles can run but not low idles, or only default priority not
idles, could be implemented. Just has to be defined.)
I don't see the need to choose between painting-starves-everything and
everything-starves-painting.
The "dispatch everything but painting" gap can have a max length, not
a fixed length - if you do get everything else done you can proceed to
paint. (Well, that's what we do with the 5ms. I guess it might be
harder to do with a pure GMainLoop solution but you could do it, by
having the paint source dynamically change its priority or by having a
super-low-priority paint source that's always ready plus a
high-priority one that is only ready when frame-complete or after an
interval.)
The effect of max rather than fixed length "everything else" gap is
that if painting truly needs the whole frame interval (rather than
interval minus gap), painting may still keep up on those frames where
there's little else to do.
The idea is there's a period in which painting will be well-behaved by
yielding - it isn't allowed to defer everything else indefinitely /
100% of the time.
This changes your max time you theoretically have to paint from 1/fps
seconds to (1/fps - gap) seconds. However even if you can't paint in
(1/fps-gap) you will only lose a frame due to the gap when there's
really stuff to do inside the gap. And if you can't afford a
reasonable gap you probably really are hosed anyhow. There is already
a fixed max on paint time in order to look good, so that doesn't
change, the max is just tweaked (and can flex up to the absolute max,
1/fps, whenever possible).
The idea is that if your average paint-cpu and everything-else-cpu
adds up to less than 100% cpu, we shouldn't need to drop frames.
However with a starvation model, you drop frames even if you can keep
up on average, due to lumpiness, right? so that's the idea of only
yielding for a gap - if you get a lump in your everything-else, such
as a flood of messages, try to spread it over a few frames.
In practice I think that's what we're talking about. Say I have a
spinner animating smoothly, and I get some flood of IO or dbus or
whatever it is. Do I handle that flood in 30ms and my spinner hiccups,
or do I spread the flood in chunks over several frames, and my spinner
stays pretty. As an app developer I think what I want is the latter.
But I also don't want to wait for the spinner to go away before
handling this flood. I want to smooth the flood over multiple frames,
but not defer it indefinitely, I want to make progress on every frame.
I don't think that can be expressed by just having a fixed priority on
paint and a fixed priority on my handler; there has to be some way in
which the priorities "flip" during the frame.
> Are IO completions and IPC well behaved? Well that's really up to the
> application.... however, they have to be *somewhat* well behaved in any
> case.
What's hard I think is to make them well behaved in the aggregate and
on every single frame.
i.e. it's hard to avoid just randomly having "too much" to dispatch
from time to time, then you drop 3 frames, it just looks bad. But as
long as you're OK *on average* this can be solved by spreading the
dispatch of everything else across more than one frame, instead of
insisting on doing it all at once.
If you aren't OK on average, that's a problem the app is just going to
have to solve.
> If I have a GIO async callback that fills a treeview, there is one
> pathology where my callback gets called so frequently that we never get
> get to repaint. But what may happen instead is that I get so much data
> in a *single* callback that I block the main loop for an unacceptably
> long period of time. So we always will have the requirement that
> callbacks from the main loop must be *individually* short.
Yes. I think things can be made to work pretty well with just this requirement.
> Making IO
> completions and IPC highest priority makes this requirement a bit more
> stringent - it means that callbacks from the main loop must be *in
> aggregate* short. That callbacks from the mainloop aren't allowed to do
> expensive stuff, but instead must queue it up for an idle at lower
> priority.
A priority lower than paint is the right priority for most stuff, IMO,
as long as we're going to dispatch that lower priority at least for
some gap per frame. If the presence of an animation means that we
aren't going to dispatch lower-than-paint sources until we aren't
animating, then at that point I think most stuff would have to be
higher than paint, which breaks paint.
Doing only painting or only other stuff in any frame just isn't right.
What's right is to do painting plus "up to a fixed amount of
everything else which on average is all of everything else, but can be
less than all sometimes if there's too much" on each frame.
> While a two-part system like this sounds like a huge pain for
> application writers - it does have the big advantage that everybody gets
> a say. If we just cut things off after a fixed time and started
> painting, then we could end up in a situation where we were just filling
> the treeview and painting, and never processing D-Bus events at all.
If painting is a higher than default priority you could still add
sources at an even higher priority, or you could hook into the paint
clock in the same place events, resize, etc. hook in to force some
queue to be drained before allowing paint to proceed.
Also if you have a handler that just does your whole queue of whatever
at once, it effectively does run on every frame and compress it all,
even if it's an idle - since the main loop can't interrupt a dispatch
in progress, and the gap means that we'll probably run the dispatch
handler once on all nonpaint sources in a typical frame.
The litl "manual main loop" (not using GMainLoop, manually iterating)
always forces at least one nonblocking iteration in between draining
the event queue and painting, even if 5ms was spent on the event
queue, as paranoia to be sure some progress is always made. So a
default-priority handler that drains the whole queue always
compresses.
> Well, sort of - the main loop algorithms are designed to protect against
> this. *All* sources at the current priority are collected and dispatched
> once before we check again for higher priority sources. But since
> sources have different granularities and policies, the effect would be
> some unpredictable. The behavior of the GDK event source is to unqueue
> and dispatch one X event source per pass of the main loop, D-Bus and GIO
> probably do different things.
dbus works like the GDK source (because it copied it). One message per
dispatch at default priority. I'm not sure how gdbus works.
I think what dbus does works well, as long as painting is 1) above
default priority and 2) not ready for dispatch for at least some time
during each frame length.
The thing is that as long as "everything but painting" is basically
sane, then the "up to 5ms" or "while waiting for vsync" gap is going
to be enough to dispatch everything. If you get a flood of dbus
messages or whatever though, then you start spreading those over
frames (but still making progress) instead of losing frames
indefinitely until you make it through the queue.
It's just less bad, to spread dispatching stuff out over a few frames
if you get a flood, than it is to drop a few frames.
> Right now event compression in Clutter counts on events getting unqueued
> from the X socket at the default priority and then stored in an internal
> queue for compression and dispatching before painting. Going to a system
> where painting was higher priority than normal stuff would actually
> require 3 priorities: Event queueing, then painting, then everything
> else. [*]
Events have to be worked into this so they get done before painting,
but that seems straightforward. It can be done with an event source
even higher priority than painting, or by having a "do all events now"
hook that is part of painting, I think.
I guess in litl shell we even have a time limit on the event
compression; we will stop compressing events if we exceed the 5ms.
Separate discussion if that's desirable. We count event processing
against the "yield gap" time limit though we do always iterate
non-event stuff at least once.
> But can we say for sure that nothing coming in over D-Bus should be
> treated like an event? Generally, anything where the bulk of the work is
> compressible is better to handle before painting.
No, but I'd suggest this is rare, and easier for apps to special-case,
compared to it being essentially wrong to ever do anything at default
priority.
> An example: If we have a change notification coming over D-Bus which is
> compressible - it's cheap other than a triggered repaint. Say updating
> the text of label. And combine that with our GtkTreeView filler, then we
> might have:
>
> Fill chunk of tree view
> Change notification
I'd tend to handle this by having a way in gdbus to say "yank me all
messages matching xyz out of the queue now" and then you would add
either a higher-priority-than-paint handler or a hook into the clock
before paint, and in that handler/hook you'd yank the change
notifications from the queue. Or you could do it as, if you handle one
change notification, at that point you go ahead and yank any other
pending ones out of the queue.
> Yep. But this cuts both ways - if we handle painting first and
> everything else uniformly second, then we can't *up* the priority of
> anything coming over D-Bus.
Either higher-priority source or a hook in the clock or queue lookahead, right?
> I'm not sure how well the model you are proposing here:
>
> - A single GSource dispatch cycle is a single queue element
> - Each queue element is individually short
> - All queue elements in aggregate may be long
>
> Really corresponds to the reality of GSource usage.
It's the way dbus-glib worked anyhow. I'm not sure how gio works.
(Though I think it does install default-priority handlers for the
GAsyncResult returns, I'm not sure if the usual way to read a stream
would be to get a series of chunks back into the main thread.)
> - We can do a reasonable job scheduling painting versus everything
> else. (I think about the best you can do is to try and spend 50%
> of the time painting and 50% of the time doing everything else.
> Anything that's more sophisticated than that is tweaking around
> the edges.)
Basing this on waiting for frame-complete seems nicer than waiting
some fixed interval though right? The fixed interval is hopefully not
needed.
If stuck with a fixed length, I'd say more like 25/75 (which is
roughly the 5ms) - just based on painting usually needs a lot more
time than "other stuff" does, in my experience, and because it's more
OK to slow down other stuff than it is to slow down painting. But
remember, if there's nothing to dispatch, just go ahead and start
painting - it isn't required to wait the "yield to other stuff"
interval if there's nothing to yield to. The "yield to nonpainting
stuff" gap is a max length not a fixed time.
>> Why not make paint priority greater than the default priority, and so
>> most things should be default, and idle is reserved for things that
>> it's acceptable to starve?
>
> I guess the basic question here is whether "most stuff" is updating the
> text of a label or whether most stuff is adding 10000 elements to a
> GtkTreeView. If most things that applications are doing in response to
> timeouts/IO completions/IPC are cheap and take almost no time, then we
> gain compression and efficiency by doing them before painting and we can
> ask the applications to code specially for the rest.
If most things are cheap and take no time, then they will run in the
yield gap and then painting can start right away as soon as there's
nothing left to dispatch. That's your average case and it works. But
we can also make the lumpy flood case work - just stop iterating if
it's time to paint now, finish doing the leftover work on the next
frame. Spread out the lump. Why not?
Almost everything Just Works in this scenario. The only thing you have
to special case is if compression is important to you, and "compress
during the yield gap" is not good enough (i.e. if you want to compress
absolutely everything, not just stuff that happens within a certain
time), then you need to run the queue in your dispatch, rather than
dispatching one item per iteration.
> Note: I've talked about "idles" above for deferring to the lower
> priority, but something more sophisticated might be useful - anything
> deferred should be making *some* progress - it shouldn't be completely
> starved. It might also be useful to be able to do
> 'while (!my_deferred_work_function_should_yield())
> { add row to tree view }.'
Exactly, yep. I just don't think paint-starves or other-stuff-starves
are what's wanted.
Havoc
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]