Merge lp:~mhr3/unity/background-icon-loading into lp:unity

Proposed by Michal Hruby
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
Reviewer Review Type Date Requested Status
Gord Allott (community) Approve
Sam Spilsbury (community) Approve
Review via email: mp+89233@code.launchpad.net

Description of the change

Move all icon loading into background gio threads. (there was mainly issue with gtk_icon_info_load_icon which is doing synchronous IO and noticeably blocks the UI when using some themes)

To post a comment you must log in.
Revision history for this message
Sam Spilsbury (smspillaz) 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_push_job - if it processes jobs in serial or parallel.

review: Approve
Revision history for this message
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_push_job - if it processes
> 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.

Revision history for this message
Gord Allott (gordallott) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+}