Mir

Merge lp:~vanvugt/mir/unfreeze-1 into lp:mir

Proposed by Daniel van Vugt
Status: Merged
Merged at revision: 2117
Proposed branch: lp:~vanvugt/mir/unfreeze-1
Merge into: lp:mir
Prerequisite: lp:~vanvugt/mir/workaround-1391261
Diff against target: 271 lines (+163/-51)
2 files modified
src/server/compositor/buffer_queue.cpp (+71/-43)
tests/unit-tests/compositor/test_buffer_queue.cpp (+92/-8)
To merge this branch: bzr merge lp:~vanvugt/mir/unfreeze-1
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Alan Griffiths Approve
Alexandros Frantzis (community) Approve
Review via email: mp+243115@code.launchpad.net

Commit message

Ensure frame dropping never drops the newest frame. Dropping the
newest frame causes stuttering at best (LP: #1379685) or in the case
of an event-driven app could make it appear to freeze indefinitely
(LP: #1396006).

Description of the change

If you want to see an immediate benefit of this branch, try glmark2 in fullscreen so that it uses bypass. Without the branch it stutters like it's at 30Hz. With this branch it's perfectly smooth.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Alexandros Frantzis (afrantzis) wrote :

Can confirm the visual improvement and the code seems sensible.

A couple of questions/concerns about the overallocation behavior:

135 + * 5. Overallocate; more buffers! Yes, see below.
136 + */
137 + auto const& buffer = gralloc->alloc_buffer(the_properties);

1. In framedropping mode, could we end up allocating more buffers than needed (e.g. because of some kind of spike) which we will then not release?

2. In framedropping mode, could we end up in a situation in which we continuously allocate buffers, eventually exhausting graphics memory? Could a malicious client force such behavior?

review: Needs Information
Revision history for this message
Daniel van Vugt (vanvugt) wrote :

(1) Yes a framedropping surface could end up overallocated if the compositor demand spikes and then subsides (e.g. fullscreen bypass and then restored to a window). However dynamic monitoring of real buffer demand is a tricky problem that I have a separate branch for (hopefully ready this year) and would prefer to keep separate. Also a very uncommon occurrence whose penalty is quite low. So that's an optimization more than a bug.

(2) That concerned me too. However an upper limit on number of buffers allocated is implicitly enforced by the behaviour of the rest of the class. A new buffer is only allocated if the free list is empty and the ready list is size 0 or 1. So a new buffer only comes into being if the compositors (and/or client) are behaving in a way that it's actually required. As soon as the lifecycle moves along a bit, we prefer to reuse buffers already in the free or ready lists, so no new buffers are created.

If you explicitly set a fixed upper limit on the number of buffers, that's actually option #4 described in the comments. It works and I have a branch for that too. But less preferable than what's in this branch I think.

Revision history for this message
Alexandros Frantzis (afrantzis) wrote :

OK. Looks good, then.

review: Approve
Revision history for this message
Alan Griffiths (alan-griffiths) wrote :

I share the above concerns, but this is an improvement on status quo

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/server/compositor/buffer_queue.cpp'
2--- src/server/compositor/buffer_queue.cpp 2014-11-24 02:16:00 +0000
3+++ src/server/compositor/buffer_queue.cpp 2014-12-01 02:09:23 +0000
4@@ -135,37 +135,6 @@
5 framedrop_policy = policy_provider.create_policy([this]
6 {
7 std::unique_lock<decltype(guard)> lock{guard};
8-
9- if (pending_client_notifications.empty())
10- {
11- /*
12- * This framedrop handler may be in the process of being dispatched
13- * when we try to cancel it by calling swap_unblocked() when we
14- * get a buffer to give back to the client. In this case we cannot
15- * cancel and this function may be called without any pending client
16- * notifications. This is a benign race that we can deal with by
17- * just ignoring the framedrop request.
18- */
19- return;
20- }
21-
22- if (ready_to_composite_queue.empty())
23- {
24- /*
25- * NOTE: This can only happen under two circumstances:
26- * 1) Client is single buffered. Don't do that, it's a bad idea.
27- * 2) Client already has a buffer, and is asking for a new one
28- * without submitting the old one.
29- *
30- * This shouldn't be exposed to the client as swap_buffers
31- * blocking, as the client already has something to render to.
32- *
33- * Our current implementation will never hit this case. If we
34- * later want clients to be able to own multiple buffers simultaneously
35- * then we might want to add an entry to the CompositorReport here.
36- */
37- return;
38- }
39 drop_frame(std::move(lock));
40 });
41 }
42@@ -198,7 +167,7 @@
43 }
44
45 /* Last resort, drop oldest buffer from the ready queue */
46- if (frame_dropping_enabled && !ready_to_composite_queue.empty())
47+ if (frame_dropping_enabled)
48 {
49 drop_frame(std::move(lock));
50 return;
51@@ -468,23 +437,82 @@
52 framedrop_policy->swap_unblocked();
53 give_buffer_to_client(buffer, std::move(lock));
54 }
55+ else if (!frame_dropping_enabled && buffers.size() > size_t(nbuffers))
56+ {
57+ /*
58+ * We're overallocated.
59+ * If frame_dropping_enabled, keep it that way to avoid having
60+ * to repeatedly reallocate. We must need the overallocation due to a
61+ * greedy compositor and insufficient nbuffers (LP: #1379685).
62+ * If not framedropping then we only overallocated to briefly
63+ * guarantee the framedropping policy and poke the client. Safe
64+ * to free it then because that's a rare occurrence.
65+ */
66+ for (auto i = buffers.begin(); i != buffers.end(); ++i)
67+ {
68+ if (i->get() == buffer)
69+ {
70+ buffers.erase(i);
71+ break;
72+ }
73+ }
74+ }
75 else
76 free_buffers.push_back(buffer);
77 }
78
79 void mc::BufferQueue::drop_frame(std::unique_lock<std::mutex> lock)
80 {
81- auto buffer_to_give = pop(ready_to_composite_queue);
82- /* Advance compositor buffer so it always points to the most recent
83- * client content
84- */
85- if (!contains(current_compositor_buffer, buffers_sent_to_compositor))
86- {
87- current_buffer_users.clear();
88- void const* const impossible_user_id = this;
89- current_buffer_users.push_back(impossible_user_id);
90- std::swap(buffer_to_give, current_compositor_buffer);
91- }
92+ // Make sure there is a client waiting for the frame before we drop it.
93+ // If not, then there's nothing to do.
94+ if (pending_client_notifications.empty())
95+ return;
96+
97+ mg::Buffer* buffer_to_give = nullptr;
98+
99+ if (!free_buffers.empty())
100+ { // We expect this to usually be empty, but always check free list first
101+ buffer_to_give = free_buffers.back();
102+ free_buffers.pop_back();
103+ }
104+ else if (!ready_to_composite_queue.empty() &&
105+ !contains(current_compositor_buffer, buffers_sent_to_compositor))
106+ {
107+ // Remember current_compositor_buffer is implicitly the front
108+ // of the ready queue.
109+ buffer_to_give = current_compositor_buffer;
110+ current_compositor_buffer = pop(ready_to_composite_queue);
111+ current_buffer_users.clear();
112+ void const* const impossible_user_id = this;
113+ current_buffer_users.push_back(impossible_user_id);
114+ }
115+ else if (ready_to_composite_queue.size() > 1)
116+ {
117+ buffer_to_give = pop(ready_to_composite_queue);
118+ }
119+ else
120+ {
121+ /*
122+ * Insufficient nbuffers for frame dropping? This means you're either
123+ * trying to use frame dropping with bypass/overlays/multimonitor or
124+ * have chosen nbuffers too low, or some combination thereof. So
125+ * consider the options...
126+ * 1. Crash. No, that's really unhelpful.
127+ * 2. Drop the visible frame. Probably not; that looks pretty awful.
128+ * Not just tearing but you'll see very ugly polygon rendering
129+ * artifacts.
130+ * 3. Drop the newest ready frame. Absolutely not; that will cause
131+ * indefinite freezes or at least stuttering.
132+ * 4. Just give a warning and carry on at regular frame rate
133+ * as if framedropping was disabled. That's pretty nice, but we
134+ * can do better still...
135+ * 5. Overallocate; more buffers! Yes, see below.
136+ */
137+ auto const& buffer = gralloc->alloc_buffer(the_properties);
138+ buffers.push_back(buffer);
139+ buffer_to_give = buffer.get();
140+ }
141+
142 give_buffer_to_client(buffer_to_give, std::move(lock));
143 }
144
145
146=== modified file 'tests/unit-tests/compositor/test_buffer_queue.cpp'
147--- tests/unit-tests/compositor/test_buffer_queue.cpp 2014-11-24 02:16:00 +0000
148+++ tests/unit-tests/compositor/test_buffer_queue.cpp 2014-12-01 02:09:23 +0000
149@@ -806,7 +806,7 @@
150 handle->release_buffer();
151 }
152
153- EXPECT_THAT(ids_acquired.size(), Eq(nbuffers));
154+ EXPECT_THAT(ids_acquired.size(), Ge(nbuffers));
155 }
156 }
157
158@@ -1050,6 +1050,87 @@
159 }
160 }
161
162+TEST_F(BufferQueueTest, framedropping_policy_never_drops_newest_frame)
163+{ // Regression test for LP: #1396006
164+ for (int nbuffers = 2; nbuffers <= max_nbuffers_to_test; ++nbuffers)
165+ {
166+ mtd::MockFrameDroppingPolicyFactory policy_factory;
167+ mc::BufferQueue q(nbuffers,
168+ allocator,
169+ basic_properties,
170+ policy_factory);
171+
172+ auto first = client_acquire_sync(q);
173+ q.client_release(first);
174+
175+ // Start rendering one (don't finish)
176+ auto d = q.compositor_acquire(nullptr);
177+ ASSERT_EQ(first, d.get());
178+
179+ auto second = client_acquire_sync(q);
180+ q.client_release(second);
181+
182+ // Client waits for a new frame
183+ auto end = client_acquire_async(q);
184+
185+ // Surface goes offscreen or occluded; trigger a timeout
186+ policy_factory.trigger_policies();
187+
188+ // If the queue is still willing to drop under these difficult
189+ // circumstances (and we don't mind if it doesn't), then ensure
190+ // it's never the newest frame that's been discarded.
191+ // That could be catastrophic as you never know if a client ever
192+ // will produce another frame.
193+ if (end->has_acquired_buffer())
194+ ASSERT_NE(second, end->buffer());
195+
196+ q.compositor_release(d);
197+ }
198+}
199+
200+TEST_F(BufferQueueTest, framedropping_surface_never_drops_newest_frame)
201+{ // Second regression test for LP: #1396006, LP: #1379685
202+ for (int nbuffers = 2; nbuffers <= max_nbuffers_to_test; ++nbuffers)
203+ {
204+ mc::BufferQueue q(nbuffers,
205+ allocator,
206+ basic_properties,
207+ policy_factory);
208+
209+ q.allow_framedropping(true);
210+
211+ // Fill 'er up
212+ std::vector<mg::Buffer*> order;
213+ for (int f = 0; f < nbuffers; ++f)
214+ {
215+ auto b = client_acquire_sync(q);
216+ order.push_back(b);
217+ q.client_release(b);
218+ }
219+
220+ // Composite all but one
221+ std::vector<std::shared_ptr<mg::Buffer>> compositing;
222+ for (int n = 0; n < nbuffers-1; ++n)
223+ {
224+ auto c = q.compositor_acquire(nullptr);
225+ compositing.push_back(c);
226+ ASSERT_EQ(order[n], c.get());
227+ }
228+
229+ // Ensure it's not the newest frame that gets dropped to satisfy the
230+ // client.
231+ auto end = client_acquire_async(q);
232+
233+ // The queue could solve this problem a few ways. It might choose to
234+ // defer framedropping till it's safe, or even allocate additional
235+ // buffers. We don't care which, just verify it's not losing the
236+ // latest frame. Because the screen could be indefinitely out of date
237+ // if that happens...
238+ ASSERT_TRUE(!end->has_acquired_buffer() ||
239+ end->buffer() != order.back());
240+ }
241+}
242+
243 TEST_F(BufferQueueTest, uncomposited_client_swaps_when_policy_triggered)
244 {
245 for (int nbuffers = 2;
246@@ -1238,15 +1319,18 @@
247 * so the next client request should not be satisfied until
248 * a compositor releases its buffers */
249 auto handle = client_acquire_async(q);
250- EXPECT_THAT(handle->has_acquired_buffer(), Eq(false));
251-
252- /* Release compositor buffers so that the client can get one */
253- for (auto const& buffer : buffers)
254+ /* ... unless the BufferQueue is overallocating. In that case it will
255+ * have succeeding in acquiring immediately.
256+ */
257+ if (!handle->has_acquired_buffer())
258 {
259- q.compositor_release(buffer);
260+ /* Release compositor buffers so that the client can get one */
261+ for (auto const& buffer : buffers)
262+ {
263+ q.compositor_release(buffer);
264+ }
265+ EXPECT_THAT(handle->has_acquired_buffer(), Eq(true));
266 }
267-
268- EXPECT_THAT(handle->has_acquired_buffer(), Eq(true));
269 }
270
271 TEST_F(BufferQueueTest, compositor_never_owns_client_buffers)

Subscribers

People subscribed via source and target branches