Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: VladCore  
Дата: 20.11.19 11:52
Оценка: 144 (6)
На днях озадачился тестами на appveyor и travis-ci и выложил отдельный nuget-пакет для получения Cpu Usage потока/процесса.

https://github.com/devizer/Universe.CpuUsage

Работает везде. Минимальные версии OS:
— Windows 7/2008R2,
— Linux Kernel 2.6.26+
— macOS 10.9+ (дада, свежий моно работает и на таком старье тоже),
Сначала не хотел писать что на Windows XP/2003 тоже работает, но потом вспомнил что таргеты в nuget есть все начиная с net framework 2.0. Потому упомяну что на XP/2003 тоже работает.

Собственно таргеты в nuget почти все — netcoreapp 1.0+, net framework 2.0+, netstandard 1.3+. На самых первых net standard нельзя было надежно отличить macos от линукс, потому netstandard минимум 1.3 (если конечно старые netstandard кому то нужны).

Авто-тесты на appveyor и travis-ci.org покрывают почти все платофрмы:
* Linux: x86_64 (core & mono), Windows 2016 (только core), Mac OS 10.14 (core & mono)
* Linux Arm 64-bit (core & mono), Arm 32-bit (mono), i386 (mono)
* Старая Mac OS 10.10 (mono)

На Windows arm никогда не тестировал – не на чем даже вручную.
У кого есть, запустить тесты можно как обычно:
dotnet test -f netcoreapp2.2 или 3.0


§

Очень интересные бенчмарки: На Windows получить Cpu Usage стоит столько же сколько и DateTime.Now (200 наносекунд). Mac OS свежая – самая тормознутая (2000 наносекунд).
  Benchmarks


§

Реализация вся работает с помощью P/Invoke, никаких native-бинарников не используются (я это упоминаю потому что в Mono.Posix.NETStandard nuget-пакете от xamarin как и в mono очень много просьб добавить хотя бы getrusage)
  impelementation references


§

Еще интересное наблюдение – гранулярность. На Windows 2016 – 25 микросекунд. А вот на линукс сильно зависит от железа и ядра. На старых ядрах — 10 или 1 миллисекунд. На новых ядрах – 1 микросекунда.

§

Собираюсь добавить класс AdvancedStopwatch прям в этот же nuget с дополнительными свойствами: UserUsage и KernelUsage
Собираюсь написать middle-ware в ASP.NET Core и модуль в ASP.NET которые будут мерять CPU Usage http запроса.

Жду feedback. И от @AndreyAkinshin – может увидим в benchmark.net еще кучу метрик и гистограмм. Ведь kernel usage отличный от нуля говорит о том что скорее всего много malloc/free хоть в GC хоть в unmanaged коде.

О себе — я с 15-го года работаю с .Net на Linux/MacOS, а с .NET под windows — очень давно.

P.S. сорри за скриншоты таблиц картинками — много их просто
Отредактировано 21.11.2019 8:07 VladCore . Предыдущая версия . Еще …
Отредактировано 21.11.2019 7:11 VladCore . Предыдущая версия .
Отредактировано 20.11.2019 22:09 VladCore . Предыдущая версия .
Отредактировано 20.11.2019 22:00 VladCore . Предыдущая версия .
Отредактировано 20.11.2019 12:01 VladCore . Предыдущая версия .
Отредактировано 20.11.2019 11:56 VladCore . Предыдущая версия .
Отредактировано 20.11.2019 11:54 VladCore . Предыдущая версия .
Re: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Sinclair Россия https://github.com/evilguest/
Дата: 21.11.19 04:52
Оценка: 6 (1)
Здравствуйте, VladCore, Вы писали:

VC>На днях озадачился тестами на appveyor и travis-ci и выложил отдельный nuget-пакет для получения Cpu Usage потока/процесса.



VC>

§

VC>Собираюсь добавить класс AdvancedStopwatch прям в этот же nuget с дополнительными свойствами: UserUsage и KernelUsage
Ухты! А как это будет сделано в случае, скажем, асинхронщины?
Ну, то есть когда мы делаем

        private static async void Test()
        {
            var sw = new AdvancedStopwatch();
            sw.Start();
            await ProcessTaskAsync();
            sw.Stop();
            Console.WriteLine($"Time:\t{sw.Elapsed}\nUser:\t{sw.UserUsage}\nKern:\t{sw.KernelUsage}");
        }

        private static Task<int64> ProcessTaskAsync()
        {
            return Task.Factory.StartNew(() =>
            {
                int64 s = 0;
                for(int i=0; i<1000000; i++) s+=i; 
                return s;
            });
        }

Время какого потока будет учитываться в подсчётах — того, в котором был запущен таймер, того, в котором он был остановлен, или всех потоков по мере переключения?


VC>Собираюсь написать middle-ware в ASP.NET Core и модуль в ASP.NET которые будут мерять CPU Usage http запроса.


VC>Жду feedback. И от @AndreyAkinshin – может увидим в benchmark.net еще кучу метрик и гистограмм. Ведь kernel usage отличный от нуля говорит о том что скорее всего много malloc/free хоть в GC хоть в unmanaged коде.

Очень, очень круто.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: VladCore  
Дата: 21.11.19 07:27
Оценка: 114 (1)
Здравствуйте, Sinclair, Вы писали:

VC>>Собираюсь добавить класс AdvancedStopwatch прям в этот же nuget с дополнительными свойствами: UserUsage и KernelUsage


S>Ухты! А как это будет сделано в случае, скажем, асинхронщины?

S>Ну, то есть когда мы делаем

S>
S>        private static async void Test()
S>        {
S>            var sw = new AdvancedStopwatch();
S>            sw.Start();
S>            await ProcessTaskAsync();
S>            sw.Stop();
S>            Console.WriteLine($"Time:\t{sw.Elapsed}\nUser:\t{sw.UserUsage}\nKern:\t{sw.KernelUsage}");
S>        }

S>        private static Task<int64> ProcessTaskAsync()
S>        {
S>            return Task.Factory.StartNew(() =>
S>            {
S>                int64 s = 0;
S>                for(int i=0; i<1000000; i++) s+=i; 
S>                return s;
S>            });
S>        }
S>

S>Время какого потока будет учитываться в подсчётах — того, в котором был запущен таймер, того, в котором он был остановлен, или всех потоков по мере переключения?

Хороший вопрос. А еще появился async foreach.

Ответ есть — AsyncLocal. Вот так работает, но про это или нигде не написано или я плохо искал

public class AdvancedStopwatch
{
    private AsyncLocal<object> _ContextSwitchListener = new AsyncLocal<object>(args =>
    {
        // Сюда "прилетают" вызовы и ПЕРЕД переключением потока и ПОСЛЕ
        // если args.PreviousValue null, то это ПЕРЕД 
        // если args.CurrentValue null, то это ПОСЛЕ
        var tid = Thread.CurrentThread.ManagedThreadId;
        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine($"Thread Changed {(args.ThreadContextChanged ? $"WITH context #{tid}" : $"WITHOUT context #{tid}")}: {args.PreviousValue} => {args.CurrentValue}");
    });

    public AdvancedStopwatch()
    {
        _ContextSwitchListener.Value = "online";
    }

    ....
}


Намного интереснее что делать если код старый и без async/await написан

Может два класса AdvancedStopwatch завести?
— Один будет как я выше написал под async/await
— Второй (для старого кода) будет кидать исключение если Stop() или Elapsed вызван не в том потоке, в котором вызван Start()

Я думаю именно так и надо сделать.
И назвать их AdvancedLegacyStopwatch (второй) и AdvancedAsyncStopwatch (первый)?

S>Очень, очень круто.


Отредактировано 21.11.2019 7:28 VladCore . Предыдущая версия .
Re[2]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Pavel Dvorkin Россия  
Дата: 21.11.19 07:36
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Время какого потока будет учитываться в подсчётах — того, в котором был запущен таймер, того, в котором он был остановлен, или всех потоков по мере переключения?


AFAIK в WinAPI нет такого — получить время всех потоков процеса. Есть GetThreadTimes, которую автор и использует с передачей ей GetCurrentThread(). Так что из какого потока просили, того и получите.

В принципе все это можно (по крайней мере для Windows) доработать, если применить вот это

https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-thread32first
https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-thread32next

но если всерьез это делать, то надо еще и ловить создание/завершение потоков, поскольку пока твой await ждал, там много чего могло произойти в этом плане
With best regards
Pavel Dvorkin
Re[3]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Sinclair Россия https://github.com/evilguest/
Дата: 21.11.19 08:11
Оценка: +1
Здравствуйте, Pavel Dvorkin, Вы писали:

PD>AFAIK в WinAPI нет такого — получить время всех потоков процеса. Есть GetThreadTimes, которую автор и использует с передачей ей GetCurrentThread(). Так что из какого потока просили, того и получите.

Не, речь-то не о том, чтобы получить "время всех потоков". А о том, чтобы понять "сколько времени выполнялся мой код".
В однопоточном случае как раз всё просто и понятно. Линейное время аффектится, в общем-то, только вытеснениями нашего потока со стороны "чужих" потоков; Benchmark.Net потому и гоняет бенчмарк по нескольку раз, чтобы свести влияние постороннего шума к минимуму. AdvancedStopwatch, вычисляя чистое время работы "нашего" потока может помочь ускорить сходимость бенчмарка.

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

PD>В принципе все это можно (по крайней мере для Windows) доработать, если применить вот это


PD>но если всерьез это делать, то надо еще и ловить создание/завершение потоков, поскольку пока твой await ждал, там много чего могло произойти в этом плане

Вот именно. Ну, вот автор вроде как придумал способ
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Pavel Dvorkin Россия  
Дата: 21.11.19 12:37
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Не, речь-то не о том, чтобы получить "время всех потоков". А о том, чтобы понять "сколько времени выполнялся мой код".

S>В однопоточном случае как раз всё просто и понятно. Линейное время аффектится, в общем-то, только вытеснениями нашего потока со стороны "чужих" потоков; Benchmark.Net потому и гоняет бенчмарк по нескольку раз, чтобы свести влияние постороннего шума к минимуму. AdvancedStopwatch, вычисляя чистое время работы "нашего" потока может помочь ускорить сходимость бенчмарка.

Те средства, которые использует автор, к чужим потокам индифферентны. Он использует GetProcessTimes и GetThreadTimes, а они возвращают время исполнения данного процесса или потока. Грубо говоря, сколько машинных тактов он взял. В kernel и user mode

lpKernelTime

A pointer to a FILETIME structure that receives the amount of time that the process has executed in kernel mode. The time that each of the threads of the process has executed in kernel mode is determined, and then all of those times are summed together to obtain this value.

lpUserTime

A pointer to a FILETIME structure that receives the amount of time that the process has executed in user mode. The time that each of the threads of the process has executed in user mode is determined, and then all of those times are summed together to obtain this value. Note that this value can exceed the amount of real time elapsed (between lpCreationTime and lpExitTime) if the process executes across multiple CPU cores.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes

S>В многопоточном случае (все эти Task, await, а также закаты солнц вручную) использование таймера помогает хотя бы косвенно прикинуть, какой код работает быстрее. А вот вычислить чистое время работы "нашего" кода становится как-то тяжело.


А что такое "время работы "нашего" кода" ? Суммарное количество тактов , которое все потоки (или некоторые, которые тебя интересуют — тогда какие именно , как определить ? ) потребили ? Или же астрономическое время от момента T1 до момента T2 ? Если второе — я понимаю твои высказывания выше о чужих потоках. К первому чужие потоки отношения не имеют
With best regards
Pavel Dvorkin
Отредактировано 21.11.2019 12:45 Pavel Dvorkin . Предыдущая версия .
Re[5]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Sinclair Россия https://github.com/evilguest/
Дата: 21.11.19 14:08
Оценка:
Здравствуйте, Pavel Dvorkin, Вы писали:

PD>А что такое "время работы "нашего" кода" ? Суммарное количество тактов , которое все потоки (или некоторые, которые тебя интересуют — тогда какие именно , как определить ? ) потребили ?

Именно. Интересует, сколько "тактов" потратил "наш" код.
Попробую объяснить на пальцах: вот, есть у нас, допустим, какая-то CPU-bound задача. Пусть будет — задача сортировки.
Мы пытаемся выбрать один из нескольких вариантов решения.
Пока мы работаем в одном потоке, всё просто: готовим тестовые данные, и сортируем их различными алгоритмами.
При помощи Stopwatch замеряем, кто быстрее.
Повторяем забег N раз для того, чтобы устранить несистематические ошибки.

Есть такой чудесный проект — Benchmark.Net, который всё это автоматизирует.


Теперь предположим, что мы в одном из вариантов решили молотить данные в несколько потоков.
Понятно, что этот способ покажет лучшее "астрономическое" время.
Но! Что, если этот код у нас бегает не в настольной задаче, а в сервисе, где подобных запросов может быть много одновременно.
Многопоточный вариант всё ещё может быть интересен, т.к. он улучшает response time. Но он также может ухудшать throughput из-за накладных расходов на параллелизацию.

Вот тут, по идее, AdvancedStopwatch сможет помочь — честно подсчитав, сколько времени выполнялся workflow. Вот закавыка как раз в том, "как определить, какие именно" потоки нас интересуют.
Если я правильно понял, VladCore как раз нашёл и способ подсчитать, сколько тактов съел наш алгоритм, несмотря на то, что физически исполнялся он в нескольких разных потоках.

PD>Или же астрономическое время от момента T1 до момента T2 ? Если второе — я понимаю твои высказывания выше о чужих потоках. К первому чужие потоки отношения не имеют

Астрономическое время как раз понятно: оно и так подсчитывается обычным Stopwatch.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Pavel Dvorkin Россия  
Дата: 22.11.19 02:46
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Вот тут, по идее, AdvancedStopwatch сможет помочь — честно подсчитав, сколько времени выполнялся workflow. Вот закавыка как раз в том, "как определить, какие именно" потоки нас интересуют.

S>Если я правильно понял, VladCore как раз нашёл и способ подсчитать, сколько тактов съел наш алгоритм, несмотря на то, что физически исполнялся он в нескольких разных потоках.

Меряет он GetProcessTimes(GetCurrentProcess(),...) и GetThreadTimes(GetCurrentThread(),...)

Разумеется, первое дает количество тактов всего процесса, всех его потоков. Для "настольной задачи", когда потоки в процессе только те, что ты хочешь иметь и никакие посторонние (по отношению к тому, что ты хочешь) потоки не появляются, ты получишь именно то, что тебе надо. Например, для твоей многопоточной сортировки ты и получишь время всех сортирующих потоков и время основного потока, который потом слияние производит (предполагаю, что алгоритм такой). Однако если ты захочешь это делать не в модельной ситуации, ты получишь время всех реально запущенных потоков. Например, если это web-сервер, то ты получишь время всех потоков, которые отвечали на все запросы, и тот, который тебя интересует, и все остальные.

Что касается второго, то тут строго один поток, текущий. Поэтому для основного потока, который await эту сортировку, ты получишь лишь время слияния после сортировки. Чтобы мерить время сортирующих потоков, надо это делать в них самих.
Никто не мешает вызвать GetThreadTimes для любого другого потока . Например, в той же сортировке с фиксированным числом потоков надо было бы при старте этих потоков раздобыть их хендлы, дать потокам отработать, дать основному потоку произвести слияние, а потом найти сумму GetThreadTimes для всех рабочих потоков и основного. Но тут все просто, появление и количество потоков более или менее под контролем, хотя бы в принципе. Если же в течение этого await потоки зарождаются и заканчиваются в этой "сортировке" по желанию каких-то классов дотнета, то надо ловить создание потоков, внедряться в них и мерить их время. В принципе задача решается, так как внедренная в процесс DLL получает уведомления о старте потоков и их окончании (DLL_THREAD_ATTACH и DLL_THREAD_DETACH). Но, похоже, настоящего аналога DllMain в дотнете нет, так что придется внедрять unmanaged DLL. Интересная задача
With best regards
Pavel Dvorkin
Re[7]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Sinclair Россия https://github.com/evilguest/
Дата: 22.11.19 03:57
Оценка: +1
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Разумеется, первое дает количество тактов всего процесса, всех его потоков. Для "настольной задачи", когда потоки в процессе только те, что ты хочешь иметь и никакие посторонние (по отношению к тому, что ты хочешь) потоки не появляются, ты получишь именно то, что тебе надо.
В наше время это редкая роскошь

PD>Например, для твоей многопоточной сортировки ты и получишь время всех сортирующих потоков и время основного потока, который потом слияние производит (предполагаю, что алгоритм такой). Однако если ты захочешь это делать не в модельной ситуации, ты получишь время всех реально запущенных потоков. Например, если это web-сервер, то ты получишь время всех потоков, которые отвечали на все запросы, и тот, который тебя интересует, и все остальные.

Вот именно.

PD>Что касается второго, то тут строго один поток, текущий. Поэтому для основного потока, который await эту сортировку, ты получишь лишь время слияния после сортировки. Чтобы мерить время сортирующих потоков, надо это делать в них самих.

PD>Никто не мешает вызвать GetThreadTimes для любого другого потока . Например, в той же сортировке с фиксированным числом потоков надо было бы при старте этих потоков раздобыть их хендлы, дать потокам отработать, дать основному потоку произвести слияние, а потом найти сумму GetThreadTimes для всех рабочих потоков и основного. Но тут все просто, появление и количество потоков более или менее под контролем, хотя бы в принципе. Если же в течение этого await потоки зарождаются и заканчиваются в этой "сортировке" по желанию каких-то классов дотнета, то надо ловить создание потоков, внедряться в них и мерить их время. В принципе задача решается, так как внедренная в процесс DLL получает уведомления о старте потоков и их окончании (DLL_THREAD_ATTACH и DLL_THREAD_DETACH). Но, похоже, настоящего аналога DllMain в дотнете нет, так что придется внедрять unmanaged DLL. Интересная задача
Ну вот вроде бы AsyncLocal как раз и отслеживает переключения контекстов в процессе исполнения одного и того же workflow. Т.е. теоретически можно следить за всеми переключениями и отлавливать привязку/отвязку потоков от "наших" задач.
Посмотрим, что предложит VladCore на самом деле.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[8]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Pavel Dvorkin Россия  
Дата: 22.11.19 08:33
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Ну вот вроде бы AsyncLocal как раз и отслеживает переключения контекстов в процессе исполнения одного и того же workflow. Т.е. теоретически можно следить за всеми переключениями и отлавливать привязку/отвязку потоков от "наших" задач.


Честно говоря, не очень в курсе всего этого, так как на C# давно не пишу. Но вообще-то мне идея кажется сомнительной, и вот почему. В реальном коде как отличить те потоки, которые нас интересуют, от тех, которые нас не интересуют ?
Кстати, и моя идея насчет DllMain не очень-то годится. Это бы годилось, если бы потоки именно запускались и заканчивались в рамках некоторого workflow. Но скорее всего они и не запускаются, и не заканчиваются, потому что там пул потоков, и этим потокам периодически сбрасывают некий workitem, который они и выполняют, а потом ждут следующего и так до exit процесса. А если еще менеджер этого пула количеством потоков управляет сам по только ему ведомым соображениям, то все еще больше усложняется.

А вообще-то есть Pdh API, который умеет все это считать

https://docs.microsoft.com/en-us/windows/win32/perfctrs/using-the-pdh-functions-to-consume-counter-data

(юзеровский интерфейс тебе должен быть хорошо известен — perfmon.exe)

но с делением потоков на нужные и ненужные тут то же самое.
With best regards
Pavel Dvorkin
Re[9]: Cpu Usage процесса/потока на Windows, Linux и Mac OS
От: Sinclair Россия https://github.com/evilguest/
Дата: 22.11.19 10:26
Оценка: 7 (1) +1
Здравствуйте, Pavel Dvorkin, Вы писали:

PD>Здравствуйте, Sinclair, Вы писали:


S>>Ну вот вроде бы AsyncLocal как раз и отслеживает переключения контекстов в процессе исполнения одного и того же workflow. Т.е. теоретически можно следить за всеми переключениями и отлавливать привязку/отвязку потоков от "наших" задач.


PD>Честно говоря, не очень в курсе всего этого, так как на C# давно не пишу. Но вообще-то мне идея кажется сомнительной, и вот почему. В реальном коде как отличить те потоки, которые нас интересуют, от тех, которые нас не интересуют ?

Тут дело даже не в том, как отличить нужные от ненужных.
1. Вот мы "стартовали таймер", фактически — сделали снэпшот GetThreadTimes().
2. Поработали-поработали, и "уснули", т.е. прервали workflow. В этот момент надо таймер остановить — запомнить, сколько натикало тиков между моментами 1 и 2.
3. Теперь у нас неопределённое время рабочие потоки (и тот исходный в том числе) молотят какие-то другие таски. Или не молотят.
4. Вот у нас началась обработка следующей задачки из нашего workflow — в каком-то другом потоке. Тут надо снова стартовать таймер.
5. Возможно, задачи из workflow параллельно молотятся в нескольких разных потоках — у каждого должен быть записан снэпшот GetThreadTimes(),
6. После окончания каждой задачки, надо прибавить к elapsed дополнительно натикавшее время.
Примерно так я себе это представляю. И опасаюсь, что и AsyncLocal такого не делает — скорее всего, он отслеживает моменты "подключения" потока к задаче, а вот моменты "отключения" — скорее всего нет.

PD>https://docs.microsoft.com/en-us/windows/win32/perfctrs/using-the-pdh-functions-to-consume-counter-data

Хм.
PD>(юзеровский интерфейс тебе должен быть хорошо известен — perfmon.exe)

PD>но с делением потоков на нужные и ненужные тут то же самое.

То-то и оно. Вопрос не в технике подсчёта времени потоков, а в том, как отличить, где тики "нашей" задачки и "ненашей"
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.