7 апреля 2010 г.

Некоторые оптимизации оконных программ

Поскольку я решил не переводить посты с кодом, но нижеуказанная серия весьма полезна - я решил кратко просуммировать её тут, но без кода.

Итак, этот пост является выжимкой из следующих постов:

В своих оконных программах вы можете делать множество вещей, которые отвечают за ваш внешний вид. Чаще всего этот код приносит пользу. Но бывают ситуации, когда ему лучше бы не работать. Вот три примера таких ситуаций:
  • Если окно не видимо - нет смысла его перерисовывать (включая вычисления и прочие сопутствующие действия).
  • Если приложение запущенно через удалённый рабочий стол - вам лучше бы минимизировать обновления или программа будет тормозить.
  • Если человек за машиной переключился на другого пользователя или просто заблокировал машину - вам лучше бы отключить всю работу, отвечающую за красивости.

Обновление окна только когда оно видимо

Иногда вы хотели бы выполнить действия, такие как обновление статуса операции в окне, только когда окно не закрыто другими окнами.

Простейшим способом определить это - не пытаться определять вообще. Например, вот как панель задач обновляет себя:
  1. Она вычисляет, сколько осталось времени до конца следующей минуты.
  2. Она вызывает SetTimer для срабатывания через этот промежуток времени.
  3. Когда таймер срабатывает, панель задач вызывает на себя InvalidateRect и удаляет таймер.
  4. Обработчик WM_PAINT рисует текущее время, потом возвращается к шагу 1.
Если часы панели задач не видимы (либо они перекрыты другим окном, либо же панель задач авто-скрылась), то Windows не станет доставлять сообщение WM_PAINT, так что панель задач просто войдёт в спячку и перестанет потреблять процессорное время. Как только панель задач будет показана или окно, её перекрывающее, уйдёт, Windows сама отправит панели задач сообщение WM_PAINT, так что его обработчик нарисует часы, выполнит расчёт времени до следующего срабатывания и установит таймер снова - таким образом, панель задач сама проснётся из спячки.

Ключевой момент здесь - удаление и пересоздание таймера. Заметьте, как при этом вы получаете режим спячки просто ничего не делая. Этот метод также перестанет обновлять ваше окно, если запустится хранитель экрана, машина окажется заблокированной или будет выполнено переключение на другого пользователя. Эта техника также применима, если вам нужно обновлять не окно целиком, а только часть его (определённую область или элемент управления) - просто вызывайте InvalidateRect для конкретной области, а не окна целиком. В обработчике WM_PAINT устанавливайте таймер только, если область обновления пересекается с заданной конкретной областью.

Поддержка удалённого рабочего стола

Ценность поддержки удалённого рабочего стола постоянно возрастает. Когда пользователь подключён через удалённый рабочий стол, видео-операции передаются по сети на клиентское место для отображения. Поскольку сети имеют большие задержки и невысокую скорость (в сравнении с локальной шиной PCI или AGP), то вам нужно адаптировать количество изменений на экране.

Если вы рисуете на экрана линию, то по сети передаётся команда "рисуй линию". Если вы рисуете текст, то отправляется команда "рисуй текст" (вместе с самим текстом и его координатами для рисования). Пока неплохо. Но если вы выводите на экран рисунок, то этот рисунок тоже будет передан по сети.

Попробуйте сделать программу, которая рисует двадцать строк столбиком через TextOut. Убедитесь, что опция "Показывать содержимое окна при перетаскивании" включена, и попробуйте изменять размер вашего окна. Ух, какое гадкое получается мерцание.

Исправьте этот недостаток с использованием обычной техники двойного буфера: рисуйте строки в специально подготовленный bitmap и выводите его на окно одним махом. Если вы запустите программу теперь, то увидите, что окно изменяет размеры гладко и красиво.

Теперь подключитесь к своему компьютеру через удалённый рабочий стол и запустите программу снова (не забудьте включить по максимуму опции соединения рабочего стола). Попробуйте изменять размеры окна или максимизировать и восстанавливать его. Заметьте задержку при этих операциях. Это потому что мы проталкиваем огромный bitmap через сеть.

Вернитесь к предыдущей версии вашей программы, где вы выводили текст напрямую, и попробуйте запустить её в этой же сессии удалённого рабочего стола. Ох, а эта работает очень быстро. Это потому что более простая версия не копирует большие рисунки по сети - она просто отправляет двадцать вызовов TextOut. Это занимает намного меньше трафика, чем 1024x768 bitmap.

Мы определили, что один метод работает быстрее через удалённый рабочий стол, а другой метод работает быстрее при локальном использовании. Какой же нам использовать?

Мы используем оба, выбирая метод в зависимости от окружения. Для определения того, что вы запущены через удалённый рабочий стол - используйте вызов GetSystemMetrics(SM_REMOTESESSION).

Теперь у нас есть лучшее от обоих миров. Когда мы запущены локально - мы используем двойную буферизацию для красивого рисования без мерцания, но когда мы запущены через удалённый рабочий стол - мы рисуем прямо на экран, минуя буфер.

Поддержка Fast User Switching

Другой ситуацией, где вы могли бы играть дружелюбнее, является сценарий с быстрым переключением пользователей. Когда рабочая станция заблокирована или отключена (это включает в себя вызов "смены пользователя", а также отключение от сессии удалённого рабочего стола без её завершения), вам следует выключить все не важные таймеры, минимизировать фоновую активность и, вообще говоря, отправить свою программу в максимально спящее состояние. Если вы уже используете техники, описанные выше, вы можете получить такое поведение на халяву, поскольку заблокированная или отключенная рабочая станция не генерирует сообщений перерисовки.

Но если помимо перерисовки вы выполняете и другие действия, то вам нужно отключать их вручную, когда пользователь отключается от рабочей станции. Вы можете зарегистрироваться, чтобы получать уведомления о смене состояния рабочей станции.

Для этого, вызовите функцию WTSRegisterSessionNotification(Handle, NOTIFY_FOR_THIS_SESSION) при старте вашего приложения и ловите сообщение WM_WTSSESSION_CHANGE:
if Msg = WM_WTSSESSION_CHANGE then
  case wParam of
    WTS_CONSOLE_DISCONNECT,
    WTS_REMOTE_DISCONNECT,
    WTS_SESSION_LOCK,
    WTS_SESSION_LOGOFF:
    // отключить фоновую работу

    WTS_CONSOLE_CONNECT,
    WTS_REMOTE_CONNECT,
    WTS_SESSION_UNLOCK,
    WTS_SESSION_LOGON:
    // включить фоновую работу
В такой программе вы регистрируетесь на уведомления после создания главного окна и ждёте уведомлений. Если вы видите одно из сообщений "отошёл" - вы переходите в состояние спячки, если вы видите одно из сообщений "вернулся" - вы просыпаетесь.

7 комментариев :

  1. Замечу, что за время жизни одного и того же экземпляра программы он может работать как локально, так и через удалённый рабочий стол.

    Например, мы запустили программу локально на клиентской ОС. Потом, не выходя из системы, подключились к машине этим же пользователем через удалённый рабочий стол. Windows подключит вас к тому же сеансу, что уже открыт, заблокировав локальную консоль. Поработали удалённо - вернулись за свою машину и вошли в систему. Тогда Windows выкинет удалённого клиента и подключит сеанс к локальной консоли.

    Иными словами, программа за свою жизнь может работать как локально, так и удалённо, но не одновременно.

    Я это к тому, что если вы кэшируете проверку GetSystemMetrics(SM_REMOTESESSION) в глобальной переменной, то вам лучше бы пересчитывать её по приходу WM_WTSSESSION_CHANGE.

    ОтветитьУдалить
  2. Спасибо, очень интересно.
    Многие мои клиенты работают с моей программой с помощью Remote Desktop Connection, но я никогда не задумывался об этом.
    Более того, я даже не помню уже, используется ли у меня в программе двойная буферизация или нет.

    Но я пожалуй попробую отслеживать указанные в статье сообщения, и в случае обнаружения удалённого подключения буду отключать спецэффекты.

    ОтветитьУдалить
  3. В новых Delphi достаточно переключить свойство DoubleBuffered и, если ParentDoubleBuffered у всех стоит в True, этого будет достаточно для переключения буферизации у всех контролов.

    Однако, некоторые навороченные контролы вполне могут использовать двойную буферизацию где-то внутри себя вне зависимости от свойства DoubleBuffered. Причём, не давая никакого способа это изменить.

    Ну, тут уж ничего не сделаешь.

    ОтветитьУдалить
  4. Получается что контролы которые для отрисовки себя используют gdi+ (например скиновые компоненты) наверное тоже будут рисоваться на порядок медленнее.

    ОтветитьУдалить
  5. Спасибо за статью, интересно, но возникает один вопрос - а стоит ли овчинка выделки ? Стоят ли затраченные усилия на ускорение отрисовки через уделенный рабочий стол изменений в коде и его дальнейшего сопровождения ? Каналы для становятся со временем все толще, скорости обмена все выше.
    Специально после прочтения статьи полез на удаленный рабочий стол позапускать всяких разных программ, начиная от Open Office до программ собственного производства, на Delphi, но без особых украшательств. И не увидел ужасающей разницы в видимом быстродействии.
    Да, Far медленнее перерисовывает окно при обновлении каталога, чем на локальном рабочем столе. С другой стороны, сетевой каталог он и на локальном рабочем столе не мгновенно обновляет. Open Office листает довольно большой Word-овский документ с картинками вполне приемлемо по скорости, скроллбар не так быстро работает, но в целом, повторюсь, терпимо.

    ОтветитьУдалить
  6. Интересно, не задумывался о различии для удалёнки! Хотя и так неплохо работает - виндовое средство весьма и весьма нетребовательно к скорости сети!
    Но буду иметь в виду.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.