QML

Тестирование и отладка анимаций в Qt Quick Profiler

Введение

В заключительной статье цикла и всего расширенного плана статей разберём инструмент, упомянутый в нескольких предыдущих статьях — QML Profiler (часть Qt Creator), специально предназначенный для диагностики проблем производительности именно QML/Qt Quick приложений, включая детальный анализ того, какие конкретно анимации, привязки свойств и операции перекомпоновки сцены потребляют время каждого кадра.

Концепция

QML Profiler записывает временную шкалу событий выполнения QML-приложения — время, затраченное на JavaScript-обработчики, вычисление привязок свойств (binding evaluation), компоновку (layout), и непосредственно рендеринг кадров сцены — и представляет эти данные в виде визуального таймлайна, на котором можно увидеть конкретные кадры с превышенным бюджетом времени и углубиться в то, что именно происходило в этот момент, вплоть до конкретной строки QML-кода, ответственной за дорогую операцию.

Пример кода: Тестирование и отладка

# Запуск приложения с включённым профилированием QML
# (типичный способ через Qt Creator: Analyze → QML Profiler, либо вручную)

QML_DEBUG=1 QSG_RENDER_LOOP=basic ./MyQuickApp -qmljsdebugger=port:3768
// main.cpp — программное включение поддержки подключения профайлера
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlDebuggingEnabler> // только для отладочных сборок

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.loadFromModule("MyQuickApp", "Main");

    return app.exec();
}

// Сборка с CONFIG += qml_debug в .pro-файле или аналогичной настройкой CMake
// необходима для возможности подключения QML Profiler к запущенному приложению
// Пример QML-кода с потенциальной проблемой производительности,
// которую QML Profiler помогает обнаружить и локализовать
Item {
    // Дорогая привязка свойства, пересчитываемая при каждом изменении любого
    // из используемых в выражении свойств — если таких пересчётов очень много
    // в секунду, профайлер покажет значительное накопленное время в Binding Evaluation
    property real expensiveComputedValue: {
        var sum = 0;
        for (var i = 0; i < 10000; i++) {
            sum += Math.sin(i) * mouseArea.mouseX; // пересчитывается при каждом движении мыши!
        }
        return sum;
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        hoverEnabled: true
    }
}
// ИСПРАВЛЕНО — дорогое вычисление либо кешируется, либо троттлится,
// не пересчитываясь при абсолютно каждом, очень частом изменении mouseX
Item {
    property real expensiveComputedValue: 0

    Timer {
        interval: 100 // пересчёт максимум 10 раз в секунду, а не при каждом микро-движении мыши
        repeat: true
        running: true
        onTriggered: {
            var sum = 0;
            for (var i = 0; i < 10000; i++) {
                sum += Math.sin(i) * mouseArea.mouseX;
            }
            expensiveComputedValue = sum;
        }
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        hoverEnabled: true
    }
}

Пояснения к коду

Запуск приложения с -qmljsdebugger=port:3768 (или эквивалентная настройка через Qt Creator) открывает порт, к которому подключается QML Profiler для записи событий времени выполнения — это требует, чтобы сама сборка приложения была собрана с включённой поддержкой отладки QML (CONFIG += qml_debug), что обычно не делается для финальных релизных сборок по соображениям как производительности, так и безопасности (открытый порт отладки не должен присутствовать в продакшен-версии приложения).

«Проблемный» пример показывает классическую находку QML Profiler — привязка свойства (property real expensiveComputedValue: { ... }), зависящая от mouseArea.mouseX, пересчитывается при каждом, даже самом микроскопическом изменении позиции мыши, выполняя дорогой цикл из 10000 итераций при каждом таком пересчёте — в профайлере это проявилось бы как значительный, многократно повторяющийся блок времени в категории «Binding Evaluation». Исправленная версия показывает типичное решение — троттлинг через Timer, ограничивающий частоту реального пересчёта дорогого значения до разумного предела (10 раз в секунду), независимо от того, насколько часто реально меняется исходное значение mouseX.

Подводные камни

  • QML Profiler требует сборки с включённой отладочной поддержкой QML, которая имеет собственные, хотя обычно умеренные накладные расходы — измерения, сделанные с включённым профилированием, могут не точно соответствовать производительности финальной, полностью оптимизированной релизной сборки без отладочной поддержки; для финальной проверки реальной производительности стоит также тестировать релизную сборку без профилирования, используя более простые средства измерения (как ручной счётчик FPS из статьи 274).
  • Интерпретация данных профайлера требует понимания различия между категориями событий («JavaScript», «Binding Evaluation», «Layout», «Painting») — попытка оптимизировать неправильную категорию (например, тратить время на оптимизацию JavaScript-кода, когда реальная проблема в категории Layout из-за сложной, глубоко вложенной структуры компонентов) не даст ожидаемого улучшения производительности, и важно сначала точно определить, какая категория событий реально доминирует по затраченному времени для конкретного наблюдаемого случая просадки FPS.
  • Профилирование на короткой, нерепрезентативной сессии использования приложения (например, всего несколько секунд статичного интерфейса без реального взаимодействия) может не отразить реальные проблемы производительности, проявляющиеся только при специфичных пользовательских сценариях (быстрая прокрутка длинного списка, одновременное выполнение нескольких сложных анимаций) — для содержательного профилирования сессия должна включать репрезентативный набор реальных пользовательских действий, провоцирующих потенциально проблемные участки кода.
  • Слишком частое срабатывание привязок свойств — не единственная возможная причина просадки FPS, и попытка оптимизировать абсолютно все привязки без разбора, основываясь только на их частоте срабатывания, без учёта реальной стоимости каждого отдельного вычисления, может привести к избыточному, не оправданному усложнению кода (через троттлинг, кеширование) там, где конкретная привязка достаточно лёгкая и не создаёт измеримой проблемы — оптимизация должна следовать за реальными данными профайлера о суммарном затраченном времени, а не за одним лишь количеством срабатываний привязки.