leechcraft
Wt!
К этому посту.
Итак, по результатам обсуждений в c_plus_plus@conference.jabber.ru и собственных мыслей решено делать так.
Есть плагин к программе, все тот же Remoter, который просто тупо берет и открывает какой-нибудь порт (14600, раз уж такая традиция). Он собирает сущности от других плагинов и преобразует их, оборачивая в XML. К нему коннектятся программы-клиенты.
Программа-клиент WtServer хавает этот XML и выдает с другой стороны нормальный такой HTML (или что она выдает), через который можно управлять LeechCraft'ом. То есть, WtServer — довольно своеобразный адаптер, вёб-интерфейс, как и реквестировал Civilian.
Программа-клиент RemoteManager хавает этот XML и по нему формирует GUI, через который можно управлять LeechCraft'ом. То есть, RemoteManager — отдельная программа-клиент, как и реквестировал QZMa.
Форк RemoteManager'а, программа-клиент RemoteManagerForFreaks хавает этот XML и по нему формирует CLI, через который можно управлять LeechCraft'ом. То есть, RemoteManagerForFreaks — отдельная программа-клиент, как пока никто не реквестировал, но такие запросы по-любому будут от всяких фриков и задротов.
Блин, прямо самописный XML-RPC какой-то... Впрочем, самописный XUL уже есть, так что пусть будет такое решение.
Хочется услышать критику такого подхода, лучше конструктивную :)
Wt & Qt
Ну вот, поступил, отоспался, да и девушка потихоньку сдает экзамены, так что можно опять заняться LeechCraft'ом в полную силу.
Господин Civilian вместе с QZma активно реквестируют веб-морды, поэтому занимаюсь LeechCraft::Remoter'ом. Стоит задача интеграции event loop'ов Wt и Qt, поэтому пытаюсь общаться с разработчиками Wt на этот предмет. Ооо, я чую, взрыв мозга гарантирован. И, в конце концов, это мой первый серьезный опыт сотрудничества с командой разработчиков более-менее устоявшегося проекта. Посмотрим, посмотрим, что из этого выйдет.
Откуда растут ноги
Глядя на Wt, все больше убеждаюсь, что её разработчики и дизайнеры явно брали свое вдохновение из Qt.
Надо становиться взрослым мальчиком девелопером и связаться с разработчиками на тему интеграции их велосипедной модели (greetz MVC!) с QAbstractItemModel. Все равно мне придется это делать, так уж пусть лучше оно войдет в саму либу. Ну или в набор аддонов для либы, чтобы Qt с собой не тащить.
Милые дамы, не читайте этот пост
Нет, я просто кончаю с Wt! Такой красивой, концептуальной и продуманной библиотеки я давно не встречал, и знаю всего две таких же: Qt и boost. Хочу уже побыстрее вступить с девелоперами в in team-ные связи.
Не буду расписывать, чего либа умеет, ибо иначе у меня потекут всякие слюнки прямо в бложек.
POCO стопроцентно идет лесом.
Оптимизационное (Тухлый косит под тру девелопера)
LeechCraft показывает довольно полную статистику о подключенных пирах для каждого торрента. Для этой статистики есть свой QTreeView, QSortFilterProxyModel, дабы юзер мог сортировать статистику, и своя модель class PeersModel : public QAbstractItemModel. Для поддержания модели в актуальном состоянии раз в секунду вызывается функция PeersModel::Update(const QList<PeerInfo>& peers, int torrent), которой передается список текущих пиров и номер торрента.
В начале все выглядело так:
1: void PeersModel::Update (const QList<PeerInfo>& peers, int torrent)
2: {
3: CurrentTorrent_ = torrent;
4: QList<PeerInfo> peers2insert (peers.size ());
5: QMap<QString, int> IP2position;
6:
7: qDebug () << "filling ip2position";
8: for (int i = 0; i < Peers_.size (); ++i)
9: IP2position [Peers_.at (i).IP_] = i;
10:
11: qDebug () << "finding positions";
12: int psize = peers.size (),
13: lPsize = Peers_.size ();
14: for (int i = 0; i < psize; ++i)
15: {
16: PeerInfo pi = peers.at (i);
17:
18: int found = false;
19: for (int j = 0; j < lPsize; ++j)
20: if (Peers_.at (j).IP_ == pi.IP_)
21: {
22: found = true;
23: Peers_ [j] = pi;
24: IP2position.remove (pi.IP_);
25: emit dataChanged (index (j, 1), index (j, 11));
26: break;
27: }
28:
29: if (found)
30: continue;
31:
32: peers2insert << pi;
33: }
34:
35: qDebug () << "removing";
36: QList<int> values = IP2position.values ();
37: qSort (values.begin (), values.end (), qGreater<int> ());
38: for (int i = 0; i < values.size (); ++i)
39: {
40: beginRemoveRows (QModelIndex (), values.at (i), values.at (i));
41: Peers_.removeAt (values.at (i));
42: endRemoveRows ();
43: }
44:
45: qDebug () << "adding";
46: if (peers2insert.size ())
47: {
48: beginInsertRows (QModelIndex (), Peers_.size (), Peers_.size () + peers2insert.size () - 1);
49: Peers_ += peers2insert;
50: endInsertRows ();
51: }
52: qDebug () << "finished";
53: }
При больших количествах пиров (Peers_.size () == peers.size () == 70) код на строках 12-33, ищущий, какие пиры обновились, какие добавились, а какие вообще перестали существовать, выполнялся чуть более 3 секунд (сравнивались временные метки в логах, так как нормального профилера под рукой не оказалось). Не дело, тормоза видны невооруженным глазом. Остальные части функции выполнялись не больше 100 миллисекунд, чем можно пренебречь. Оптимизируем именно тот кусок. При всех оптимизациях мы будем пользоваться тем, что никакие постоянные структуры данных не меняются в ходе работы кода 12-33, все добавление/удаление идет после вычисления вторым проходом.
Оптимизация 1
Замечаем, что мапа IP2position уже содержит всю информацию по соответствию IP <-> позиция в Peers_. Поиск по ней — O(log(n)), судя по Assistant'у, в то время как поиск по QList — O(n). Итак, заменяем тот код таким:
1: int psize = peers.size ();
2: for (int i = 0; i < psize; ++i)
3: {
4: PeerInfo pi = peers.at (i);
5: QMap<QString, int>::iterator pos = IP2position.find (pi.IP_);
6:
7: if (pos != IP2position.end ())
8: {
9: int ind = pos.value ();
10: Peers_ [ind] = pi;
11: IP2position.erase (pos);
12: emit dataChanged (index (ind, 1), index (ind, 11));
13: }
14: else
15: peers2insert << pi;
16: }
Судя по логам, на таком же количестве пиров в установившемся режиме этот кусок выполняется чуть меньше 2 секунд. Прогресс налицо!
Оптимизация 2
Можно заметить, что порядок данных в IP2position неважен, и для QString определен qHash. К чему я это? Правильно, к тому, что можно QMap заменить на QHash. Выигрыш будет, так как O(log(n)) сведется к O(1). Заодно вынесем испускание сигнала dataChanged в отдельный блок, чтобы посмотреть, сколько времени он занимает. Итак, получаем лог вида
[16:42:38.607] [thread ptr 0x641d30] filling ip2position [16:42:38.607] [thread ptr 0x641d30] finding positions [16:42:38.608] [thread ptr 0x641d30] emitting [16:42:40.640] [thread ptr 0x641d30] removing [16:42:40.641] [thread ptr 0x641d30] adding [16:42:40.642] [thread ptr 0x641d30] finished
для кода
1: qDebug () << "finding positions";
2: int psize = peers.size ();
3: QList<PeerInfo> peers2insert;
4: QList<int> changedPos;
5: for (int i = 0; i < psize; ++i)
6: {
7: const PeerInfo& pi = peers.at (i);
8: QHash<QString, int>::iterator pos = IP2position.find (pi.IP_);
9: if (pos != IP2position.end ())
10: {
11: int ind = pos.value ();
12: Peers_ [ind] = pi;
13: IP2position.erase (pos);
14: changedPos << ind;
15: }
16: else
17: peers2insert << pi;
18: }
19:
20: qDebug () << "emitting";
21: int cp = changedPos.size ();
22: for (int i = 0; i < cp; ++i)
23: {
24: emit dataChanged (index (changedPos [i], 1), index (changedPos [i], 11));
25: }
26:
27: qDebug () << "removing";
То есть, мы видим, что оптимизировать поиск измененных пиров было вообще бессмысленно. Ну что ж, все равно, можно будет ощущать внутреннюю гордость, что что-то работает быстрее.
Анализ
Такая длительная задержка в emit в сочетании с тем, что она проявляется лишь при включенной сортировке, наводит на мысль, что при каждом сигнале dataChanged прокси-модель QSortFilterProxyModel пересортировывается. Таким образом, нам нужно либо свести количество испусканий dataChanged к минимуму, либо вообще уведомлять View об изменении данных по-другому.
Так как пока не нужно сохранять выделение между обновлениями (разве что, для красоты, но к черту красоту!), будем просто вызывать reset(). Итак, вот последняя версия функции, которая совершенно не тормозит:
1: void PeersModel::Update (const QList<PeerInfo>& peers, int torrent)
2: {
3: CurrentTorrent_ = torrent;
4:
5: QHash<QString, int> IP2position;
6: for (int i = 0; i < Peers_.size (); ++i)
7: IP2position [Peers_.at (i).IP_] = i;
8:
9: int psize = peers.size ();
10: QList<PeerInfo> peers2insert;
11: for (int i = 0; i < psize; ++i)
12: {
13: const PeerInfo& pi = peers.at (i);
14: QHash<QString, int>::iterator pos = IP2position.find (pi.IP_);
15: if (pos != IP2position.end ())
16: {
17: Peers_ [pos.value ()] = pi;
18: IP2position.erase (pos);
19: }
20: else
21: peers2insert << pi;
22: }
23:
24:
25:
26: QList<int> values = IP2position.values ();
27: qSort (values.begin (), values.end (), qGreater<int> ());
28: for (int i = 0; i < values.size (); ++i)
29: {
30: beginRemoveRows (QModelIndex (), values.at (i), values.at (i));
31: Peers_.removeAt (values.at (i));
32: endRemoveRows ();
33: }
34:
35: reset ();
36:
37: if (peers2insert.size ())
38: {
39: beginInsertRows (QModelIndex (), Peers_.size (), Peers_.size () + peers2insert.size () - 1);
40: Peers_ += peers2insert;
41: endInsertRows ();
42: }
43: }
Здесь мы переставили reset() так, что модель опрашивается при минимальном видимом количестве пиров, чтобы количество запросов тоже было минимальным.
Результат
Среднее время на цикл до всех оптимизаций — 3 секунды, после — 40 миллисекунд. Причем, это время можно еще уменьшить за счет подбора соотношения количество_пиров/количество_измененных_данных, когда выгоднее испускать несколько dataChanged, а когда — сразу бабахать reset()'ом.
Premature optimization is the root of all evil (эпилог)
Да, правда, так. Сначала нужно узнать, что на самом деле вызывает падение производительности, и только потом браться за оптимизацию.
08 марта 2006 (как все начиналось)
Итак, на картинке слева — интерфейс самой ранней ревизии LeechCraft, которую мне удалось найти (svn co -r 1, как ни странно). Коммит датирован 8 марта 2006 года. Собирается всего 7.5 секунд в один поток, весит 42 килобайта, минимум зависимостей (QtCore, QtGui, QtNetwork), ничего не умеет делать и, следовательно не глючит. 605 строк, из них половина — шапка с лицензией. Судя по файликам, рисовалось все это дело в KDevelop'е. Красивый стиль, абсолютно незапутанный код, и совершенная неизвестность впереди. Да, кажется, тогда я вообще не задумывался, что из этого всего выйдет, и не заброшу ли я проект на следующий день.
Ан нет, не забросил. Уже больше двух лет прошло, а энтузиазма все больше и больше! К проекту приложило руку уже минимум пять человек, гораздо больше людей просто принимало участие в обсуждении и оказывало посильную помощь. Сейчас программа собирается 4 минуты 11 секунд, содержит 22155 строк, весит в райное 3 мегабайт без учета многочисленных сторонних библиотек, умеет делать много чего, в том числе и глючить. Хотя, если серьезно, глючит оно не так уж и сильно. А из библиотек — Qt, boost, libtorrent, POCO, на смену которому вскоре придет Wt. Планируется скоро добавить поддержку Lua.
А вот каким монстром оно стало:
![]()
Я сделал это!
changing servers
Ну вот, перехожу с POCO на Wt. Первая уже вконец достала своей долбанутостью и неконцептуальностью, а также постоянно чувствуемым запашком студенческой поделки. Вторая же привлекает красивым дизайном (бегло пробежался по докам), законченным модулем для отрисовки вёб-виджетов и еще тем, что она основана на boost::asio. А я лучше доверюсь тому, что когда-нибудь войдет в Стандарт, нежели какому-то велосипеду.
Насущное (абстрагируя абстрагированные абстракции)
Вот уже два года, как я пытаюсь сделать все офигенно абстрагированно и независимо, и поэтому жёстко парюсь. Но нет, вы покажите, где в ядре линуха указано, что init может быть не /sbin/init, а где у init опция, что inittab может лежать не в /etc/inittab? И ничего, живут, развиваются.
Думаю, будет ничего, если будут плагины с поддержкой очереди и без (дабы с поддержкой интегрировались в Одно Большое Месиво). Думаю, будет ничего, если будут плагины с вёб-мордой и без.
Комбайны!
Для всех, кто говорит, что мой личкрафт — слишком комбайн и не unix-way:
Задача посмотреть видео:
Юниксвейный вариант:
1) скачиваем файл
2) утилитой определяем тип контейнера
3) утилитой определяем тип видео кодека
4) утилитой определяем тип аудио кодека
5) разделяем видеофайл на аудио и видео потоки
6) декодируем видео
7) декодируем аудио
8) Направляем декодированное аудио на звуковое устройство
9) Декодированное видео разрезаем на кадры
10) пишем скрипт показывающий через фреймбуфер разрезанные битмапы.
Не юниксвейный вариант: mplayer
Receiving free cookies!
Вот, заключил тут брачный союз с POCO, она так мило компилится уже минут 10... Или я -j2 забыл указать... Не важно. В общем, скоро моя программа обретет полноценный веб-сервер, HTML-генератор и прочие фенечки. Черт, мне дико везет на библиотеки!
LeechCraft progress
Вот что значит не занимался LeechCraft'ом неделю: за сегодня, отдохнувший, закрыл 7 task'ов. Релиз 0.3.0 уже не за горами!
np в тему, хехе.
Еще оптимизационное
Сейчас заметил, что LeechCraft запускается секунд 7-9 на моем Core2Duo, что с оптимизациями (-O2), что без. Начал выяснять. Оказалось, работа с QSettings в XMLSettingsManager ведется, мягко говоря, не очень оптимально: они пересоздаются при каждом изменении свойства. В итоге, простенький diff, и время старта уменьшено на порядок: 0.9 секунды.
% svn diff plugininterface/basesettingsmanager.h -r 574:575 Index: plugininterface/basesettingsmanager.h =================================================================== --- plugininterface/basesettingsmanager.h (revision 574) +++ plugininterface/basesettingsmanager.h (revision 575) @@ -20,6 +20,7 @@ QMap<QByteArray, QPair<QObject*, QByteArray> > Properties2Object_; bool Initializing_; + QSettings *Settings_; public: /*! @brief Initalizes the settings manager. * @@ -30,15 +31,12 @@ */ void Init () { - QSettings *settings = BeginSettings (); - QStringList properties = settings->childKeys (); + Settings_ = BeginSettings (); + QStringList properties = Settings_->childKeys (); Initializing_ = true; for (int i = 0; i < properties.size (); ++i) - setProperty (PROP2CHAR (properties.at (i)), settings->value (properties.at (i), QVariant ())); + setProperty (PROP2CHAR (properties.at (i)), Settings_->value (properties.at (i), QVariant ())); Initializing_ = false; - EndSettings (settings); - delete settings; - settings = 0; } /*! @brief Prepares the settings manager for deletion. @@ -51,10 +49,11 @@ void Release () { QList<QByteArray> dProperties = dynamicPropertyNames (); - QSettings *settings = BeginSettings (); for (int i = 0; i < dProperties.size (); ++i) - settings->setValue (dProperties.at (i), property (dProperties.at (i).constData ())); - EndSettings (settings); + Settings_->setValue (dProperties.at (i), property (dProperties.at (i).constData ())); + EndSettings (Settings_); + delete Settings_; + Settings_ = 0; } /*! @brief Subscribes object to property changes. @@ -107,11 +106,7 @@ QDynamicPropertyChangeEvent *event = dynamic_cast<QDynamicPropertyChangeEvent*> (e); QByteArray name = event->propertyName (); - QSettings *settings = BeginSettings (); - settings->setValue (name, property (name)); - EndSettings (settings); - delete settings; - settings = 0; + Settings_->setValue (name, property (name)); if (Properties2Object_.contains (name)) {
Базы данных два метра занимают, из них полтора — Aggregator'овские фиды.
Я ненавижу википедиков
Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков. Я ненавижу википедиков.
Моя статья о LeechCraft — blatant advertising?! Да вы там совсем охренели что ли?! Об Азуреусе так писать нормально, а о моей проге — нет?! Да еще и нельзя писать о нераспространенных продуктах! Напрочь ежанутая бюрократизированная американоговноязычная педивикия.
АНГСТ!