Речь пойдет о unity-launcher-editor — редакторе элементов и контекстного меню (quicklists) панели Unity для Ubuntu. Редактор написан с использованием связки python+gtk, с обязанностями справляется сносно. Раздражает время запуска: до появления главного окна проходит непозволительно много времени.
Можно пенять на Python, Unity или разработчиков, а можно попробовать разобраться в чем же дело, проект ведь с открытым кодом. Во время «исследования» делал пометки, которые и легли в основу этой заметки. Любопытных прошу под кат.
Смотрим под капот с помощью стандартного профайлера:
Результат:
Ого! 50 секунд на запуск небольшого приложения.
Судя по всему, что-то неладное происходит в модуле
Беглый просмотр кода подтверждает догадку, что функция
Начнем с
Видимо в панели находятся 24 элемента для каждого из которых вызывается
Первое, что бросается в глаза, это строка:
Иконка типа image-missing одна для всех, поэтому нет необходимости каждый раз ее искать. Сделаем ее атрибутом класса. В методе
и заменяем соответствующую строку в
Посмотрим как изменилось время запуска:
Результат2:
Результат несколько неожиданный, поэтому перепроверил(с изменениями и без): стабильно минус 35-40 секунд.
9 секунд гораздо меньше раздражают, но тем не менее раздражают.
Попробуем еще улучшить.
IconManager каждый раз создается как отдельный объект (и всегда без аргументов), сделаем его атрибутом класса.
В
Все вхождения
Проверяем:
Результат3:
Окно появляется практически мгновенно. Общее время почти две секунды, однако оно включает также закрытие окна с помощью мыши. Лучше ориентироваться на строку
На этом можно и остановиться.
Небольшие изменения привели к уменьшению времени запуска приложения с 46 секунд до 0.3 (для
Изменения отправлены разработчикам.
Открытый код не идеален, но каждый может его улучшить потратив совсем немного своего времени.
UPD:
Невыясненными остались два вопроса:
Косвенным виновником стал объект
В конструкторе IconManager-а расширяется список путей, в которых происходит поиск иконок. Все изменения сохраняются на время выполнения приложения, хотя Gtk.IconTheme.get_default() и не является Singleton-ом. В моем случае каждая инициализация IconManager-а увеличивает список на 67 элементов, что приводит к изменению его размера примерно в 300 раз (с 10 до 2985).
Изменив несколько строк в конструкторе можно уменьшить время запуска до 0.8 секунды(без предыдущих оптимизаций) и 0.6(с оптимизациями). Предыдущие изменения стали «косметическими».
Bug создан, patch отправлен.
Можно пенять на Python, Unity или разработчиков, а можно попробовать разобраться в чем же дело, проект ведь с открытым кодом. Во время «исследования» делал пометки, которые и легли в основу этой заметки. Любопытных прошу под кат.
Шаг 1
Смотрим под капот с помощью стандартного профайлера:
python -m cProfile -s cumulative ule
Результат:
211267 function calls (203946 primitive calls) in 49.801 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.002 0.002 49.801 49.801 ule:19(<module>)
1 0.000 0.000 49.415 49.415 ule:29(main)
4598/4584 0.007 0.000 49.224 0.011 types.py:46(function)
4599/4585 49.208 0.011 49.218 0.011 {method 'invoke' of 'gi.CallableInfo' objects}
1 0.001 0.001 46.013 46.013 app.py:47(__init__)
1 0.001 0.001 45.945 45.945 app.py:230(__populate)
24 0.007 0.000 45.936 1.914 app.py:211(new_from_file)
45 0.002 0.000 45.454 1.010 iconmanager.py:93(get_icon)
45 0.014 0.000 0.408 0.009 iconmanager.py:29(__init__)
1 0.000 0.000 0.343 0.343 iconmanager.py:71(get_icon_with_file)
1 0.000 0.000 0.343 0.343 iconmanager.py:60(get_icon_with_name)
................................................................................
Ого! 50 секунд на запуск небольшого приложения.
Шаг 2
Судя по всему, что-то неладное происходит в модуле
app.py
:app.py:230(__populate)
— вызывается один раз, на вызов тратится 46 секундapp.py:211(new_from_file)
— вызывается 24 раза, примерно 2 секунды на каждый вызовiconmanager.py:93(get_icon)
— вызывается 45 раз, примерно 1 секунда на вызовБеглый просмотр кода подтверждает догадку, что функция
__populate
вызывает new_form_file
, которая вызывает get_icon
.Начнем с
__populate
:def __populate(self):
self.launcher_view.clear_list();
self.unity = self.gsettings.get_value('favorites')
log.info(self.unity)
for menu_item in self.unity:
self.new_from_file(menu_item, False)
self.launcher_view.connect('selection-changed', self.__launcher_view_row_change)
#make first row active
self.launcher_view.set_selected_iter(0)
Видимо в панели находятся 24 элемента для каждого из которых вызывается
new_from_file(menu_item, False)
def new_from_file(self, filename, selected=True):
try:
file_path = normalize_path(filename, True)
obj = DesktopParser(file_path)
sname = obj.get('Name',locale=LOCALE)
desc = obj.get('Comment',locale=LOCALE)
icon = obj.get('Icon')
pix = IconManager().get_icon(ThemedIcon('image-missing'),32)
if icon:
if icon.rfind('.') != -1:
pix = IconManager().get_icon(FileIcon(File(icon)),32)
else:
pix = IconManager().get_icon(ThemedIcon(icon),32)
data = (pix, '%s' % sname, obj, sname.upper(), file_path)
return self.launcher_view.add_row(data,selected)
except:
return None
Первое, что бросается в глаза, это строка:
pix = IconManager().get_icon(ThemedIcon('image-missing'),32)
Иконка типа image-missing одна для всех, поэтому нет необходимости каждый раз ее искать. Сделаем ее атрибутом класса. В методе
__init__
перед вызовом __populate
добавляем:self.pix_missing = IconManager().get_icon(ThemedIcon('image-missing'), 32)
и заменяем соответствующую строку в
new_from_file
на pix = self.pix_missing
Посмотрим как изменилось время запуска:
python -m cProfile -s cumulative ule
Результат2:
194247 function calls (186926 primitive calls) in 8.863 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.007 0.007 8.864 8.864 ule:19(<module>)
1 0.000 0.000 8.363 8.363 ule:29(main)
2988/2974 0.008 0.000 8.189 0.003 types.py:46(function)
2989/2975 8.176 0.003 8.182 0.003 {method 'invoke' of 'gi.CallableInfo' objects}
1 0.001 0.001 6.817 6.817 app.py:48(__init__)
1 0.000 0.000 6.704 6.704 app.py:244(__populate)
24 0.003 0.000 6.697 0.279 app.py:224(new_from_file)
22 0.001 0.000 6.489 0.295 iconmanager.py:93(get_icon)
1 0.004 0.004 0.248 0.248 app.py:18(<module>)
Результат несколько неожиданный, поэтому перепроверил(с изменениями и без): стабильно минус 35-40 секунд.
9 секунд гораздо меньше раздражают, но тем не менее раздражают.
Попробуем еще улучшить.
Шаг 3
IconManager каждый раз создается как отдельный объект (и всегда без аргументов), сделаем его атрибутом класса.
В
__init__
добавляем: self.icon_manager = IconManager()
Все вхождения
IconManager()
заменяем на self.icon_manager
.Проверяем:
python -m cProfile -s cumulative ule
Результат3:
178980 function calls (171659 primitive calls) in 1.949 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 1.949 1.949 ule:19(<module>)
1 0.000 0.000 1.449 1.449 ule:29(main)
1560/1546 0.004 0.000 1.314 0.001 types.py:46(function)
1561/1547 1.304 0.001 1.310 0.001 {method 'invoke' of 'gi.CallableInfo' objects}
1 0.001 0.001 0.285 0.285 app.py:48(__init__)
1 0.004 0.004 0.252 0.252 app.py:18(<module>)
206/45 0.005 0.000 0.191 0.004 {__import__}
9/5 0.000 0.000 0.187 0.037 importer.py:56(load_module)
9/5 0.000 0.000 0.186 0.037 module.py:239(_load)
1 0.002 0.002 0.174 0.174 pkg_resources.py:14(<module>)
1 0.000 0.000 0.159 0.159 app.py:245(__populate)
2617/377 0.011 0.000 0.159 0.000 {map}
24 0.003 0.000 0.153 0.006 app.py:225(new_from_file)
1 0.004 0.004 0.146 0.146 Gtk.py:22(<module>)
159/132 0.012 0.000 0.132 0.001 module.py:111(__getattr__)
1 0.000 0.000 0.107 0.107 pkg_resources.py:698(subscribe)
102 0.000 0.000 0.107 0.001 pkg_resources.py:2835(<lambda>)
102 0.001 0.000 0.106 0.001 pkg_resources.py:2256(activate)
22 0.001 0.000 0.098 0.004 iconmanager.py:93(get_icon)
Окно появляется практически мгновенно. Общее время почти две секунды, однако оно включает также закрытие окна с помощью мыши. Лучше ориентироваться на строку
1 0.001 0.001 0.285 0.285 app.py:48(__init__)
: время выполнения изменилось с 6.8 секунд в предыдущем замере до 0.3 в последнем. На этом можно и остановиться.
Итог
Небольшие изменения привели к уменьшению времени запуска приложения с 46 секунд до 0.3 (для
app.py:48(__init__)
).Изменения отправлены разработчикам.
Открытый код не идеален, но каждый может его улучшить потратив совсем немного своего времени.
Изменения в виде diff-файла
=== modified file 'unitylaunchereditor/dialogs/app.py'
--- unitylaunchereditor/dialogs/app.py 2011-11-28 17:12:07 +0000
+++ unitylaunchereditor/dialogs/app.py 2013-02-26 16:47:52 +0000
@@ -71,6 +71,8 @@
#bottom window buttons
self.__create_bottom_box(main_box)
+ self.icon_manager = IconManager()
+ self.pix_missing = self.icon_manager.get_icon(ThemedIcon('image-missing'),32)
self.__populate()
self.connect('delete-event', Gtk.main_quit)
@@ -198,12 +200,12 @@
sname = info['Name']
desc = info['Comment']
icon = info['Icon']
- pix = IconManager().get_icon(ThemedIcon('image-missing'),32)
+ pix = self.pix_missing
if icon:
if icon.rfind('.') != -1:
- pix = IconManager().get_icon(FileIcon(File(icon)),32)
+ pix = self.icon_manager.get_icon(FileIcon(File(icon)),32)
else:
- pix = IconManager().get_icon(ThemedIcon(icon),32)
+ pix = self.icon_manager.get_icon(ThemedIcon(icon),32)
data = (pix, '%s' % sname, obj, sname.upper(), file_path)
return self.launcher_view.add_row(data,True)
@@ -215,12 +217,12 @@
sname = obj.get('Name',locale=LOCALE)
desc = obj.get('Comment',locale=LOCALE)
icon = obj.get('Icon')
- pix = IconManager().get_icon(ThemedIcon('image-missing'),32)
+ pix = self.pix_missing
if icon:
if icon.rfind('.') != -1:
- pix = IconManager().get_icon(FileIcon(File(icon)),32)
+ pix = self.icon_manager.get_icon(FileIcon(File(icon)),32)
else:
- pix = IconManager().get_icon(ThemedIcon(icon),32)
+ pix = self.icon_manager.get_icon(ThemedIcon(icon),32)
data = (pix, '%s' % sname, obj, sname.upper(), file_path)
return self.launcher_view.add_row(data,selected)
@@ -325,12 +327,12 @@
self.launcher_view.set_value(obj['Name'], self.launcher_view.COLUMN_NAME)
self.launcher_view.set_value(obj['Name'].upper(), self.launcher_view.COLUMN_NAME_UPPER)
icon = obj['Icon']
- pix = IconManager().get_icon(ThemedIcon('image-missing'),32)
+ pix = self.pix_missing
if icon:
if icon.rfind('.') != -1:
- pix = IconManager().get_icon(FileIcon(File(icon)),32)
+ pix = self.icon_manager.get_icon(FileIcon(File(icon)),32)
else:
- pix = IconManager().get_icon(ThemedIcon(icon),32)
+ pix = self.icon_manager.get_icon(ThemedIcon(icon),32)
self.launcher_view.set_value(pix, self.launcher_view.COLUMN_ICON)
if button_type == TOOLBUTTON_REMOVE:
UPD:
А слона то я и не заметил
Невыясненными остались два вопроса:
- слишком долгий запуск(50 секунд) для двух десятков элементов меню;
- неожиданное уменьшение времени загрузки(в 5 раз) после первой же оптимизации.
Косвенным виновником стал объект
self.icontheme = Gtk.IconTheme.get_default()
. В конструкторе IconManager-а расширяется список путей, в которых происходит поиск иконок. Все изменения сохраняются на время выполнения приложения, хотя Gtk.IconTheme.get_default() и не является Singleton-ом. В моем случае каждая инициализация IconManager-а увеличивает список на 67 элементов, что приводит к изменению его размера примерно в 300 раз (с 10 до 2985).
Конструктор класса IconManager
def __init__(self):
self.icontheme = Gtk.IconTheme.get_default()
# add the humanity icon theme to the iconpath, as not all icon
# themes contain all the icons we need
# this *shouldn't* lead to any performance regressions
path = '/usr/share/icons/Humanity'
if exists(path):
for subpath in listdir(path):
subpath = join(path, subpath)
if isdir(subpath):
for subsubpath in listdir(subpath):
subsubpath = join(subpath, subsubpath)
if isdir(subsubpath):
self.icontheme.append_search_path(subsubpath)
Изменив несколько строк в конструкторе можно уменьшить время запуска до 0.8 секунды(без предыдущих оптимизаций) и 0.6(с оптимизациями). Предыдущие изменения стали «косметическими».
Изменения в виде diff-файла
=== modified file 'unitylaunchereditor/core/iconmanager.py'
--- unitylaunchereditor/core/iconmanager.py 2011-11-26 14:36:43 +0000
+++ unitylaunchereditor/core/iconmanager.py 2013-02-28 19:13:42 +0000
@@ -28,6 +28,7 @@
class IconManager:
def __init__(self):
self.icontheme = Gtk.IconTheme.get_default()
+ search_paths = self.icontheme.get_search_path()
# add the humanity icon theme to the iconpath, as not all icon
# themes contain all the icons we need
# this *shouldn't* lead to any performance regressions
@@ -38,7 +39,7 @@
if isdir(subpath):
for subsubpath in listdir(subpath):
subsubpath = join(subpath, subsubpath)
- if isdir(subsubpath):
+ if isdir(subsubpath) and subsubpath not in search_paths:
self.icontheme.append_search_path(subsubpath)
def get_icon_with_type(self,filepath, size=24):
Bug создан, patch отправлен.