reckfellENGINE

Измерить время кадра и проверить оптимизацию

Готовый C# сборщик интервалов: прогрев, среднее, p95, максимум и кадры сверх бюджета.

На этой странице

Снять одинаковый сценарий

  1. В C#-проекте 1.0.0 создайте файл FrameTimingProbe.cs. Прикрепите его к Entity, которая остаётся в дереве на время замера.
  2. Для первого запуска оставьте 60 кадров прогрева, 300 измерений и бюджет 16,67 мс. Соберите проект и запустите уровень через F6.
  3. Во время замера повторяйте выбранное действие. Не меняйте камеру, число объектов, разрешение и ограничения FPS между сравниваемыми прогонами.
  4. Дождитесь единственной строки FRAME n=300 .... Сохраните её вместе с конфигурацией сборки и устройства.
FrameTimingProbe.cs
C#
using System;
using System.Diagnostics;
using Reckfell;

public partial class FrameTimingProbe : Entity
{
    public readonly record struct Summary(int Count, double MeanMs, double P95Ms,
        double MaxMs, int OverBudget, double BudgetMs);

    [Export] public int SampleCount { get; set; } = 300;
    [Export] public int WarmupFrames { get; set; } = 60;
    [Export] public double BudgetMs { get; set; } = 1000.0 / 60;
    public bool Busy { get; private set; }
    public Summary Last { get; private set; }
    public event Action<Summary>? Completed;

    private double[] _samples = [];
    private int _warmup;
    private int _count;
    private long _previous;
    private double _budgetMs;

    public override void _Ready()
    {
        if (Start(SampleCount, WarmupFrames, BudgetMs) != Error.Ok)
            ReckfellApi.PrintErr("FrameTimingProbe: invalid settings");
    }

    public Error Start(int count = 300, int warmup = 60, double budgetMs = 1000.0 / 60)
    {
        if (!ReckfellThread.IsMainThread()) return Error.Unavailable;
        if (!IsInsideTree()) return Error.Unconfigured;
        if (count < 1 || count > 10000 || warmup < 0 || warmup > 10000 ||
            !double.IsFinite(budgetMs) || budgetMs <= 0) return Error.InvalidParameter;
        _samples = new double[count];
        _count = 0;
        _warmup = warmup;
        BudgetMs = budgetMs;
        _budgetMs = budgetMs;
        _previous = Stopwatch.GetTimestamp();
        Busy = true;
        SetProcess(true);
        return Error.Ok;
    }

    public override void _Process(double delta)
    {
        if (!Busy) return;
        long now = Stopwatch.GetTimestamp();
        double ms = Stopwatch.GetElapsedTime(_previous, now).TotalMilliseconds;
        _previous = now;
        if (_warmup > 0) { _warmup--; return; }
        _samples[_count++] = ms;
        if (_count != _samples.Length) return;
        Last = Summarize(_samples, _budgetMs);
        Busy = false;
        SetProcess(false);
        ReckfellApi.Print(FormattableString.Invariant(
            $"FRAME n={Last.Count} mean_ms={Last.MeanMs:F3} p95_ms={Last.P95Ms:F3} max_ms={Last.MaxMs:F3} over={Last.OverBudget}"));
        Completed?.Invoke(Last);
    }

    public static Summary Summarize(double[] milliseconds, double budgetMs)
    {
        ArgumentNullException.ThrowIfNull(milliseconds);
        if (milliseconds.Length == 0 || !double.IsFinite(budgetMs) || budgetMs <= 0)
            throw new ArgumentOutOfRangeException(nameof(budgetMs));
        double[] sorted = (double[])milliseconds.Clone();
        double mean = 0;
        int over = 0;
        for (int i = 0; i < sorted.Length; i++)
        {
            double value = sorted[i];
            if (!double.IsFinite(value) || value < 0)
                throw new ArgumentOutOfRangeException(nameof(milliseconds));
            mean += (value - mean) / (i + 1);
            if (value > budgetMs) over++;
        }
        Array.Sort(sorted);
        int p95 = (int)Math.Ceiling(sorted.Length * 0.95) - 1;
        return new Summary(sorted.Length, mean, sorted[p95], sorted[^1], over, budgetMs);
    }

    public override void _ExitTree()
    {
        Busy = false;
        _samples = [];
    }
}

Скрипт измеряет монотонное реальное время между входами в свой _Process. Пауза ОС, ограничение FPS и ожидание показа кадра могут попасть в этот интервал. Это не отдельное время CPU-функции и не GPU timestamp. delta здесь намеренно не используется как секундомер: это вход игрового шага, на который влияют настройки времени движка.

Пока Entity не обрабатывается из-за паузы или отключённого процесса, измерения не приходят. Пауза между двумя вызовами увеличит следующий интервал. Не включайте такие действия в обычное сравнение, либо измеряйте их отдельным сценарием. Start сбрасывает текущий набор; выход из дерева прекращает сбор без Completed. При повторном входе запустите Start явно.

Прочитать результат

ПолеЗначение
mean_msСреднее время интервала; скрывает отдельные длинные кадры
p95_msЗначение отсортированной выборки с индексом ceil(0,95 × n) − 1
max_msСамый долгий интервал в этом прогоне
overЧисло интервалов строго больше BudgetMs

Для четырёх интервалов 10, 20, 30, 40 мс и бюджета 25 мс среднее равно 25, p95 и максимум — 40, over — 2. Эта проверка встроена в стенд документации. При малой выборке p95 часто совпадает с максимумом; это не ошибка округления и не обещание будущих задержек.

1000 / mean_ms даёт частоту по средней длительности интервала; это не среднее арифметическое мгновенных FPS. Не сравнивайте средний FPS одного прогона с p95 другого как одну метрику. Сохраняйте все четыре показателя.

Проверить одну гипотезу

Снимите исходный вариант A, изменённый B и снова A в одном сценарии после одинакового прогрева. Повторите серию. Меняйте одну причину: например, число поисков цели в кадре. Проверьте одинаковое число игровых результатов. Улучшение длительности бесполезно, если изменённый код пропускает столкновения или работу ИИ.

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

Проверены расчёт статистики, границы входных данных, живой сбор _Process, повторный запуск и главный поток Completed. Headless-прогон стенда проверяет поведение сборщика, а не производительность отрисовки или конкретного устройства. Для локализации причины продолжите измерением CPU-кода.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.