Любую задачу обычно можно разбить на набор небольших шагов: найти изображение, передвинуть мышь, кликнуть, загрузить ML-модель и т. д. Каждый такой шаг потребляет ресурсы. Если точно измерять, сколько времени уходит на каждую операцию, становится понятно, где стоит подтянуть производительность, а где и так всё в порядке. Этот процесс называется профилированием.
Профилирование даёт точные замеры: сколько раз вызывался метод, сколько в среднем занимал вызов, как распределяются результаты и т. д.
Причём формат вывода может быть любым — вы сами решаете, что именно хотите анализировать. Например, для аим-бота с циклом while можно измерять, сколько времени занимает каждая операция на каждой итерации.
[Script] Performance stats:
DESKTOP-PK81KT3 at 3/22/2025 2:28:50 PM
Test1\ImageFind CV Profiler 0ms
>> Loop #1 3,084.42ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Creation 2,272.84ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Preparations 2.19ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Refresh 744.23ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - OSD 24.12ms
>>>> Mouse Move 22.17ms
>>>> Mouse Click 13.5ms
>> Loop #2 33.77ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Resolution 0.46ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Preparations 0.15ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Refresh 24.69ms
>>>> Mouse Move 0.73ms
>>>> Mouse Click 7.38ms
>> Loop #3 24.11ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Resolution 0ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Preparations 0.1ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Refresh 16.32ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - OSD 0.79ms
>>>> Mouse Mov...ations 0.11ms
>>>> MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Refresh 21.66ms
>>>> Mouse Move 0.58ms
>>>> Mouse Click 10.52ms
А можно выводить агрегированные значения:
[Mouse Click]
Total Iterations: 766
Total Duration: 5672.19 ms
Average Duration: 7.40 ms
Min Duration: 1.33 ms
Max Duration: 28.64 ms
1% Percentile: 4.14 ms
99% Percentile: 14.05 ms
---------------------------
[MLSearchRaw - FindMLKey { ThreadId = 63, ModelPathOrUrl = https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx } - Resolution]
Total Iterations: 769
Total Duration: 2.29 ms
Average Duration: 0.00 ms
Min Duration: 0.00 ms
Max Duration: 0.46 ms
1% Percentile: 0.00 ms
99% Percentile: 0.08 ms
Или даже отображать те же данные в виде таблицы:

Вариантов здесь много.
В EyeAuras используется один из лучших «внутренних» профилировщиков — https://miniprofiler.com/.
Его создала та же команда, что стоит за https://stackoverflow.com/, и он уже много лет проверен в реальных проектах. Подробнее можно почитать здесь (github) или здесь (docs).
Чтобы начать им пользоваться, достаточно подключить namespace using StackExchange.Profiling;. Формально этого уже хватает для доступа ко всему API, но EyeAuras, как обычно, добавляет несколько удобных хелперов. Один из них встроен в Computer Vision API.
Чтобы включить профилирование для CV API, просто вызовите EnableProfiling. После этого начнётся запись таймингов для всех методов этого API — будет замеряться каждая операция.
var cv = GetService<IComputerVisionExperimentalScriptingApi>()
.ForWindow(targetWindow)
.EnableProfiling() // enable profiling
.EnableOsd(fps: 10);
Важно!
EnableProfilingвключает тайминги только для этого конкретного экземпляра CV API и только в рамках текущего скрипта. Если вы хотите профилировать сразу несколько скриптов, используйтеIProfilerProviderвместе сMiniProfiler GetOrAdd(string name)— выберите уникальное имя профайлера и используйте его совместно в нескольких скриптах.
Если вы включили профилирование в CV API, EyeAuras начнёт автоматически записывать внутренние шаги каждого вызова. Но обычно, помимо внутренних этапов, хочется замерять и свои действия: движение мыши, вычисления, собственную логику и т. п.
Делается это очень просто: в CV API используйте Step и оборачивайте измеряемый участок в using, например так:
using(cv.Profiler.Step($"Mouse Move"))
{
sendInput.MouseMoveTo(movePosition);
}
Все тайминги хранятся в памяти и организованы в виде дерева: у каждого измеренного шага может быть несколько вложенных подшагов.
В какой-то момент вы наверняка захотите отрисовать и проанализировать эти данные. Для этого можно использовать:
RenderPlainTextRenderPerStepStatisticsHtml и RenderPerStepStatisticsChatGPT отлично справляется с такими задачами — оба этих метода были написаны им. Он знает MiniProfiler и умеет извлекать из него данные, поэтому вам достаточно просто описать желаемый формат вывода.
Важно! Все результаты профилирования хранятся в памяти. Если ничего с этим не делать, память со временем закончится, поэтому запись обычно ограничивают либо по времени («хранить последние 10 минут»), либо по объёму («хранить до 500 MB»). По умолчанию профайлер, настроенный в CV API, хранит данные за последние 60 minutes. Настройки для этого будут добавлены позже.
using System.Text;
using StackExchange.Profiling;
using Mason.FluentHtml;
Log.Info("Hello, World!");
//this is an example of a script
//which uses image search to find and follow a specific image inside a window
//It uses 2-times probing for optimization - if we know the previous location of the image
//it makes sense to repeat the search in that specific region first
//picking a window
var windowSelector = GetService<IWindowSelector>();
windowSelector.TargetWindow = "Aim Trainer";
var targetWindow = windowSelector.ActiveWindow ?? throw new InvalidOperationException("Window not found");
//this API is used to do Computer Vision stuff (image/text/color/ml search)
var cv = GetService<IComputerVisionExperimentalScriptingApi>()
.ForWindow(targetWindow) //for a specific window
.EnableProfiling()
.EnableOsd(fps: 10); //with enabled on-screen-display refreshed @ 10 fps
//this API is used to generate mouse movements
var sendInput = GetService<ISendInputScriptingApi>();
sendInput.TargetWindow = targetWindow; //for a specific window
//repeat until stopped
MLSearchLoop();
//or
//ImageSearchLoop();
//or
//ColorSearchLoop();
Log.Info($"Performance stats:\n{cv.Profiler.RenderPlainText()}");
Log.Info($"Performance stats(aggregated):\n{RenderPerStepStatistics(cv.Profiler)}");
GetService<IAuraEventLoggingService>()
.LogMessage(new AuraEvent(){
Text = RenderPerStepStatisticsHtml(cv.Profiler),
IsHtml = true,
Loglevel = FluentLogLevel.Info
});
private void MLSearchLoop()
{
//files could be either paths or URLs
var mlModelPath = @"https://s3.eyeauras.net/media/2025/03/AimLab_20240213193604OOBfW2f00U5K.onnx";
var loopIdx = 0;
while (true)
{
loopIdx++;
using var _ = cv.Profiler.Step($"Loop #{loopIdx}");
var result = cv.MLSearchRaw(mlModelPath, new Rectangle(719, 406, 988, 584));
if (!result.Detected.Predictions.Any())
{
//not found
continue;
}
var currentPosition = result.Detected.Predictions.First().Rectangle.Transform(result.ViewportTransforms.WorldToWindow);
Log.Info($"Found via ML @ {currentPosition}");
if (!currentPosition.IsEmpty)
{
//found the image - moving the mouse to the center
var movePosition = currentPosition.ToWinRectangle().Center();
using (cv.Profiler.Step($"Mouse Move"))
{
sendInput.MouseMoveTo(movePosition);
}
using (cv.Profiler.Step($"Mouse Click"))
{
sendInput.MouseLeftClick(0);
}
}
}
}
private void ImageSearchLoop()
{
var targetImagePath = @"C:\\Users\\Xab3r\\Documents\\ShareX\\Screenshots\\2025-03\\uupb4m3RQkfHa8sv.png";
var loopIdx = 0;
while (true)
{
loopIdx++;
using var _ = cv.Profiler.Step($"Loop #{loopIdx}");
var position = cv.ImageSearch(targetImagePath, new Rectangle(714, 397, 999, 598));
if (position.IsEmpty)
{
//not found
continue;
}
Log.Info($"Found via ImageSearch @ {position}");
//found the image - moving the mouse to the center
var movePosition = position.Center();
using (cv.Profiler.Step($"Mouse Move"))
{
sendInput.MouseMoveTo(movePosition);
}
using (cv.Profiler.Step($"Mouse Click"))
{
sendInput.MouseLeftClick(0);
}
}
}
private void ColorSearchLoop()
{
var targetImagePath = @"C:\\Users\\Xab3r\\Documents\\ShareX\\Screenshots\\2025-03\\uupb4m3RQkfHa8sv.png";
var loopIdx = 0;
while (true)
{
loopIdx++;
using var _ = cv.Profiler.Step($"Loop #{loopIdx}");
var position = cv.PixelSearch(Color.FromArgb(255, 87, 34), new Rectangle(714, 397, 999, 598));
if (position.IsEmpty)
{
//not found
continue;
}
Log.Info($"Found via ColorSearch@ {position}");
//found the image - moving the mouse to the center
using (cv.Profiler.Step($"Mouse Move"))
{
sendInput.MouseMoveTo(position);
}
using (cv.Profiler.Step($"Mouse Click"))
{
sendInput.MouseLeftClick(0);
}
}
}
public string RenderPerStepStatistics(MiniProfiler profiler)
{
if (profiler == null || profiler.Root == null)
{
return "No profiling data available.";
}
// Aggregate data across all loops
var timingsByStep = profiler.Root.Children
.SelectMany(loopTiming => loopTiming.Children)
.GroupBy(t => t.Name)
.ToDictionary(
group => group.Key,
group => group
.Select(t => t.DurationMilliseconds)
.Where(duration => duration.HasValue)
.Select(duration => duration.Value)
.ToList()
);
if (!timingsByStep.Any())
{
return "No valid step data found.";
}
var reportBuilder = new StringBuilder();
reportBuilder.AppendLine("---- PER-STEP PROFILING STATS ----");
foreach (var (stepName, durations) in timingsByStep)
{
if (!durations.Any()) continue;
// Statistics calculations
var totalIterations = durations.Count;
var totalDuration = durations.Sum();
var average = durations.Average();
var min = durations.Min();
var max = durations.Max();
// Percentile Calculation
var sortedDurations = durations.OrderBy(x => x).ToList();
var percentile1 = sortedDurations[(int)(totalIterations * 0.01)];
var percentile99 = sortedDurations[(int)(totalIterations * 0.99)];
reportBuilder.AppendLine($@"
[{stepName}]
Total Iterations: {totalIterations}
Total Duration: {totalDuration:F2} ms
Average Duration: {average:F2} ms
Min Duration: {min:F2} ms
Max Duration: {max:F2} ms
1% Percentile: {percentile1:F2} ms
99% Percentile: {percentile99:F2} ms
---------------------------");
}
return reportBuilder.ToString();
}
public string RenderPerStepStatisticsHtml(MiniProfiler profiler)
{
if (profiler == null || profiler.Root == null)
{
return "<p>No profiling data available.</p>";
}
var timingsByStep = profiler.Root.Children
.SelectMany(loopTiming => loopTiming.Children)
.GroupBy(t => t.Name)
.ToDictionary(
group => group.Key,
group => group
.Select(t => t.DurationMilliseconds)
.Where(duration => duration.HasValue)
.Select(duration => duration.Value)
.ToList()
);
if (!timingsByStep.Any())
{
return "<p>No valid step data found.</p>";
}
var htmlBuilder = new HtmlBuilder();
htmlBuilder.Table(new { @class = "table table-striped table-bordered" }, table =>
{
table.Thead(thead =>
{
thead.Tr(tr =>
{
tr.Th(th => th.Content("Step Name"));
tr.Th(th => th.Content("Iterations"));
tr.Th(th => th.Content("Total Duration (ms)"));
tr.Th(th => th.Content("Average (ms)"));
tr.Th(th => th.Content("Min (ms)"));
tr.Th(th => th.Content("Max (ms)"));
tr.Th(th => th.Content("1% Percentile (ms)"));
tr.Th(th => th.Content("99% Percentile (ms)"));
});
});
table.Tbody(tbody =>
{
foreach (var (stepName, durations) in timingsByStep)
{
if (!durations.Any()) continue;
var totalIterations = durations.Count;
var totalDuration = durations.Sum();
var average = durations.Average();
var min = durations.Min();
var max = durations.Max();
var sortedDurations = durations.OrderBy(x => x).ToList();
var percentile1 = sortedDurations[(int)(totalIterations * 0.01)];
var percentile99 = sortedDurations[(int)(totalIterations * 0.99)];
tbody.Tr(tr =>
{
tr.Td(td => td.Content(stepName));
tr.Td(td => td.Content(totalIterations.ToString()));
tr.Td(td => td.Content(totalDuration.ToString("F2")));
tr.Td(td => td.Content(average.ToString("F2")));
tr.Td(td => td.Content(min.ToString("F2")));
tr.Td(td => td.Content(max.ToString("F2")));
tr.Td(td => td.Content(percentile1.ToString("F2")));
tr.Td(td => td.Content(percentile99.ToString("F2")));
});
}
});
});
return htmlBuilder.ToString();
}