Счётчики производительности

Часть 2. Создание

Автор: Сергей Холодилов
The RSDN Group

Источник: RSDN Magazine #4-2003
Опубликовано: 24.02.2004
Версия текста: 1.0
Общая картина
Минимальный пример
Реестр
DLL
Эксперименты
Запрос «Global» - v1
Запрос «Global» - v2
Запрос «Costly»
Запросы «ABCD», «EFGHIJ», …
Запрос «2» - v1
Запрос «2» - v2
Запрос «22222» - v1
Запрос «22222» - v2
Возврат ошибки из Open
Возврат ошибки из Collect
Запрос с удалённой машины
Нормальный пример
Реестр
Почти настоящая DLL
Рассуждения о настоящей DLL
Регистрация в базах имён
Код
Бегемоты
MyCoolCounter

Five, six, seven eight nine ten I love you.
J.Lennon

Демонстрационный пример

О том, что такое счётчики производительности, рассказано в первой части статьи. Там же описано, как их читать и как расшифровывать прочитанное. Эта часть статьи посвящена вопросам создания собственных счётчиков.

Общая картина

Начнём с того, что создать счётчик производительности нельзя :) То есть добавить к объекту «System» счётчик «MyCoolCounter» не получится просто в силу ограничений технологии.

ПРИМЕЧАНИЕ

На самом деле можно :) Но это хак (хотя и изящный), то есть использование технологии не по назначению. Идея описана в самом конце статьи, реализация прилагается в качестве примера.

Зато можно создать свой объект с произвольным количеством счётчиков. В первом приближении, для этого нужно написать dll с определённым интерфейсом и зарегистрировать её в реестре. Механизм работает примерно так:

Управление переходит к Windows. Она:

Минимальный пример

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

Реестр

Для регистрации dll в реестре необходимо сделать следующее:

DLL

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

Первая функция условно называется «Open» и выглядит так:

DWORD CALLBACK Open(LPWSTR lpDeviceNames);

Функция вызывается после загрузки dll и производит начальную инициализацию. Параметр игнорируйте – если при инсталляции вы не сделаете некоторых специальных телодвижений (я никогда их не делал, поэтому они выходят за рамки данной статьи), его значением будет NULL. Возвращаемое значение – ERROR_SUCCESS или стандартный код ошибки.

ПРИМЕЧАНИЕ

В MSDN в описании функции Collect написано, что при удалённом запросе данных несколькими приложениями Open может быть вызвана несколько раз подряд. Но мне не удалось этого добиться, хотя я очень старался. Про Close ничего подобного не написано, как с ней – не ясно.

Вторая функция – «Close»

DWORD WINAPI Close();

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

Последняя и наиболее интересная функция – «Collect»

DWORD WINAPI Collect(
  LPWSTR lpwszValue, 
  LPVOID *lppData, 
  LPDWORD lpcbBytes, 
  LPDWORD lpcObjectTypes);

Собственно, эта функция и возвращает запрашиваемые данные.

Тип Параметр Значение
IN lpwszValue Строка, переданная в RegQueryValueEx
IN/OUT lppData На входе – указатель на буфер для данных. На выходе – указатель на первый оставшийся свободным байт в буфере. Число занятых байтов должно быть кратно размеру DWORD-а.
IN/OUT lpcbBytes На входе содержит размер буфера. На выходе должен содержать количество записанных байтов, при этом число должно быть кратно размеру DWORD-а.
OUT lpcObjectTypes Количество объектов (структур PERF_OBJECT_TYPE), записанных в буфер.
ПРИМЕЧАНИЕ

Желательно, чтобы количество записанных байтов было кратно 8. В противном случае в Event Log добавится предупреждение, рекомендующее обратиться к производителю за более новой версией dll. Почему-то MSDN об этом умалчивает.

Возвращаемым значением должно быть ERROR_SUCCESS или ERROR_MORE_DATA. Возвращаемые данные – это те самые структуры PERF_OBJECT_TYPE вместе с PERF_COUNTER_DEFINITION, PERF_INSTANCE_DEFINITION и PERF_COUNTER_BLOCK, о которых говорилось в первой части статьи. Если вы по каким-то причинам ничего не записали в буфер (разумные причины – ваш объект не запрашивают, недостаточный размер буфера, ошибка), нужно установить *lpcbBytes и *lpcObjectTypes в 0, а lppData не трогать.

Пример:

        #include <windows.h>

extern"C"__declspec(dllexport) DWORD CALLBACK Open(LPWSTR lpDeviceNames)
{
  MessageBox(NULL, TEXT("Open"), TEXT("Open from counter.dll"), MB_OK);
  return ERROR_SUCCESS;
}

extern"C"__declspec(dllexport) DWORD WINAPI Collect(
                      LPWSTR lpwszValue, 
                      LPVOID *lppData, 
                      LPDWORD lpcbBytes, 
                      LPDWORD lpcObjectTypes)
{
  MessageBoxW(NULL, lpwszValue, L"Collect from counter.dll", MB_OK);
  return ERROR_SUCCESS;
}

extern"C"__declspec(dllexport) DWORD WINAPI Close()
{
  MessageBox(NULL, TEXT("Close"), TEXT("Close from counter.dll"), MB_OK);
  return ERROR_SUCCESS;
}

DWORD APIENTRY DllMain(HINSTANCE, DWORD dwReason, LPVOID)
{
  const TCHAR* reason = TEXT("unknown");

  switch (dwReason)
  {
    case DLL_PROCESS_ATTACH: reason = TEXT("DLL_PROCESS_ATTACH"); break;
    case DLL_PROCESS_DETACH: reason = TEXT("DLL_PROCESS_DETACH"); break;
    case DLL_THREAD_ATTACH:  reason = TEXT("DLL_THREAD_ATTACH"); break;
    case DLL_THREAD_DETACH:  reason = TEXT("DLL_THREAD_DETACH"); break;
  }

  MessageBox(NULL, reason, TEXT("DllMain from counter.dll"), MB_OK);
  return TRUE;
}

Как видите, всё предельно просто. В процессе экспериментов код будет меняться, но незначительно; действительно работающей версии посвящён отдельный раздел.

Итак, библиотека собрана, изменения в реестр внесены – GO!

Эксперименты

Мой полигон:

Собственно эксперименты заключаются в вызове RegQueryValueEx(HKEY_PERFORMANCE_DATA, …) с различными строчками в качестве имени значения, подобное действие я называю «запрос».

ПРИМЕЧАНИЕ

Смысл «запросов» описан в первой части статьи в разделе "Параметр lpValueNameфункции RegQueryValueEx".

Запрос «Global» - v1

  1. Появляется окно «DLL_PROCESS_ATTACH».
  2. Появляется окно «Open»
  3. Появляется окно «Collect» с текстом «Global»
  4. На последующие запросы dll не реагирует.
  5. При закрытии проявляется окно «Close», потом «DLL_PROCESS_DETACH»
  6. При последующих запусках dll не реагирует, даже не грузится.
  7. В Event Log-е появилось два новых сообщения с источником «Perflib». Первое (тип – предупреждение): «The pointer returned did not match the buffer length returned by the Collect procedure for the "MyPerf" service in Extensible Counter DLL…» Второе (ошибка): «Performance counter data collection from the "MyPerf" service has been disabled due to one or more errors generated by the performance counter library for that service…»
  8. В реестре в «HKLM\SYSTEM\CurrentControlSet\Services\MyPerf\Performance» появилось значение «Disable Performance Counters» типа REG_DWORD, равное 1.

В общем, чего-то такого следовало ожидать. Функция Collect содержит ошибку – она не устанавливает *lpcbBytes и *lpcObjectTypes в 0. Система ошибку обнаружила, добавила сообщение в Event Log и запретила дальнейшее использование dll.

Удаляем из реестра значение «Disable Performance Counters», исправляем код Collect так:

...
  *lpcbBytes = 0;
  *lpcObjectTypes = 0;
  return ERROR_SUCCESS;
}

Запрос «Global» - v2

  1. Появляется окно «DLL_PROCESS_ATTACH»
  2. Появляется окно «Open»
  3. Появляется окно «Collect» с текстом «Global»
  4. Последовательно появляется ещё десяток окон «Collect» с текстом «Global»
  5. На последующие запросы dll реагирует.
  6. При закрытии проявляется окно «Close», потом «DLL_PROCESS_DETACH»
  7. При последующих запусках dll реагирует.

Дело было так:

Запрос «Costly»

Всё происходит аналогично «Global». Дифференциация объектов на «Costly» и обычные оставляется на усмотрение разработчиков.

ПРИМЕЧАНИЕ

Можно даже проводить границу на уровне отдельных счётчиков, то есть включать в ответы на «Costly» запросы дополнительные детали. Упоминаний о такой возможности я не обнаружил, впрочем как и препятствий при её реализации. Вроде работает…

Запросы «ABCD», «EFGHIJ», …

Не работают. То есть ни одна dll не грузится.

Запрос «2» - v1

Очередной сюрприз! Если этот запрос был первым, то dll даже не загрузится, а если нет, функция Collect просто не будет вызвана. Видимо, где-то есть сопоставление между индексами имён объектов (если кто не помнит – «2» – индекс объекта «System») и библиотеками.

Запрос «2» - v2

Залезаем в реестр, в «HKLM\SYSTEM\CurrentControlSet\Services\MyPerf\Performance» создаём строковое значение «Object List», записываем в него «2», повторяем запрос. Получилось! Dll грузится, Collect вызывается. И другим вроде не мешаем, так как данные объекта «System» отображаются нормально.

Запрос «22222» - v1

Объекта с таким индексом у меня нет, он значительно превосходит индекс последнего использованного имени. Делаем запрос, не убирая значение «Object List» – не работает. То есть dll не грузится, Collect не вызывается.

Запрос «22222» - v2

Убираем «Object List», запрашиваем данные – работает! Вот и сопоставление. Видимо, система ведёт себя так:

Отсюда вывод – иметь в наличии список полезно. Во-первых, dll будет загружена, когда нужно, во-вторых, не будет грузиться без повода, в-третьих, не будут грузиться лишние dll.

Возврат ошибки из Open

Изменяем код следующим образом:

        extern
        "C"
        __declspec(dllexport) DWORD CALLBACK Open(LPWSTR lpDeviceNames)
{
  MessageBox(NULL, TEXT("Open"), TEXT("Open from counter.dll"), MB_OK);
  return ERROR_ACCESS_DENIED;
}

Запрашиваем «Global». В результате получаем:

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

Возврат ошибки из Collect

Результат этого действия похож на возврат ошибки из Open, но dll не выгружается, даже дополнительных вызовов Open/Close не происходит. Просто добавляется сообщение об ошибке, и на следующий раз опять вызывается Collect.

Запрос с удалённой машины

Вносим в код следующие изменения:

Запрос выдаём с какой-нибудь удалённой машины. В результате получаем:

Если запросы производить от разных пользователей с двух машин одновременно, то начинается что-то странное. Логики я не уловил, но, вроде бы всё работает корректно, а именно между вызовами Close и Collect обязательно будет Open, между Collect и Open – Close. Двух Open подряд я не дождался, но, возможно, бывает и такое.

Нормальный пример

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

Реестр

Чтобы dll загружалась, достаточно внести в реестр изменения, описанные в разделе «Минимальный пример\Реестр». Но помимо этого нужно изменить ещё и базы имён/описаний. Для этого предназначены утилиты lodctr (для добавления) и unlodctr (для удаления).

ПРИМЕЧАНИЕ

Джеффри Рихтер (в книге «Программирование серверных приложений для Windows 2000» и в статье «Custom Performance Monitoring for Your Windows NT Applications» MSJ, August 1998) делает всё вручную. Рихтер, конечно, молодец... но в данном вопросе, по-моему, не прав.

При инсталляции нужно:

При деинсталляции:

ini-файл имеет следующий формат:

[info]
drivername=<имя ключа в «HKLM\SYSTEM\CurrentControlSet\Services»>
symbolfile=<имя файла смещений, описан ниже>

[languages] 
langid_1=<что угодно или ничего, для наглядности - название языка>
langid_2=<что угодно или ничего, для наглядности - название языка>
langid_n=<...>

[objects]
object1_offset_langid_1_NAME=<что угодно или ничего>
object1_offset_langid_2_NAME=<что угодно или ничего>
object2_offset_langid_1_NAME=<что угодно или ничего>
object2_offset_langid_2_NAME=<что угодно или ничего>
то же для остальных объектов..

[text]  
object1_offset_langid_1_NAME=<имя первого объекта на первом языке>
object1_offset_langid_1_HELP=<описание первого объекта на первом языке>
то же для остальных языков..

то же для остальных объектов..

counter1_offset_langid_1_NAME=<имя первого счётчика на первом языке>
counter1_offset_langid_1_HELP=<описание первого счётчика на первом языке>
то же для остальных языков..

то же для остальных счётчиков..

Здесь «langid_X» – идентификатор языка (например «009», «019»), а «xxxxX_offset» – смещение объекта или счётчика из файла смещений. «Файл смещений» это h-файл, который может выглядеть так:

        #define object1_offset  0
#define object2_offset  2
#define counter1_offset 4
#define counter2_offset 6
#define counter3_offset 8
#define counter4_offset 10

В файле смещений могут быть комментарии, #ifndef/#define/#endif и, вроде, вообще всё, что угодно. Но перегружать файл объявлением структур, функций и шаблонов не рекомендую, как-то это неправильно...

Результатом выполнения lodctr является следующее:

ПРЕДУПРЕЖДЕНИЕ

Утилита lodctr – вещь полезная. Но её авторам я бы посоветовал подумать про необходимость проверки входных параметров. Так, помимо забавного бага с «Object List», имеется ещё более забавный. Если установить отрицательные смещения, то затираются существующие строчки базы. Точнее, так это проявляется в Windows 2000. В Windows XP старые строчки не затираются, но в базе оказывается две строчки с одним индексом. И при деинсталляции unloctr удаляет более старую…

Если потребуется определить индексы имён/описаний, которые получили объекты/счётчики, нужно просто прочитать из «HKLM\SYSTEM\CurrentControlSet\Services\<имя>\Performance» значения «First Counter» и «First Help» и прибавить соответствующие xxxxX_offset.

СОВЕТ

Если вы не хотите запускать эти утилиты, можете воспользоваться функциями LoadPerfCounterTextStrings и UnloadPerfCounterTextStrings из loadperf.dll (объявлены в Loadperf.h). Перед использованием обязательно прочитайте статью Q188769 (BUG: LoadPerfCounterTextString Fails with Error 87). Она уже даже не забавная...

Почти настоящая DLL

В качестве примера рассмотрим «самодостаточную» DLL. Самодостаточную в том смысле, что данные (значения счётчиков, количество экземпляров, их имена) она будет генерировать сама при помощи генератора случайных чисел. Это существенно упрощает код. Некоторые рассуждения об источнике данных и связаных с ним проблемах есть в следующем разделе, пока что будем рассматривать его как «элементарную деталь реализации, не имеющую отношения к рассматриваемому вопросу».

Ещё одна «деталь», которую я проигнорировал – разбор строки запроса. Но тут всё достаточно прозрачно, просто жалко места. Параметр lpwszValue функции Collect содержит набор (ни одной, одну или более) строк, разделённых пробелами. Перебираем, ищем знакомые строчки. Такие как «Global», «Costly», индексы имён объектов.

ПРИМЕЧАНИЕ

Для большинства стандартных объектов пустая строка является аналогом «Global». У себя на машине я обнаружил только одно исключение – объект «Print Queue», он почему-то не реагирует на пустую строку. Или наоборот, остальные почему-то реагируют…

«Одиночный» объект

Рассмотрим наиболее простой случай – один объект, два счётчика.

ПРИМЕЧАНИЕ

Разнообразия ради первый счётчик сделан текстовым (конечно, это «Hello, World!»). Небольшая проблема заключается в том, что оснастка Performance не умеет отображать такие счётчики (вроде бы не столько она, сколько библиотека PDH, но нам это безразлично). Пользуйтесь PCView из первой части статьи :) Второй счётчик Performance показывает без проблем.

Код с комментариями:

          #include <windows.h>   
#include"offset.h"// Содержит смещения индексов счетчиков и объекта#include"registry.h"// Объявление пространства имён для работы с реестром.// Содержит функцию – чтение значений «First Counter»// и «First Help» из нужного ключа.// Код этого модуля не приводится.

PERF_OBJECT_TYPE        type     = {0};
PERF_COUNTER_DEFINITION counter1 = {0};
PERF_COUNTER_DEFINITION counter2 = {0};
PERF_COUNTER_BLOCK      bl       = {0};

const WCHAR* hw = L"Hello, World!";

// Округляет v вверх до кратности 8, напишите её сами ;) 
DWORD UpTo8(DWORD v);

//// Вызывается при загрузке, инициализирует глобальные структуры.extern"C"__declspec(dllexport) DWORD CALLBACK Open(LPWSTR lpDeviceNames)
{
  DWORD name;
  DWORD help;

  // Читает реестр, получает значения «First Counter» и «First Help»
  DWORD res = Registry::GetFirst(&name, &help);
  if (res != ERROR_SUCCESS)
  {
    // Не судьба. Вероятно, библиотека некорректно зарегистрирована.return res;
  }

  // Инициализируем описание объекта. Размер объекта вычисляется в конце.
  type.ObjectNameTitleIndex  = name + TYPE_OFFSET;
  type.ObjectHelpTitleIndex  = help + TYPE_OFFSET;
  type.NumCounters           = 2;                   // Два счётчика
  type.NumInstances          = PERF_NO_INSTANCES;   // Никаких экземпляров
  type.HeaderLength          = sizeof(type);
  
  // Инициализируем описание первого счётчика
  counter1.CounterNameTitleIndex = name + COUNTER1_OFFSET;
  counter1.CounterHelpTitleIndex = help + COUNTER1_OFFSET;
  counter1.CounterSize           = (lstrlenW(hw) + 1) * sizeof(WCHAR);
  counter1.CounterType           = PERF_SIZE_VARIABLE_LEN | PERF_TYPE_TEXT 
                                          | PERF_TEXT_UNICODE;
  // До него должна влезть структура PERF_COUNTER_BLOCK
  counter1.CounterOffset         = sizeof(bl);
  counter1.ByteLength            = sizeof(counter1);

  // Инициализируем описание второго счётчика
  counter2.CounterNameTitleIndex = name + COUNTER2_OFFSET;
  counter2.CounterHelpTitleIndex = help + COUNTER2_OFFSET;
  counter2.CounterSize           = sizeof(DWORD);
  counter2.CounterType           = PERF_SIZE_DWORD | PERF_TYPE_NUMBER;
  // Идёт сразу после первого
  counter2.CounterOffset     = counter1.CounterOffset + counter1.CounterSize;
  counter2.ByteLength        = sizeof(counter2);

  // размер данных – смещение последнего счётчика плюс его длина
  bl.ByteLength = counter2.CounterOffset + counter2.CounterSize;

  type.DefinitionLength = type.HeaderLength +
                          counter1.ByteLength +
                          counter2.ByteLength;

  // Размер объекта должен быть кратен 8 байтам, иначе в EventLog добавится// сообщение, рекомендующее обратиться к производителю за новой версией dll.
  type.TotalByteLength  = UpTo8(type.DefinitionLength + bl.ByteLength);
  
  return ERROR_SUCCESS;
}

//// Вызывается при сборе данных. Не анализирует строку // запроса, просто копирует данные в буфер.extern"C"__declspec(dllexport) DWORD WINAPI Collect(LPWSTR lpwszValue, 
                                                      LPVOID *lppData, 
                                                      LPDWORD lpcbBytes, 
                                                      LPDWORD lpcObjectTypes)
{
  if (*lpcbBytes < type.TotalByteLength)
  {
    // Не влезаем
    *lpcbBytes = 0;
    *lpcObjectTypes = 0;
    return ERROR_MORE_DATA;
  }

  char* temp = (char*)(*lppData);

  // Копируем описание объекта
  memcpy(temp, &type, sizeof(type));
  temp += type.HeaderLength;

  // Копируем описание первого счётчика
  memcpy(temp, &counter1, sizeof(counter1));
  temp += counter1.ByteLength;

  // Копируем описание второго счётчика
  memcpy(temp, &counter2, sizeof(counter2));
  temp += counter2.ByteLength;

  // Копируем заголовок блока данных
  memcpy(temp, &bl, sizeof(bl));

  // Копируем данные первого счётчика
  memcpy(temp+counter1.CounterOffset, hw, (lstrlenW(hw) + 1)*sizeof(WCHAR));

  DWORD v = rand() % 10;

  // Копируем данные второго счётчика
  memcpy(temp + counter2.CounterOffset, &v, sizeof(DWORD));

  // Устанавливаем выходные параметры
  *lppData        = (char*)(*lppData) + type.TotalByteLength;
  *lpcbBytes      = type.TotalByteLength;
  *lpcObjectTypes = 1;

  return ERROR_SUCCESS;
}

extern"C"__declspec(dllexport) DWORD WINAPI Close()
{
  return ERROR_SUCCESS;
}

Согласитесь, ничего сложного. А ведь работает! :) Запустив оснастку Performance, можно даже следить за скачками второго счётчика.

«Экземплярный» объект

Теперь рассмотрим более сложный случай. Один объект, один счётчик и неизвестное количество экземпляров. Чтобы обобщить, генерация данных вынесена в отдельный файл, я реализовал его с помощью функции rand().

ПРИМЕЧАНИЕ

И снова возникает проблема с оснасткой Performance. Если количество экземпляров и их имена постоянно изменяются случайным образом, применить её не удастся. Поэтому для проверки работоспособности придётся сделать версию с постоянным количеством экземпляров и статическими именами.

Код для этого случая приведён ниже:

          #include <windows.h>   
#include"offset.h"// Содержит смещения индексов счетчиков и объекта#include"registry.h"// Объявление пространства имён для работы с реестром.// Содержит функцию – чтение значений «First Counter»// и «First Help» из нужного ключа.// Код этого модуля не приводится.#include"data_provider.h"// Модуль, генерирующий данные. Содержит// пространство имён Data. Код не приводится.

PERF_OBJECT_TYPE         type     = {0};
PERF_COUNTER_DEFINITION  counter  = {0};
PERF_INSTANCE_DEFINITION instance = {0};
PERF_COUNTER_BLOCK       bl       = {0};

// Округляет v вверх до кратности 8, напишите её сами ;) 
DWORD UpTo8(DWORD v);

//// Вызывается при загрузке, инициализирует глобальные структуры.extern"C"__declspec(dllexport) DWORD CALLBACK Open(LPWSTR lpDeviceNames)
{
  DWORD name;
  DWORD help;

  // Читает реестр, получает значения "First Counter" и "First Help"
  DWORD res = Registry::GetFirst(&name, &help);
  if (res != ERROR_SUCCESS)
  {
    // Не судьба. Вероятно, библиотека некорректно зарегистрирована.return res;
  }

  // Инициализируем описание объекта. Размер объекта и количество// экземпляров вычисляются в Collect
  type.ObjectNameTitleIndex  = name + TYPE_OFFSET;
  type.ObjectHelpTitleIndex  = help + TYPE_OFFSET;
  type.NumCounters           = 1;
  type.HeaderLength          = sizeof(type);
  type.CodePage              = 0;               // Все имена в unicode// Инициализируем описание счётчика
  counter.CounterNameTitleIndex = name + COUNTER_OFFSET;
  counter.CounterHelpTitleIndex = help + COUNTER_OFFSET;
  counter.CounterSize           = sizeof(DWORD);
  counter.CounterType           = PERF_SIZE_DWORD | PERF_TYPE_NUMBER;
  counter.CounterOffset         = sizeof(bl);
  counter.ByteLength            = sizeof(counter);

  // Инициализируем описание экземпляра// остальные поля будут уникальны у каждого экземпляра.
  instance.ParentObjectTitleIndex = 0;
  instance.ParentObjectInstance   = 0;
  instance.UniqueID               = PERF_NO_UNIQUE_ID;
  instance.NameOffset             = sizeof(instance);

  // Инициализируем блок данных
  bl.ByteLength = counter.CounterOffset + counter.CounterSize;
  
  type.DefinitionLength = type.HeaderLength + counter.ByteLength;

  return ERROR_SUCCESS;
}

//// Вызывается при сборе данных. Не анализирует строку // запроса, просто копирует данные в буфер.extern"C"__declspec(dllexport) DWORD WINAPI Collect(LPWSTR lpwszValue,
                                                      LPVOID *lppData, 
                                                      LPDWORD lpcbBytes, 
                                                      LPDWORD lpcObjectTypes)
{
  char* temp = (char*)(*lppData);
  int size = type.DefinitionLength;

  // Возвращает количесто экземпляровint count = Data::get_instances_count();

  // Где-нибудь тут можно предварительно оценить необходимый размер буфера// и сравнить его с имеющимся. Для простоты опущено.for (int i = 0; i < count;i++)
  {
    WCHAR* name = 0;
    int    name_len = 0;
  // Возвращает имя и его длину. Выделяет память для имени malloc-ом
    Data::get_name(i, &name, &name_len);
  // Длина имени – в байтах; если не добавлять завершающий// нуль, Performance работает некорректно
    instance.NameLength = (name_len + 1) * sizeof(WCHAR);
    instance.ByteLength = instance.NameOffset + instance.NameLength;

    if (UpTo8(size + instance.ByteLength + bl.ByteLength) > *lpcbBytes)
    {
      // не влезаем
      free(name);
      *lpcbBytes = 0;
      *lpcObjectTypes = 0;
      return ERROR_MORE_DATA;
    }

    // Копируем определение экземпляра
    memcpy(temp + size, &instance, sizeof(instance));
    // Копируем имя экземпляра
    memcpy(temp + size + instance.NameOffset, name, instance.NameLength);

    size += instance.ByteLength;

    // Копируем структуру PERF_COUNTER_BLOCK
    memcpy(temp + size, &bl, sizeof(bl));

    // Возвращает данные
    DWORD data = Data::get_data(i);

    // Копируем данные
    memcpy(temp + size + counter.CounterOffset, &data, sizeof(data));

    size += bl.ByteLength;

    free(name);
  }

  type.NumInstances = count;
  type.TotalByteLength = UpTo8(size);

  // Копируем описание типа
  memcpy(temp, &type, sizeof(type));
  
  // Копируем описание счётчика
  memcpy(temp + type.HeaderLength, &counter, sizeof(counter));

  // Устанавливаем выходные параметры
  *lppData        = (char*)(*lppData) + type.TotalByteLength;
  *lpcbBytes      = type.TotalByteLength;
  *lpcObjectTypes = 1;

  return ERROR_SUCCESS;
}

extern"C"__declspec(dllexport) DWORD WINAPI Close()
{
  return ERROR_SUCCESS;
}

В общем, опять ничего сложного. И опять работает. Но необходимость следить за всеми этими смещениями, размерами и выравниваниями раздражает и вносит существенную вероятность ошибки. Очевидно, что процесс заполнения и копирования структур нужно как-то автоматизировать. Как именно – не так уж важно. Мой вариант вы найдёте в примерах, описанных в конце статьи.

Рассуждения о настоящей DLL

ПРИМЕЧАНИЕ

В статье представлены только рассуждения, а не код. Однако, ограничившись ими, я поступил бы, наверное, не самым лучшим образом. Поэтому один из примеров – «настоящая» DLL. Но, естественно, пример один, а вариантов много, иначе и рассуждать не о чем. По ходу этого раздела я буду ссылаться на выбранный мной вариант реализации.

Единственное существенное отличие «настоящей dll» от примеров, рассмотренных в предыдущем разделе, – это источник данных. Обычно данные кем-то генерируются и их нужно сначала получить, скорее всего, из другого процесса (случай с отображением статистики не рассматриваем; во-первых, это проще, во-вторых, большая часть статистики уже отображается; случай с базами данных и прочими источниками информации тоже не рассматриваем). Тут есть несколько вариантов:

В примере я использую COM.

Далее, независимо от выбранного метода IPC, может появиться ещё одна проблема – согласованность данных. Допустим, у вас два счётчика, которые всегда одинаковы и меняются одновременно. Пока вы получаете значение первого счётчика, значение второго может успеть поменяться. В результате вы получите разные значения, охранная система, которая следит за этими счётчиками, поднимет панику, откроет стрельбу, пустит газ, десять человек погибнет, а потом они придут за вами... Вариантов решения проблемы несколько:

Я использую последний вариант. Он прост и понятен, но имеет существенный недостаток – у сервера получается довольно бедный интерфейс (один метод, возвращающий либо сырые данные, либо SAFEARRAY структур; если у вас одиночный объект, то, конечно, просто метод с 25-ю параметрами), который может плохо сочетаться с другими клиентами (а вдруг таковые будут?). В остальных вариантах такого ограничения нет. Но зато чем удобнее интерфейс, тем больше межпроцессных вызовов нужно сделать, тем сильнее всё это будет тормозить... Как обычно, нужно выбирать.

И ещё несколько вопросов, над которыми можно подумать:

Но это для продвинутых. В примере всё максимально просто: запрос инициирует dll, сервер передает всё в формате, максимально удобном для сервера, кэширование всех данных производится на одну десятую секунды.

Регистрация в базах имён

Это какая-то больная тема. Рихтер делал – не сделал, авторы ATL 7 делали – не сделали... Причём ошибки не в мелочах, а в подходе. Авторы lodctr делали и сделали, но, во-первых, допустили другие забавные ошибки, во-вторых, использовать ini-файлы не всегда удобно... Последняя надежда на меня :)

Как делать правильно

Если когда-нибудь Вам захочется реализовать регистрацию вручную, делайте так:

Перебрать все подключи «HKLM\Software\Microsoft\Windows NT\CurrentVersion\Perflib» (функция RegEnumKey), для каждого подключа:

Подобный подход гарантирует, что во все присутствующие в системе базы будет добавлена информация о Ваших объектах/счётчиках. Это необходимое условие корректной регистрации, так как иначе программы, использующие необновлённые базы, не смогут получить имена и описания Ваших объектов и счётчиков и, скорее всего, просто не будут их отображать. Если необновлёнными останутся локализованные базы, Ваши объекты будут невидимы для всех программ, использующих PDH, в том числе и для оснастки Performance.

Пример реализации есть в проекте mycoolcounter, описанном в конце статьи.

Введение в реализацию регистрации в ATL 7

Рассмотрим PerformanceScribble – пример использования счётчиков производительности в приложениях ATL 7 из MSDN. Проект состоит из двух частей: приложения PerformanceScribble.exe и библиотеки PerfDll.dll, но так как весь код регистрации сосредоточен в PerfDll.dll, нас будет интересовать только она.

Регистрация происходит при вызове экспортируемой функции DllRegisterServer. Она выглядит так:

STDAPI DllRegisterServer(void)
{
	return _AtlModule.DllRegisterServer(FALSE);
}

_AtlModule это глобальный объект, являющийся экземпляром класса, унаследованного от шаблона CAtlDllModuleT<>. Его метод DllRegisterServer сначала вызывает RegisterAppId (который нас не интересует), а потом RegisterServer.

HRESULT CAtlModuleT<T>::RegisterServer(...) 
{
	...
#ifndef _ATL_NO_PERF_SUPPORT

	if (SUCCEEDED(hr) && _pPerfRegFunc != NULL)
		hr = (*_pPerfRegFunc)(_AtlBaseModule.m_hInst);

#endifreturn hr;
}

_pPerfRegFunc это глобальная переменная, объявленная в «atlbase.h» как _ATL_PERFREGFUNC. По умолчанию она равна NULL, но если в проект включён файл «atlperf.h», это указатель на функцию RegisterPerfMon. RegisterPerfMon перебирает все объекты-наследники CPerfMon, заявившие о себе при помощи макроса PERFREG_ENTRY (реализация неочевидна и красива), у каждого вызывает метод Register и RegisterAllStrings. Register не очень интересен, он просто добавляет ключ в «HKLM\System\CurrentControlSet\Service» и устанавливает его значения «Open», «Close» и т.п. (ещё он строит «карту» - массив структур, описывающих объекты и счётчики, и также сохраняет её в реестре, но, поскольку RegisterAllStrings строит такую же карту заново, этим можно пренебречь). А вот RegisterAllStrings – то, что мы ищем.

Без лишних деталей (некоторые из них ещё будут рассмотрены) метод выглядит так:

HRESULT CPerfMon::RegisterAllStrings(HINSTANCE hResInstance)
{
	UINT nRes;
	hr = CreateMap(0, NULL, &nRes);

	// Это будет первый вариантif (nRes == 0)
		return RegisterStrings(0, hResInstance);

	// Это будет второй вариантif (hResInstance != NULL)
		return _RegisterAllStrings(nRes, hResInstance);
	// Этот подтип второго варианта, подробнее ниже
	...
}

CreateMap – виртуальная функция, обычно она переопределяется потомками при помощи карты макросов BEGIN_PERF_MAP/CHAIN_PERF_OBJECT/END_PERF_MAP. Слегка адаптированная версия:

          #define BEGIN_PERF_MAP(AppName) \
private: \
LPCTSTR GetAppName() constthrow() { return AppName; } \
HRESULT CreateMap(WORD wLanguage, HINSTANCE hResInstance, \
		UINT* pSampleRes = NULL) throw() \
{ \
	CPerfMon* pPerf = this; \
	if (pSampleRes) *pSampleRes = 0; \
	HRESULT hr; \
	/* Очищает карту */ \
	ClearMap();

#define CHAIN_PERF_OBJECT(objectclass) \
	/* Вызывает статическую функцию CreateMap «прикреплённого» объекта */ \ 
	hr = objectclass::CreateMap(pPerf, wLanguage, hResInstance, pSampleRes); \
	if (FAILED(hr)) \
		return hr;

#define END_PERF_MAP() \
	return S_OK; \
}

«Прикреплённые» классы представляют собой объекты производительности, их CreateMap-ы определяются при помощи ещё одной карты макросов - BEGIN_COUNTER_MAP/DEFINE_COUNTER_EX/END_COUNTER_MAP. Получающиеся функции добавляют в карту описание объекта и всех его счётчиков. Как именно это происходит в данном случае не важно, важно только влияние функций на параметр pSampleRes. Если имя объекта/счётчика задано в виде идентификатора строки из ресурсов, значение pSampleRes устанавливается равным этому идентификатору, иначе (если имя это просто строчка) оно не меняется.

Первый вариант (nRes == 0)

Поскольку в проекте PerfDll все имена заданы строками, то по окончании «большой» CreateMap параметр pSampleRes равен 0. Врезультате мы оказываемся внутри функции RegisterStrings, причём в первом параметре она получает 0.

RegisterStrings имеет следующий прототип:

HRESULT CPerfMon::RegisterStrings(
	LANGID language,
	HINSTANCE hResInstance);

Эта функция предназначена для регистрации объектов и счётчиков в базе, язык которой передан в параметре language. Если language равен 0, вместо него используется результат GetThreadLocale.

Поскольку региональные настройки у меня стоят русские, GetThreadLocale возвращает 0x419, а так как Windows английская, RegisterStrings обламывается и объекты/счётчики остаются незарегистрированными нигде, DllRegisterServer возвращает ошибку, о которой сообщает regsvr32.

Это, конечно, печально, но это ещё не всё. Допустим, у меня была бы русская версия Windows и русские настройки. В этом случае регистрация прошла бы без ошибок, но в базе «009» информации об объектах и счётчиках бы не появилось. На первый взгляд, всё нормально – оснастка Performance и PDH без проблем работают с объектами. Но к каким отдалённым последствиям приведёт такая ситуация я не знаю.

Ещё один возможность – русская Windows и английские настройки (оригинально, но чего не бывает). В этом случае инсталляция пройдёт без ошибок (так как база «009» всегда существует), но вот ни оснастка Performance ни PDH так ничего и не увидят...

Как Вы, наверное, уже догадались, единственная ситуация, в которой код регистрации даёт приемлемый результат – английская Windows и английские настройки...

Второй вариант (nRes != 0)

Но это же был простой пример из MSDN. Конечно, настоящие программисты в настоящих проектах хранят строки в ресурсах. Вот только им это плохо помогает…

          inline HRESULT CPerfMon::_RegisterAllStrings(
	UINT nRes,
	HINSTANCE hResInstance
	) throw()
{
	HRESULT hrReturn = S_FALSE;
	HRESULT hr;

	CAtlArray<LANGID> langs;

	// Перебираем все языки, на которых существует // строчка с данным ID, добавляем идентификаторы// в массив. Конструкция (nRes>>4)+1, скорее всего,// необходима из-за особенностей хранения строк// в ресурсах – блоками по 16 штук.if (!EnumResourceLanguages(
			hResInstance, 
			RT_STRING, 
			MAKEINTRESOURCE((nRes>>4)+1), 
			EnumResLangProc, 
			reinterpret_cast<LPARAM>(&langs)))
		return AtlHresultFromLastError();

	for (UINT i = 0; i < langs.GetCount(); i++)
	{
		// Регистрация в базе langs[i] 
		hr = RegisterStrings(langs[i], hResInstance);
		if (FAILED(hr))
			return hr;
		if (hr == S_OK)
			hrReturn = S_OK;
	}

	return hrReturn;
}

Очевидный недостаток этого кода – ресурсы должны очень точно соответствовать операционной системе пользователя. Так как, если языков не хватает, некоторые базы останутся необновлёнными, а если в ресурсах присутствует лишний язык, обновить соответствующую ему базу не удастся (за отсутствием), функция прекратит регистрацию (возможно, не успев обновить некоторые базы) и вернёт ошибку. Ситуация усугубляется ещё и тем, что в первом случае, скорее всего, будет не хватать именно локального языка, то есть с объектами не смогут работать PDH и оснастка Performance.

Этот вариант несколько лучше предыдущего, так как теоретически его можно заставить работать на любой заданной конфигурации Windows, но:

Второй вариант, но (hResInstance== 0)

Код выглядит так:

HRESULT CPerfMon::RegisterAllStrings(HINSTANCE hResInstance) 
{
	...
	for (int i = 0; 
		hResInstance = _AtlBaseModule.GetHInstanceAt(i), hResInstance != NULL;
		i++)
	{
		hr = _RegisterAllStrings(nRes, hResInstance);
		if (FAILED(hr))
			return hr;
		if (hr == S_OK)
			hrReturn = S_OK;
	}

	return hrReturn;
}

В этом случае поиск ресурсов происходит по всем модулям, которые были добавлены при помощи AddResourceInstance, что позволяет вынести локализованные строчки в отдельную dll и при переходе к версии Windows с другим языком изменять только эту dll. Такой подход, конечно, повышает гибкость, но, во-первых, не отменяет недостатков, рассмотренных в предыдущем подразделе, во-вторых, я так и не понял, как добиться того, чтобы на входе RegisterAllStrings параметр hResInstance равнялся 0.

Код

Во-первых, я, конечно, проверил работоспособность модулей из раздела «Почти настоящая dll». После этого я решил предоставить их в качестве примеров. :) Бонус – недостающие в листинге части кода, ini-файлы, намёк на саморегистрацию dll через DllRegisterServer («намёк», потому что написан по принципу «лишь бы работало»). Проекты называются «myperf1» и «myperf1_inst» (от instance). Для регистрации нужно запустить regsvr32 (в результате DllRegisterServer создаст ключ в реестре и заполнит значения «Library», «Open», «Close» и «Collect»), а потом lodctr, передав в командной строке имя ini-файла. Для удаления нужно проделать то же самое, но в обратном порядке, то есть сначала запустить unlodctr, а потом regsvr32 с ключом «/u».

Во-вторых, я переписал эти же проекты с использованием своей небольшой библиотечки. На мой взгляд, получилось гораздо проще, понятней,гибче. Проекты - «myperf2» и «myperf2_inst», библиотека лежит в подкаталоге perf, прокомментирована насквозь.

ПРЕДУПРЕЖДЕНИЕ

Как обычно, я не могу успокоить вас, сказав, что мой код используется в больших проектах сотнями программистов по всему миру... Но я свой код проверил. Вроде работает... Если вдруг что-то не так – пишите, разберёмся.

И в третьих – «настоящий» пример – бегемоты.

Бегемоты

Будем считать бегемотов. У каждого бегемота (бегемотихи) есть параметры: пол, возраст, координата по оси X, координата по оси Y, вес, объём, день рождения. Кроме того, у бегемота есть имя (имя экземпляра) и мама (объект-родительница). Как вы, наверное, догадались, это неформальное описание объекта «Behemoth». На случай, если сведения об отдельных бегемотах пользователя не интересуют, предусмотрим объект «Behemoths Summary», который позволяет узнать количество и суммарный вес бегемотов.

Процесс-источник информации – COM-сервер, реализованный как служба Windows NT. В нём бегемоты живут, размножаются (поскольку размножение – самое сложное действие в моей модели бегемота, реализация получилась несколько хм... озабоченной... но, я думаю, все уже достаточно взрослые...) и умирают.

Поскольку оснастка Performance не способна адекватно отразить всю эту красоту, пришлось написать клиент. А чтобы как-то проверять работу клиента, и вообще для порядка, ещё и менеджер. Менеджер отличается от клиента тем, что, во-первых, получает данные непосредственно от сервера, во-вторых, имеет дополнительную функциональность.

Всё вместе выглядит так (стрелочками обозначено направление передачи данных):


Рис.1. Структура проекта «Behemoth»

И, что самое удивительное, эта конструкция работает!

ПРЕДУПРЕЖДЕНИЕ

Работает, но всё, не относящееся к счётчикам производительности, написано «на коленке». Это не значит «плохо», это значит, что с минимумом обработки ошибок и комментариев.

Немного о каждом компоненте:

Как видите, технология действительно мощная и гибкая. И действительно не очень удобная. Сильно сомневаюсь, что когда-нибудь решусь применить её на практике. Как-то обычно не очень нужно :) Хотя, конечно, чего в жизни не бывает… Но, в любом случае, я уже получил от счётчиков производительности всё, что хотел – почти месяц (если брать чистое время) удовольствия, закончившийся забавным проектом и двумя статьями. Как вы будете применять полученные знания – дело ваше.

Ну и на финал – обещанный в начале статьи хак, демонстрирующий возможности, о которых разработчики технологии и не догадывались (ну, может, и догадывались, но нам с вами не сообщали).

MyCoolCounter

Идея проста – нужно найти, где зарегистрирована системная dll, содержащая объект «System», и прописать вместо неё свою. В своей dll-и нужно загружать оригинальную dll, передавать ей все вызовы, и добавлять к возвращённым данным ещё один счётчик...

По шагам:

a) Предварительные действия

b) Инсталляция

c) Работа

d) Collect

Всё! Оно даже работает. Проект называется mycoolcounter, для разнообразия я решил обойтись без ini-файлов, поэтому для установки и удаления достаточно regsrv32.

Естественно, используя подобную технику, можно не только добавлять счётчики, но и удалять их, изменять значения, тип, и вообще делать с ними всё, что угодно. Очевидное практическое применение – морочить голову знакомым системным администраторам…


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь