Опыт использования утилиты logman для сбора метрик производительности приложения в Windows

Введение


Добрый день!

В своем первом посте я хотел бы поделитьcя опытом использования такой утилиты как logman, с помощью которой можно собирать информацию о потреблении ресурсов (и не только) приложением под Windows.

В один из дней на работе была поставлена задача снятия метрик производительности для одного приложения под Windows. Главным условием было использовать по минимуму какие-либо сторонние утилиты, и так как опыта в подобных вещах у меня к тому времени не было, немного покопавшись, выбор пал на logman. Особой точности не требовалось и надо было лишь понять характер потребления ресурсов приложением, для чего logman с первого взгляда вполне подходил.

Итак, перейдем непосредственно к сути поста.

Что же такое logman?


Многие из вас скорее всего в работе или для домашних нужд использовали нативную Windows утилиту Performance monitor (perfmon). Так вот logman — это грубо говоря command line представление perfmon'а. Он позволяет создавать, запускать, останавливать счетчики производителности, писать результат во внешние файлы и много чего еще. Мне был необходим только базовый функционал, который я и опишу ниже.

Основные операции


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


Просмотр созданных и запущенных счетчиков


Посмотреть список сборщиков данных можно командой logman
В результате чего будет получен примерно такой вывод:
Группа сборщиков данных                  Тип                           Состояние
-------------------------------------------------------------------------------
cpu                                     Счетчик                       Остановлено

Команда выполнена успешно.


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


Для создания счетчика я использовал следующую команду (с полным списком опций можно ознакомиться в официальной документации.):
logman create counter "counter_name" -f csv -si "interval" --v -o "output_file" -c counter


  • counter_name — имя счетчика. Может быть любым, например «explorer_cpu»
  • -f — формат файла с результатами измерений
  • --v — эта опция подавляет вывод версии в выходном файле
  • -si — интервал. с которым будут записываться измерения
  • -o — файл с результатами измерений
  • -c — идентификатор счетчика производительности


Остановимся подробнее на последней опции. Чтобы понять, что вписывать в поле «counter», можно проследовать в perfmon, создать группу сборщиков данных и выбрать «Создать -> Сборщик данных». Далее выбрать «Добавить счетчик» и в появившемся окне найти интересующий нас тип счетчика.



Меня интересовало потребление cpu и памяти конкретным процессом, поэтому я использовал для измерений следующие счетчики (в расчете CPU мне помог следующий пост):

  • "\Processor(_Total)\% User time" — процент CPU в пользовательском режиме
  • "\Process(application_name)\% Processor time" — доля CPU приложения от пользовательского режима
  • "\Process(application_name)\Working Set" — использование оперативной памяти процессом


Процент потребления CPU процессом можно расчитать как ("\Processor(_Total)\% User time" * "\Process(application_name)\% Processor time") / (количество_процессоров * 100)

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

В итоге приведу пример готовой команды для создания счетчика:

logman create counter "explorer-cpu.usage" -f csv -si 1 --v -o "d:\logman-test\raw\explorer-cpu.usage.csv" -c "\Processor(_Total)\% User time" "\Process(explorer)\% Processor time"


Запуск и остановка счетчика


Выполняются соответствующими командами:
logman start "counter_name"

logman stop "counter_name"


Удаление счетчика


Удалить счетчик можно следующей командой (он обязательно должен быть перед этим остановлен):
logman delete "counter_name"


Обертка на Python


Прежде чем перейти к коду, хотелось бы рассказать о небольшой (хотя для меня она стала довольно существенной) проблеме, которая возникла в ходе сбора метрик.

Так как собирать данные предполагалось за довольно небольшой промежуток времени (5-15 секунд), то стало понятно, что интервал сбора в 1 с (задаваемый через опцию "-si") слишком большой. Поискав на просторах интернета и в оф. документации, мне не удалось найти способ, которым этот интервал можно было уменьшить, а количество получаемых точек на графике очень хотелось увеличить. Немного поразмыслив, я решил пойти от противного — если нельзя уменьшить интервал, то можно увеличить количетство счетчиков. Таким образом, запуская подряд несколько сборщиков с интервалом в 100-500 миллисекунд, можно получить за одну секунду сколько нужно данных.

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

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

pylogman.py
import os
import logging
import random
import time
import subprocess
import glob
import multiprocessing


CPU_USER_COUNTER = '"\Processor(_Total)\% User time"'
CPU_APP_COUNTER = '"\Process({app})\% Processor time"'
MEMORY_COUNTER = '"\Process({app})\Working Set"'
APP_PLACEHOLDER = '{app}'


class PerflogManager(object):

    def __init__(self, app_name, precision, results_dir, max_mem=1, interval=1):
        """
        Args:
            app_name: name of the application without "exe" extention
            precision: amount of
            results_dir: output folder
            max_mem: this value is used to calculate % from total avaliable memory for further plotting
            interval: interval which will be used as "-si" logman option value
        """

        self._app_name = app_name
        self._precision = precision
        self._counters = { 'cpu.usage': '{cpu_user} {cpu_app}'.format(cpu_user=CPU_USER_COUNTER,
                                                                      cpu_app=CPU_APP_COUNTER).replace(APP_PLACEHOLDER, app_name),
                           'mem.usage': MEMORY_COUNTER.replace(APP_PLACEHOLDER, app_name) }
        self._results_dir = os.path.normpath(results_dir)
        self._raw_dir = os.path.join(self._results_dir, 'raw')
        self._final_dir = os.path.join(self._results_dir, 'csv')
        self._interval = interval
        self._max_mem = max_mem
        self._collectors = []
        if not os.path.exists(self._results_dir):
            os.makedirs(self._results_dir)
            os.makedirs(self._raw_dir)
            os.makedirs(self._final_dir)
            logging.info('Directory "{path}" for logman results has been created.'.format(path=self._results_dir))
        logging.info('Performance logman manager has been created for "{}" application.'.format(self._app_name))


    def init_collectors(self):
        """
        Creates collectors by "logman create" command
        """

        logging.info('Creating collectors set...')
        for lang in self._counters:
            for idx in range(self._precision):
                collector = '{app}-{collector}-{id}'.format(app=self._app_name,
                                                            collector=lang,
                                                            id=idx)
                output = os.path.join(self._raw_dir, collector + '.csv')
                self.__create(collector, self._counters[lang], output)
                self._collectors.append(collector)


    def start_all(self):
        """
        Starts all the collectors from self._collectors list
        """

        for collector in self._collectors:
            self.__start(collector)
            time.sleep(random.uniform(0.1, 0.9))


    def stop_all(self):
        """
        Stops and deletes all initiated collectors from self._collectors list
        """

        for collector in self._collectors:
            self.__stop(collector)
            self.__delete(collector)


    def cleanup(self):
        """
        Cleans raw_dir directory which contains unprocessed csv file from logman
        """
        logging.info('Cleaning results directory from unnecessary files.')
        for collector in self._collectors:
            csv = os.path.join(self._raw_dir, '{}.csv'.format(collector))
            if os.path.isfile(csv):
                os.remove(csv)


    def process_csv(self):
        """
        Composes and formats data from all counters for further plotting
        """

        for lang in self._counters:
            final_output = os.path.join(self._final_dir, '{}.csv'.format(lang))
            self.__compose_csv(final_output, lang)
            self.__format_data(final_output)


    def __compose_csv(self, output, counter):
        """
        Concatenates gathered csv data to one file
        """

        logging.info('Composing file "{}".'.format(output))
        try:
            with open(output, 'a') as outfile:
                for csv in glob.glob(os.path.join(self._raw_dir, '*{}*'.format(counter))):
                     with open(csv, 'r') as file:
                        outfile.writelines(file.readlines()[1:]) # Get rid of a file header with service info
            logging.info('File {} successfully created.'.format(output))
        except (FileNotFoundError, IOError) as e:
            logging.error('Failed to compose file {file}: {exception}'.format(file=output,
                                                                              exception=e))


    def __format_data(self, file):
        """
        Sorts data after self.__compose_csv function and calculates % of CPU and Memory
        """

        try:
            with open(file, 'r') as csv:
                raw_data = csv.readlines()
            with open(file, 'w') as csv:
                sorted_data = [line.replace('"', '').replace(',', ';')
                            for line in sorted(raw_data)
                            if '" "' not in line]
                csv_data = []
                if 'cpu' in file:
                    for line in sorted_data:
                        time, cpu_user, cpu_app = line.split(';')
                        cpu = (float(cpu_user) * float(cpu_app)) / (multiprocessing.cpu_count() * 100)
                        csv_data.append('{};{:.2f}\n'.format(time, cpu))
                if 'mem' in file:
                    for line in sorted_data:
                        time, total_memory = line.split(';')
                        mem = float(total_memory) / (self._max_mem * 10000)
                        csv_data.append('{};{:.2f}\n'.format(time, mem))
                csv.writelines(csv_data)
        except (FileNotFoundError, IOError) as e:
            logging.error('Failed to process file "{file}": {exception}'.format(file=file,
                                                                                exception=e))


    def __logman(self, cmd):
        """
        Default wrapper for logman commands

        Args:
            cmd: windows command to be executed
        """

        try:
            logging.debug('Running {}'.format(cmd))
            subprocess.check_call(cmd)
        except (subprocess.CalledProcessError, OSError) as e:
            logging.error('Failed to execute command "{}": {}.'.format(cmd, e))


    def __create(self, name, counter, output):
        """
        Creates logman counter

        Args:
            name: uniq name of a collector
            counter: type of the counter (can be taken from perfmon)
            output: csv file for results
        """

        logging.info('Creating collector "{}"'.format(name))
        cmd = 'logman create counter "{name}" -f csv -si {interval} ' \
              '--v -o "{output}" -c {counter}'.format(name=name,
                                                      interval=str(self._interval),
                                                      output=output,
                                                      counter=counter)
        self.__logman(cmd)


    def __start(self, name):
        """
        Starts logman collector

        Args:
            name: uniq name of a collector
        """

        logging.info('Starting collector "{}".'.format(name))
        cmd = 'logman start {}'.format(name)
        self.__logman(cmd)


    def __stop(self, name):
        """
        Stops logman collector

        Args:
            name: uniq name of a collector
        """

        logging.info('Stopping collector "{}".'.format(name))
        cmd = 'logman stop {}'.format(name)
        self.__logman(cmd)


    def __delete(self, name):
        """
        Deletes logman collector

        Args:
            name: uniq name of a collector
        """

        logging.info('Deleting collector "{}".'.format(name))
        cmd = 'logman delete {}'.format(name)
        self.__logman(cmd)


if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    app = 'skype'
    start_time = time.strftime('%Y%m%d-%H%M%S', time.gmtime())
    logman = PerflogManager(app, 3, 'd:/logman-test/')
    logman.init_collectors()
    logman.start_all()
    time.sleep(20)
    logman.stop_all()
    logman.process_csv()



Заключение


В заключении хотелось бы сказать, что logman — довольно полезная утилита, если небходимо с минимальными затратами автоматизировать сбор метрик производительности под Windows. В сжатые сроки мне не удалось найти более удобного способа, так что буду рад узнать о других путях и подходах в решении такого рода задач в комментариях.
  • +10
  • 14.9k
  • 7
Share post

Similar posts

Comments 7

    0
    Есть вопрос по поводу вашей формулы.

    Насколько я вижу вы хотите посчитать какой процент времени программа работает в режиме User Mode.
    Допустим что "\Process(application_name)\% Processor time" показывает только User Mode, хотя я неуверен.
    Тогда математика была-бы такой:

    ("\Process(application_name)\% Processor time" * "\Processor(_Total)\% User time") / 100

    И потом эту цифру ещё предполагается поделить на колличество ядер.

    Так? или я что-то путаю?
      0
      Не совсем так. Я забыл указать, что "\Process(application_name)\% Processor time" показывает количество процентов от Total User time. Например если в выходном файле User Time = 6, а Appication time = 70, то искомое значение будет 6 * 0.7 = 4.2 % при одном процессоре. Максимальное значение значений счетчика User Time — 100*n, где n — количество процессоров. Я сперва сам удивлялся, получая значения в 200-300 % )

      Вообще я воспользовался объяснениями из этого обсуждения. Какой-либо полной информации по счетчика мне найти так и не удалось и познавалось все эмпирически.
        0
        Так всё таки надо делить на 100.
        Значене "\Process(application_name)\% Processor time" отдается счётчиком в процентах.
        Ведь умножаете на 0.7 а не на 70%.
          0
          О, прошу прощения. В самой статье дейтвительно забыл деление на 100. В коде то оно присутствует.
          +1
          Спасибо за статью :)
        0
        запуская подряд несколько сборщиков с интервалом в 100-500 миллисекунд, можно получить за одну секунду сколько нужно данных.


        О, это любопытный подход.
        А вы проверили, что скажем, несколько сборщиков будут выдавать действительно разные отсчёты? Может они извне синхронно тактируются и в течение секунды будут просто выдавать один и тот же отсчёт?
        И ещё интересно, насколько такие множественные счётчики грузят систему?
          0
          Признаюсь, детально этот момент не проверял — будет над чем подумать. Но на первый взгляд выдают действительно разные значения, так как на результатах это напрямую отражается.

          Насчет нагрузки — вообще счетчиков нужно не так много. Опытным путем выявил, что для получения удовлетворяющих результатов, достаточно 3-4-х дополнительных счетчиков. Существенной нагрузки они не создают, но согласен — интересно посмотреть, если их будет например 20-40 штук. Постараюсь проверить в ближайшее время.

          Спасибо за интересные моменты, на которые я не обращал внимание)

        Only users with full accounts can post comments. Log in, please.