Merge lp:~mhr3/unity/background-icon-loading into lp:unity
- background-icon-loading
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gord Allott | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 1860 | ||||
Proposed branch: | lp:~mhr3/unity/background-icon-loading | ||||
Merge into: | lp:unity | ||||
Diff against target: |
501 lines (+285/-89) 3 files modified
plugins/unityshell/src/IconLoader.cpp (+89/-89) tests/CMakeLists.txt (+5/-0) tests/test_icon_loader.cpp (+191/-0) |
||||
To merge this branch: | bzr merge lp:~mhr3/unity/background-icon-loading | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gord Allott (community) | Approve | ||
Sam Spilsbury (community) | Approve | ||
Review via email: mp+89233@code.launchpad.net |
Commit message
Description of the change
Move all icon loading into background gio threads. (there was mainly issue with gtk_icon_
Michal Hruby (mhr3) wrote : | # |
> All ok on the code and testing.
>
> 161 + // careful here this is running in non-main thread
> 162 + if (task->icon_info)
> 163 + {
>
> Maybe here you might want to use mutexes to synchronize the threads ? I'm not
> sure about the exact semantics of g_io_scheduler_
> jobs in serial or parallel.
There is no synchronization needed, once this method starts icon_info is either already set to valid GtkIconInfo instance (now owned by the task), or NULL in case of URI request. This is merely a reminder for anyone wanting to touch that code.
As for serial vs parallel - it's both, since GIOScheduler uses a thread pool.
Gord Allott (gordallott) : | # |
Preview Diff
1 | === modified file 'plugins/unityshell/src/IconLoader.cpp' |
2 | --- plugins/unityshell/src/IconLoader.cpp 2011-09-23 15:06:46 +0000 |
3 | +++ plugins/unityshell/src/IconLoader.cpp 2012-01-23 11:29:26 +0000 |
4 | @@ -85,6 +85,9 @@ |
5 | IconLoaderCallback slot; |
6 | Handle handle; |
7 | Impl* self; |
8 | + GtkIconInfo* icon_info; |
9 | + GdkPixbuf* result; |
10 | + glib::Error error; |
11 | |
12 | IconLoaderTask(IconLoaderRequestType type_, |
13 | std::string const& data_, |
14 | @@ -95,6 +98,7 @@ |
15 | Impl* self_) |
16 | : type(type_), data(data_), size(size_), key(key_) |
17 | , slot(slot_), handle(handle_), self(self_) |
18 | + , icon_info(NULL), result(NULL) |
19 | {} |
20 | }; |
21 | |
22 | @@ -116,17 +120,19 @@ |
23 | unsigned size, |
24 | IconLoaderCallback slot); |
25 | |
26 | + // these methods might run asynchronously |
27 | bool ProcessTask(IconLoaderTask* task); |
28 | bool ProcessIconNameTask(IconLoaderTask* task); |
29 | bool ProcessGIconTask(IconLoaderTask* task); |
30 | - |
31 | - // URI processing is async. |
32 | bool ProcessURITask(IconLoaderTask* task); |
33 | - void ProcessURITaskReady(IconLoaderTask* task, char* contents, gsize length); |
34 | - static void LoadContentsReady(GObject* object, GAsyncResult* res, IconLoaderTask* task); |
35 | + |
36 | + // Loading/rendering of pixbufs is done in a separate thread |
37 | + static gboolean LoaderJobFunc(GIOSchedulerJob* job, GCancellable *canc, |
38 | + IconLoaderTask *task); |
39 | + static gboolean LoadIconComplete(IconLoaderTask* task); |
40 | |
41 | // Loop calls the iteration function. |
42 | - static bool Loop(Impl* self); |
43 | + static gboolean Loop(Impl* self); |
44 | bool Iteration(); |
45 | |
46 | private: |
47 | @@ -312,26 +318,17 @@ |
48 | |
49 | bool IconLoader::Impl::ProcessIconNameTask(IconLoaderTask* task) |
50 | { |
51 | - GdkPixbuf* pixbuf = nullptr; |
52 | GtkIconInfo* info = gtk_icon_theme_lookup_icon(theme_, |
53 | task->data.c_str(), |
54 | task->size, |
55 | (GtkIconLookupFlags)0); |
56 | if (info) |
57 | { |
58 | - glib::Error error; |
59 | + task->icon_info = info; |
60 | + g_io_scheduler_push_job ((GIOSchedulerJobFunc) LoaderJobFunc, |
61 | + task, NULL, G_PRIORITY_HIGH_IDLE, NULL); |
62 | |
63 | - pixbuf = gtk_icon_info_load_icon(info, &error); |
64 | - if (GDK_IS_PIXBUF(pixbuf)) |
65 | - { |
66 | - cache_[task->key] = pixbuf; |
67 | - } |
68 | - else |
69 | - { |
70 | - LOG_WARNING(logger) << "Unable to load icon " << task->data |
71 | - << " at size " << task->size << ": " << error; |
72 | - } |
73 | - gtk_icon_info_free(info); |
74 | + return false; |
75 | } |
76 | else |
77 | { |
78 | @@ -339,14 +336,12 @@ |
79 | << " at size " << task->size; |
80 | } |
81 | |
82 | - task->slot(task->data, task->size, pixbuf); |
83 | + task->slot(task->data, task->size, nullptr); |
84 | return true; |
85 | } |
86 | |
87 | bool IconLoader::Impl::ProcessGIconTask(IconLoaderTask* task) |
88 | { |
89 | - GdkPixbuf* pixbuf = NULL; |
90 | - |
91 | glib::Error error; |
92 | glib::Object<GIcon> icon(::g_icon_new_for_string(task->data.c_str(), &error)); |
93 | |
94 | @@ -368,18 +363,11 @@ |
95 | (GtkIconLookupFlags)0); |
96 | if (info) |
97 | { |
98 | - pixbuf = gtk_icon_info_load_icon(info, &error); |
99 | + task->icon_info = info; |
100 | + g_io_scheduler_push_job ((GIOSchedulerJobFunc) LoaderJobFunc, |
101 | + task, NULL, G_PRIORITY_HIGH_IDLE, NULL); |
102 | |
103 | - if (GDK_IS_PIXBUF(pixbuf)) |
104 | - { |
105 | - cache_[task->key] = pixbuf; |
106 | - } |
107 | - else |
108 | - { |
109 | - LOG_WARNING(logger) << "Unable to load icon " << task->data |
110 | - << " at size " << task->size << ": " << error; |
111 | - } |
112 | - gtk_icon_info_free(info); |
113 | + return false; |
114 | } |
115 | else |
116 | { |
117 | @@ -409,58 +397,91 @@ |
118 | << " at size " << task->size << ": " << error; |
119 | } |
120 | |
121 | - task->slot(task->data, task->size, pixbuf); |
122 | + task->slot(task->data, task->size, nullptr); |
123 | return true; |
124 | } |
125 | |
126 | bool IconLoader::Impl::ProcessURITask(IconLoaderTask* task) |
127 | { |
128 | - glib::Object<GFile> file(g_file_new_for_uri(task->data.c_str())); |
129 | - |
130 | - g_file_load_contents_async(file, |
131 | - NULL, |
132 | - (GAsyncReadyCallback)LoadContentsReady, |
133 | - task); |
134 | + g_io_scheduler_push_job ((GIOSchedulerJobFunc) LoaderJobFunc, |
135 | + task, NULL, G_PRIORITY_HIGH_IDLE, NULL); |
136 | |
137 | return false; |
138 | } |
139 | |
140 | -void IconLoader::Impl::ProcessURITaskReady(IconLoaderTask* task, |
141 | - char* contents, |
142 | - gsize length) |
143 | -{ |
144 | - GInputStream* stream = g_memory_input_stream_new_from_data(contents, length, NULL); |
145 | - |
146 | - glib::Error error; |
147 | - glib::Object<GdkPixbuf> pixbuf(gdk_pixbuf_new_from_stream_at_scale(stream, |
148 | - -1, |
149 | - task->size, |
150 | - true, |
151 | - NULL, |
152 | - &error)); |
153 | - if (error) |
154 | - { |
155 | - LOG_WARNING(logger) << "Unable to create pixbuf from input stream for " |
156 | - << task->data << " at size " << task->size << ": " << error; |
157 | +gboolean IconLoader::Impl::LoaderJobFunc(GIOSchedulerJob* job, |
158 | + GCancellable *canc, |
159 | + IconLoaderTask *task) |
160 | +{ |
161 | + // careful here this is running in non-main thread |
162 | + if (task->icon_info) |
163 | + { |
164 | + task->result = gtk_icon_info_load_icon(task->icon_info, &task->error); |
165 | + |
166 | + gtk_icon_info_free (task->icon_info); |
167 | + task->icon_info = NULL; |
168 | + } |
169 | + else if (task->type == REQUEST_TYPE_URI) |
170 | + { |
171 | + glib::Object<GFile> file(g_file_new_for_uri(task->data.c_str())); |
172 | + glib::String contents; |
173 | + gsize length = 0; |
174 | + |
175 | + if (g_file_load_contents(file, canc, &contents, &length, |
176 | + NULL, &task->error)) |
177 | + { |
178 | + glib::Object<GInputStream> stream( |
179 | + g_memory_input_stream_new_from_data(contents.Value(), length, NULL)); |
180 | + |
181 | + task->result = gdk_pixbuf_new_from_stream_at_scale(stream, |
182 | + -1, |
183 | + task->size, |
184 | + TRUE, |
185 | + canc, |
186 | + &task->error); |
187 | + g_input_stream_close(stream, canc, NULL); |
188 | + } |
189 | + } |
190 | + |
191 | + g_io_scheduler_job_send_to_mainloop_async (job, |
192 | + (GSourceFunc) LoadIconComplete, |
193 | + task, |
194 | + NULL); |
195 | + |
196 | + return FALSE; |
197 | +} |
198 | + |
199 | +// this will be invoked back in the thread from which push_job was called |
200 | +gboolean IconLoader::Impl::LoadIconComplete(IconLoaderTask* task) |
201 | +{ |
202 | + if (GDK_IS_PIXBUF(task->result)) |
203 | + { |
204 | + task->self->cache_[task->key] = task->result; |
205 | } |
206 | else |
207 | { |
208 | - cache_[task->key] = pixbuf; |
209 | + LOG_WARNING(logger) << "Unable to load icon " << task->data |
210 | + << " at size " << task->size << ": " << task->error; |
211 | } |
212 | |
213 | - task->slot(task->data, task->size, pixbuf); |
214 | - g_input_stream_close(stream, NULL, NULL); |
215 | + task->slot(task->data, task->size, task->result); |
216 | + |
217 | + // this was all async, we need to erase ourselves from the task_map |
218 | + task->self->task_map_.erase(task->handle); |
219 | + delete task; |
220 | + |
221 | + return FALSE; |
222 | } |
223 | |
224 | bool IconLoader::Impl::Iteration() |
225 | { |
226 | - static const int MAX_MICRO_SECS = 10000; |
227 | + static const int MAX_MICRO_SECS = 1000; |
228 | util::Timer timer; |
229 | |
230 | bool queue_empty = tasks_.empty(); |
231 | |
232 | - while (!queue_empty && |
233 | - (timer.ElapsedMicroSeconds() < MAX_MICRO_SECS)) |
234 | + // always do at least one iteration if the queue isn't empty |
235 | + while (!queue_empty) |
236 | { |
237 | IconLoaderTask* task = tasks_.front(); |
238 | |
239 | @@ -472,6 +493,8 @@ |
240 | |
241 | tasks_.pop(); |
242 | queue_empty = tasks_.empty(); |
243 | + |
244 | + if (timer.ElapsedMicroSeconds() >= MAX_MICRO_SECS) break; |
245 | } |
246 | |
247 | LOG_DEBUG(logger) << "Iteration done, queue size now at " << tasks_.size(); |
248 | @@ -487,33 +510,10 @@ |
249 | } |
250 | |
251 | |
252 | -bool IconLoader::Impl::Loop(IconLoader::Impl* self) |
253 | -{ |
254 | - return self->Iteration(); |
255 | -} |
256 | - |
257 | -void IconLoader::Impl::LoadContentsReady(GObject* obj, |
258 | - GAsyncResult* res, |
259 | - IconLoaderTask* task) |
260 | -{ |
261 | - glib::String contents; |
262 | - glib::Error error; |
263 | - gsize length = 0; |
264 | - |
265 | - if (g_file_load_contents_finish(G_FILE(obj), res, &contents, &length, NULL, &error)) |
266 | - { |
267 | - task->self->ProcessURITaskReady(task, contents.Value(), length); |
268 | - } |
269 | - else |
270 | - { |
271 | - LOG_WARNING(logger) << "Unable to load contents of " |
272 | - << task->data << ": " << error; |
273 | - task->slot(task->data, task->size, nullptr); |
274 | - } |
275 | - task->self->task_map_.erase(task->handle); |
276 | - delete task; |
277 | -} |
278 | - |
279 | +gboolean IconLoader::Impl::Loop(IconLoader::Impl* self) |
280 | +{ |
281 | + return self->Iteration() ? TRUE : FALSE; |
282 | +} |
283 | |
284 | IconLoader::IconLoader() |
285 | : pimpl(new Impl()) |
286 | |
287 | === modified file 'tests/CMakeLists.txt' |
288 | --- tests/CMakeLists.txt 2012-01-17 13:31:52 +0000 |
289 | +++ tests/CMakeLists.txt 2012-01-23 11:29:26 +0000 |
290 | @@ -184,10 +184,15 @@ |
291 | test_dashview_impl.cpp |
292 | test_texture_cache.cpp |
293 | test_main.cpp |
294 | + test_icon_loader.cpp |
295 | ${UNITY_SRC}/DashViewPrivate.cpp |
296 | ${UNITY_SRC}/DashViewPrivate.h |
297 | ${UNITY_SRC}/TextureCache.cpp |
298 | ${UNITY_SRC}/TextureCache.h |
299 | + ${UNITY_SRC}/IconLoader.cpp |
300 | + ${UNITY_SRC}/IconLoader.h |
301 | + ${UNITY_SRC}/Timer.cpp |
302 | + ${UNITY_SRC}/Timer.h |
303 | ) |
304 | target_link_libraries(test-gtest ${GTEST_BOTH_LIBRARIES}) |
305 | add_test(UnityGTest test-gtest) |
306 | |
307 | === added file 'tests/test_icon_loader.cpp' |
308 | --- tests/test_icon_loader.cpp 1970-01-01 00:00:00 +0000 |
309 | +++ tests/test_icon_loader.cpp 2012-01-23 11:29:26 +0000 |
310 | @@ -0,0 +1,191 @@ |
311 | +/* |
312 | + * Copyright 2012 Canonical Ltd. |
313 | + * |
314 | + * This program is free software: you can redistribute it and/or modify it |
315 | + * under the terms of the GNU General Public License version 3, as published |
316 | + * by the Free Software Foundation. |
317 | + * |
318 | + * This program is distributed in the hope that it will be useful, but |
319 | + * WITHOUT ANY WARRANTY; without even the implied warranties of |
320 | + * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR |
321 | + * PURPOSE. See the GNU General Public License for more details. |
322 | + * |
323 | + * You should have received a copy of the GNU General Public License |
324 | + * version 3 along with this program. If not, see |
325 | + * <http://www.gnu.org/licenses/> |
326 | + * |
327 | + * Authored by: Michal Hruby <michal.hruby@canonical.com> |
328 | + */ |
329 | + |
330 | +#include <gmock/gmock.h> |
331 | + |
332 | +#include "IconLoader.h" |
333 | + |
334 | +using namespace testing; |
335 | +using namespace unity; |
336 | + |
337 | +namespace |
338 | +{ |
339 | +bool IsValidPixbuf(GdkPixbuf *pixbuf) |
340 | +{ |
341 | + return GDK_IS_PIXBUF (pixbuf); |
342 | +} |
343 | + |
344 | +gboolean TimeoutReached (gpointer data) |
345 | +{ |
346 | + bool *b = static_cast<bool*>(data); |
347 | + |
348 | + *b = true; |
349 | + |
350 | + return FALSE; |
351 | +} |
352 | + |
353 | +struct LoadResult |
354 | +{ |
355 | + GdkPixbuf *pixbuf; |
356 | + bool got_callback; |
357 | + |
358 | + LoadResult() : pixbuf(NULL), got_callback(false) {} |
359 | + void IconLoaded(std::string const& icon_name, unsigned size, |
360 | + GdkPixbuf *buf) |
361 | + { |
362 | + pixbuf = buf; |
363 | + |
364 | + got_callback = true; |
365 | + } |
366 | +}; |
367 | + |
368 | +TEST(TestIconLoader, TestGetDefault) |
369 | +{ |
370 | + // we need to initialize gtk |
371 | + int args_cnt = 0; |
372 | + gtk_init (&args_cnt, NULL); |
373 | + |
374 | + IconLoader::GetDefault(); |
375 | +} |
376 | + |
377 | +TEST(TestIconLoader, TestGetOneIcon) |
378 | +{ |
379 | + LoadResult load_result; |
380 | + IconLoader& icon_loader = IconLoader::GetDefault(); |
381 | + volatile bool timeout_reached = false; |
382 | + |
383 | + icon_loader.LoadFromIconName("gedit-icon", 48, sigc::mem_fun(load_result, |
384 | + &LoadResult::IconLoaded)); |
385 | + |
386 | + guint tid = g_timeout_add (10000, TimeoutReached, (gpointer)(&timeout_reached)); |
387 | + while (!timeout_reached && !load_result.got_callback) |
388 | + { |
389 | + g_main_context_iteration (NULL, TRUE); |
390 | + } |
391 | + |
392 | + EXPECT_TRUE(load_result.got_callback); |
393 | + EXPECT_TRUE(IsValidPixbuf(load_result.pixbuf)); |
394 | + |
395 | + g_source_remove (tid); |
396 | +} |
397 | + |
398 | +TEST(TestIconLoader, TestGetManyIcons) |
399 | +{ |
400 | + std::vector<LoadResult> results; |
401 | + IconLoader& icon_loader = IconLoader::GetDefault(); |
402 | + volatile bool timeout_reached = false; |
403 | + int i = 0; |
404 | + int icon_count; |
405 | + |
406 | + GList *icons = gtk_icon_theme_list_icons (gtk_icon_theme_get_default (), |
407 | + "Applications"); |
408 | + // loading 100 icons should suffice |
409 | + icon_count = MIN (100, g_list_length (icons)); |
410 | + results.resize (icon_count); |
411 | + for (GList *it = icons; it != NULL; it = it->next) |
412 | + { |
413 | + const char *icon_name = static_cast<char*>(it->data); |
414 | + icon_loader.LoadFromIconName(icon_name, 48, sigc::mem_fun(results[i++], |
415 | + &LoadResult::IconLoaded)); |
416 | + if (i >= icon_count) break; |
417 | + } |
418 | + |
419 | + guint tid = g_timeout_add (30000, TimeoutReached, (gpointer)(&timeout_reached)); |
420 | + while (!timeout_reached) |
421 | + { |
422 | + g_main_context_iteration (NULL, TRUE); |
423 | + bool all_loaded = true; |
424 | + for (auto loader: results) |
425 | + { |
426 | + all_loaded &= loader.got_callback; |
427 | + if (!all_loaded) break; |
428 | + } |
429 | + if (all_loaded) break; |
430 | + } |
431 | + |
432 | + for (auto load_result: results) |
433 | + { |
434 | + EXPECT_TRUE(load_result.got_callback); |
435 | + EXPECT_TRUE(IsValidPixbuf(load_result.pixbuf)); |
436 | + } |
437 | + |
438 | + g_source_remove (tid); |
439 | +} |
440 | + |
441 | +TEST(TestIconLoader, TestCancelSome) |
442 | +{ |
443 | + std::vector<LoadResult> results; |
444 | + std::vector<int> handles; |
445 | + IconLoader& icon_loader = IconLoader::GetDefault(); |
446 | + volatile bool timeout_reached = false; |
447 | + int i = 0; |
448 | + int icon_count; |
449 | + |
450 | + GList *icons = gtk_icon_theme_list_icons (gtk_icon_theme_get_default (), |
451 | + "Emblems"); |
452 | + // loading 100 icons should suffice |
453 | + icon_count = MIN (100, g_list_length (icons)); |
454 | + results.resize (icon_count); |
455 | + handles.resize (icon_count); |
456 | + for (GList *it = icons; it != NULL; it = it->next) |
457 | + { |
458 | + const char *icon_name = static_cast<char*>(it->data); |
459 | + int handle = icon_loader.LoadFromIconName(icon_name, 48, sigc::mem_fun( |
460 | + results[i], &LoadResult::IconLoaded)); |
461 | + handles[i++] = handle; |
462 | + if (i >= icon_count) break; |
463 | + } |
464 | + |
465 | + // disconnect every other handler |
466 | + for (i = 0; i < icon_count; i += 2) |
467 | + { |
468 | + icon_loader.DisconnectHandle(handles[i]); |
469 | + } |
470 | + |
471 | + guint tid = g_timeout_add (30000, TimeoutReached, (gpointer)(&timeout_reached)); |
472 | + while (!timeout_reached) |
473 | + { |
474 | + g_main_context_iteration (NULL, TRUE); |
475 | + bool all_loaded = true; |
476 | + for (int i = 1; i < icon_count; i += 2) |
477 | + { |
478 | + all_loaded &= results[i].got_callback; |
479 | + if (!all_loaded) break; |
480 | + } |
481 | + if (all_loaded) break; |
482 | + } |
483 | + |
484 | + for (i = 0; i < icon_count; i++) |
485 | + { |
486 | + if (i % 2) |
487 | + { |
488 | + EXPECT_TRUE(results[i].got_callback); |
489 | + EXPECT_TRUE(IsValidPixbuf(results[i].pixbuf)); |
490 | + } |
491 | + else |
492 | + { |
493 | + EXPECT_FALSE(results[i].got_callback); |
494 | + } |
495 | + } |
496 | + |
497 | + g_source_remove (tid); |
498 | +} |
499 | + |
500 | + |
501 | +} |
All ok on the code and testing.
161 + // careful here this is running in non-main thread
162 + if (task->icon_info)
163 + {
Maybe here you might want to use mutexes to synchronize the threads ? I'm not sure about the exact semantics of g_io_scheduler_ push_job - if it processes jobs in serial or parallel.