Почему я получаю 30 кадров в секунду на панели производительности инструмента chrome dev, а JS и React говорят, что это 60? - PullRequest
1 голос
/ 29 февраля 2020

Я пытаюсь использовать реакцию для разработки классического c учебного пособия по игре как сделать простую - html5 -canvas-game .

Все все шло хорошо, пока я не обнаружил, что мое движение немного сбито с толку, онлайн тестовая ссылка и код .

enter image description here

В то время как оригинальная игра , написанная в JS, намного плавнее:

enter image description here

Так что я немного покопался в этом и обнаружил, что фактический fps отличается:

Реакция:

enter image description here

Pure JS: enter image description here

Странно, что после добавления некоторого кода в cal c fps я получаю "60 fps" в обеих реакциях hook and useEffect:


// log interval in useEffect
  useEffect(() => {
    console.log('interval', Date.now() - renderTime.current);
    renderTime.current = Date.now();
  });

// calc fps in hook directly
fps: rangeShrink(Math.round(1000 / (Date.now() - time.current)), 0, 60),

// render
         <Text 
          x={width - 120} 
          y={borderWidth} 
          text={`FPS: ${fps}`}
          fill="white"
          fontSize={24}
          align="right"
          fontFamily="Helvetica"
        />

enter image description here

Определение местоположения проблемы

Я добавил контрастный холст, который отображается каждый раз, когда heroPos обновляется. Это дает мне 60FPS в инструменте разработчика chrome. Теперь проблема определенно вызвана библиотекой холста, которую я использую: response-konva.

  const canvasRef = useRef(null);

  useEffect(() => {
    const ctx = canvasRef.current.getContext('2d');
    if (backgroundStatus === 'loaded') {
      ctx.drawImage(backgroundImage, 0, 0);
    }
    if (heroStatus === 'loaded') {
      ctx.drawImage(heroImage, heroPos.x, heroPos.y);
    }
  }, [backgroundStatus, heroStatus, heroPos]);

Обнаружена проблема

Я обнаружил проблему, она вызвана batchDraw react-konva используется:

После изменения этой строки я могу получить движение 60 кадров в секунду.

-  drawingNode && drawingNode.batchDraw();
+  drawingNode && drawingNode.draw();

В соответствии с их do c, batchDraw будет рисовать в the next animationFrame. Но react сам также использует RAF для запуска следующего обновления реквизита, поэтому batchDraw здесь происходит 2 frames после I setHeroPos().

Решение:

I'm собираюсь отправить pull-запрос в их проект.

1 Ответ

3 голосов
/ 01 марта 2020

Инструменты разработчика могут добавить много дополнительной нагрузки на устройство. Тем более, когда вы записываете журнал производительности.

Реакция - это последнее, что я бы использовал для приложения реального времени, поскольку оно добавляет закулисное пространство JS даже к самым простым задачам.

Расчет производительности путем измерения времени между кадрами не дает точного указания производительности.

Производительность

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

Например, чтобы получить время основной функции l oop в игре

function mainLoop(frameTime) {
    const now = performance.now(); // MUST BE FIRST LINE OF CODE TO TEST!!!!

    requestAnimationFrame(mainLoop);
    const executeTime = performance.now() - now; // MUST BE LAST LINE OF CODE TO TEST!!!
}

Это даст вам время для выполнения в миллисекундах. Поскольку JS блокирует, измеряется только код внутри двух строк.

  • Примечание НЕТ дополнительных накладных расходов, таких как G C, Компоновка, синхронная загрузка и т. Д. c ...

  • Примечание миллисекунды (1 / 1,000,000th)

  • ПРИМЕЧАНИЕ точность этого значения performance.now было намеренно уменьшено для защиты пользователей и составляет от 100 мс до 200 мс в зависимости от браузера (доступ к 1 мс возможен за флагами и конфигурацией системы))

Значимая производительность

JS выполнение не является детерминированным c, что делает единственную временную меру абсолютно ненадежной. (Причина, почему лучше использовать performance.now, чем peformance.mark)

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

Вместо того, чтобы показывать время, используйте metri c, который относится к потребностям приложения. Например, сколько кадра тратится на выполнение кода. (см. пример)

Пример

В этом примере анимируется некоторое содержимое холста с использованием requestAnimationFrame.

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

Текст Info в верхней части отображает результаты синхронизации как среднее значение.

Вы заметите, что идеализированная нагрузка кадра (IFL) намного ниже 100%, прежде чем частота кадров упадет.

Эксперименты

  1. Как Dev Tools и мониторинг производительности влияют на производительность.

Переместите ползунок чуть ниже, когда частота кадров падает ниже 60.

Откройте инструменты разработчика, чтобы увидеть, может ли и как это повлиять на кажущуюся производительность. Принять к сведению любые изменения. Есть ли эффект, и если да, то сколько?

Запишите журнал производительности и посмотрите, влияют ли на FPS и / или IFL запись

Каково максимальное время, которое ваше устройство может выделить для рендеринга до того, как будет изменена частота кадров.

Медленно перемещайте ползунок вправо.

Когда частота кадров падает ниже 60, сдвиньте слайд на шаг назад, пока он снова не покажет 60FPS.

Значения IFL дадут% идеального кадра (60-й во-вторых) выполнение кода. Время Абсолютное время выполнения в мс.

Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randItem = arr => arr[Math.random() * arr.length | 0];
CPULoad.addEventListener("input",() => loadTimeMS = Number(CPULoad.value));
var loadTimeMS = Number(CPULoad.value);
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);

function mainLoop(frameTime) {

    /* Timed section starts on next line */
    const now = performance.now();
    
    CPU_Load(loadTimeMS);
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    requestAnimationFrame(mainLoop);
    
    const exeTime = performance.now() - now;
    /* Timed section ends at above line*/

    measure(info, frameTime, exeTime);
    
}

const measure = (() => {
    const MEAN = (t, f) => t += f;
    const fTimes = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], bTimes = [...fTimes];
    var pos = 0, prevTime, busyFraction;
    return (el, time, busy) => {
        if (prevTime) {
            bTimes[pos % bTimes.length] = busy;		
            fTimes[(pos ++) % fTimes.length] = time - prevTime;		
            const meanBusy = bTimes.reduce(MEAN, 0) / bTimes.length;
            const meanFPS = fTimes.reduce(MEAN, 0) / fTimes.length;
            el.textContent = "Load: " + loadTimeMS.toFixed(1) + "ms " +
                " FPS: " + Math.round(1000 / meanFPS) + 
                " IFL: " + (meanBusy / (1000 / 60) * 100).toFixed(1) + "%" +
                " Time: " + meanBusy.toFixed(3) + "ms";
            busyFraction = meanBusy / (1000/60);
        }
        prevTime = time;
    };
})();

const colors = "#F00,#FF0,#0F0,#0FF,#00F,#F0F,#000,#FFF".split(",");
// This function shares the load between CPU and GPU reducing CPU
// heating and preventing clock speed throttling on slower systems.
function CPU_Load(ms) { // ms = microsecond and is a min value only
   const now = performance.now();
   ctx.globalAlpha = 0.1;
   do {
      ctx.fillStyle = Math.randItem(colors);
      ctx.fillRect(Math.rand(-50,250), Math.rand(-50, 100), Math.rand(1, 200), Math.rand(1,100))
   } while(performance.now()-now <= ms);
   ctx.globalAlpha = 1;
}
body {
  font-family: arial;
}
#info {
    position: absolute;
    top: 10px;
    left: 10px;
    background: white;
    font-size:small;
    width:345px;
    padding-left: 3px;
}
#canvas {
    background: #8AF;
    border: 1px solid black;
}
#CPULoad {
    font-family: arial;
    position: absolute;
    top: 130px;
    left: 10px;
    color: black;
    width: 340px !important;
}
<code id="info"></code>
<input id="CPULoad" min="0" max="36" step="0.5" value="2"  type="range" list="marks"/>
<canvas id="canvas" width="350"></canvas>
<datalist id="marks">
  <option value="0"></option>
  <option value="4"></option>
  <option value="8"></option>
  <option value="12"></option>
  <option value="16"></option>
  <option value="20"></option>
  <option value="24"></option>
  <option value="28"></option>
  <option value="32"></option>
  <option value="36"></option>
</datalist>

ПРИМЕЧАНИЕ Отображение времени влияет на результаты. Тот факт, что этот код выполняется в песочнице, повлияет на результаты Для получения наиболее точных результатов запустите код на независимой странице. Записать результаты в JS структуру данных и отобразить результаты после тестового прогона.

  • Load : Запрошенная загрузка процессора / графического процессора за 1/1000-ую секунду.

  • FPS : среднее значение кадров в секунду.

  • IFL : идеальная нагрузка на раму, процент от 60-й код выполнения.

  • Время : среднее измеренное время выполнения в 1/1000-й секунде.

...