Chrome requestAnimationFrame выдает
Связанная тема: requestAnimationFrame Сборка мусора
Я работал над плавной анимацией в виджете, который я создаю для сенсорных устройств, и одним из инструментов, которые я нашел, чтобы помочь мне в этом, был экран Chrome Memory Timeline.
Это немного помогло мне оценить потребление памяти в цикле rAF, но меня беспокоят некоторые аспекты поведения, которые я наблюдаю в Chrome 30 на данный момент.
При первом входе на мою страницу, на которой запущен цикл rAF, я вижу это.
Выглядит хорошо Там не должно быть пилообразная пила, если я выполнил свою работу и исключил распределение объектов во внутреннем цикле. Это поведение согласуется со связанной темой, то есть Chrome имеет встроенную утечку, когда вы используете rAF. (Yikes!)
Становится интереснее, когда я начинаю делать разные вещи на странице.
На самом деле я не делаю ничего другого, просто временно добавляю еще два элемента, которые применяют стили 3D-преобразования CSS3 для нескольких кадров, и затем я прекращаю взаимодействовать с ними.
Что мы видим здесь, так это то, что Chrome сообщает, что каждый раз при срабатывании rAF (16 мс) Animation Frame Fired x 3
,
Это повторение, и скорость, с которой это происходит, монотонно увеличивается до обновления страницы.
Вы уже можете видеть во втором снимке экрана наклон пилообразной формы, значительно увеличившийся после этого начального прыжка с Animation Frame Fired
в Animation Frame Fired x 3
,
Некоторое время спустя он вскочил на x 21
:
Может показаться, что мой код выполняется целую кучу лишних раз, но все лишние многократные запуски - это просто потраченное тепло, отброшенные вычисления.
Пока я снимал третий экран, мой Macbook сильно нагревался. Вскоре после того, как я смог очистить график до конца (около 8 минут), чтобы увидеть, что x
число увеличилось до, окно инспектора стало полностью не отвечающим, и мне предложили, что моя страница перестала отвечать и должна была быть закрыта.
Вот весь код, запущенный на странице:
// ============================================================================
// Copyright (c) 2013 Steven Lu
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// ============================================================================
// This is meant to be a true velocity verlet integrator, which means sending
// in for the force and torque a function (not a value). If the forces provided
// are evaluated at the current time step then I think we are left with plain
// old Euler integration. This is a 3 DOF integrator that is meant for use
// with 2D rigid bodies, but it should be equally useful for modeling 3d point
// dynamics.
// this attempts to minimize memory waste by operating on state in-place.
function vel_verlet_3(state, acc, dt) {
var x = state[0],
y = state[1],
z = state[2],
vx = state[3],
vy = state[4],
vz = state[5],
ax = state[6],
ay = state[7],
az = state[8],
x1 = x + vx * dt + 0.5 * ax * dt * dt,
y1 = y + vy * dt + 0.5 * ay * dt * dt,
z1 = z + vz * dt + 0.5 * az * dt * dt, // eqn 1
a1 = acc(x1, y1, z1),
ax1 = a1[0],
ay1 = a1[1],
az1 = a1[2];
state[0] = x1;
state[1] = y1;
state[2] = z1;
state[3] = vx + 0.5 * (ax + ax1) * dt,
state[4] = vy + 0.5 * (ay + ay1) * dt,
state[5] = vz + 0.5 * (az + az1) * dt; // eqn 2
state[6] = ax1;
state[7] = ay1;
state[8] = az1;
}
// velocity indepedent acc --- shit this is gonna need to change soon
var acc = function(x, y, z) {
return [0,0,0];
};
$("#lock").click(function() {
var values = [Number($('#ax').val()), Number($('#ay').val()), Number($('#az').val())];
acc = function() {
return values;
};
});
// Obtain the sin and cos from an angle.
// Allocate nothing.
function getRotation(angle, cs) {
cs[0] = Math.cos(angle);
cs[1] = Math.sin(angle);
}
// Provide the localpoint as [x,y].
// Allocate nothing.
function global(bodystate, localpoint, returnpoint) {
getRotation(bodystate[2], returnpoint);
// now returnpoint contains cosine+sine of angle.
var px = bodystate[0], py = bodystate[1];
var x = localpoint[0], y = localpoint[1];
// console.log('global():', cs, [px, py], localpoint, 'with', [x,y]);
// [ c -s px ] [x]
// [ s c py ] * [y]
// [1]
var c = returnpoint[0];
var s = returnpoint[1];
returnpoint[0] = c * x - s * y + px;
returnpoint[1] = s * x + c * y + py;
}
function local(bodystate, globalpoint, returnpoint) {
getRotation(bodystate[2], returnpoint);
// now returnpoint contains cosine+sine of angle
var px = bodystate[0], py = bodystate[1];
var x = globalpoint[0], y = globalpoint[1];
// console.log('local():', cs, [px, py], globalpoint, 'with', [x,y]);
// [ c s ] [x - px]
// [ -s c ] * [y - py]
var xx = x - px, yy = y - py;
var c = returnpoint[0], s = returnpoint[1];
returnpoint[0] = c * xx + s * yy;
returnpoint[1] = -s * xx + c * yy;
}
var cumulativeOffset = function(element) {
var top = 0, left = 0;
do {
top += element.offsetTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
} while (element);
return {
top: top,
left: left
};
};
// helper to create/assign position debugger (handles a single point)
// offset here is a boundingclientrect offset and needs window.scrollXY correction
var hasDPOffsetRun = false;
var dpoff = false;
function debugPoint(position, id, color, offset) {
if (offset) {
position[0] += offset.left;
position[1] += offset.top;
}
// if (position[0] >= 0) { console.log('debugPoint:', id, color, position); }
var element = $('#point' + id);
if (!element.length) {
element = $('<div></div>')
.attr('id', 'point' + id)
.css({
pointerEvents: 'none',
position: 'absolute',
backgroundColor: color,
border: '#fff 1px solid',
top: -2,
left: -2,
width: 2,
height: 2,
borderRadius: 300,
boxShadow: '0 0 6px 0 ' + color
});
$('body').append(
$('<div></div>')
.addClass('debugpointcontainer')
.css({
position: 'absolute',
top: 0,
left: 0
})
.append(element)
);
if (!hasDPOffsetRun) {
// determine the offset of the body-appended absolute element. body's margin
// is the primary offender that tends to throw a wrench into our shit.
var dpoffset = $('.debugpointcontainer')[0].getBoundingClientRect();
dpoff = [dpoffset.left + window.scrollX, dpoffset.top + window.scrollY];
hasDPOffsetRun = true;
}
}
if (dpoff) {
position[0] -= dpoff[0];
position[1] -= dpoff[1];
}
// set position
element[0].style.webkitTransform = 'translate3d(' + position[0] + 'px,' + position[1] + 'px,0)';
}
var elements_tracked = [];
/*
var globaleventhandler = function(event) {
var t = event.target;
if (false) { // t is a child of a tracked element...
}
};
// when the library is loaded the global event handler for GRAB is not
// installed. It is lazily installed when GRAB_global is first called, and so
// if you only ever call GRAB then the document does not get any handlers
// attached to it. This will remain unimplemented as it's not clear what the
// semantics for defining behavior are. It's much more straightforward to use
// the direct API
function GRAB_global(element, custom_behavior) {
// this is the entry point that will initialize a grabbable element all state
// for the element will be accessible through its __GRAB__ element through
// the DOM, and the DOM is never accessed (other than through initial
// assignment) by the code.
// event handlers are attached to the document, so use GRAB_direct if your
// webpage relies on preventing event bubbling.
if (elements_tracked.indexOf(element) !== -1) {
console.log('You tried to call GRAB() on an element more than once.',
element, 'existing elements:', elements_tracked);
}
elements_tracked.push(element);
if (elements_tracked.length === 1) { // this is the initial call
document.addEventListener('touchstart', globaleventhandler, true);
document.addEventListener('mousedown', globaleventhandler, true);
}
}
// cleanup function cleans everything up, returning behavior to normal.
// may provide a boolean true argument to indicate that you want the CSS 3D
// transform value to be cleared
function GRAB_global_remove(cleartransform) {
document.removeEventListener('touchstart', globaleventhandler, true);
document.removeEventListener('mousedown', globaleventhandler, true);
}
*/
var mousedownelement = false;
var stop = false;
// there is only one mouse, and the only time when we need to handle release
// of pointer is when the one mouse is let go somewhere far away.
function GRAB(element, onfinish, center_of_mass) {
// This version directly assigns the event handlers to the element
// it is less efficient but more "portable" and self-contained, and also
// potentially more friendly by using a regular event handler rather than
// a capture event handler, so that you can customize the grabbing behavior
// better and also more easily define it per element
var offset = center_of_mass;
var pageOffset = cumulativeOffset(element);
var bcrOffset = element.getBoundingClientRect();
bcrOffset = {
left: bcrOffset.left + window.scrollX,
right: bcrOffset.right + window.scrollX,
top: bcrOffset.top + window.scrollY,
bottom: bcrOffset.bottom + window.scrollY
};
if (!offset) {
offset = [element.offsetWidth / 2, element.offsetHeight / 2];
}
var model = {
state: [0, 0, 0, 0, 0, 0, 0, 0, 0],
offset: offset,
pageoffset: bcrOffset // remember, these values are pre-window.scroll[XY]-corrected
};
element.__GRAB__ = model;
var eventhandlertouchstart = function(event) {
// set
var et0 = event.touches[0];
model.anchor = [0,0];
local(model.state, [et0.pageX - bcrOffset.left - offset[0], et0.pageY - bcrOffset.top - offset[1]], model.anchor);
debugPoint([et0.pageX, et0.pageY], 1, 'red');
event.preventDefault();
requestAnimationFrame(step);
};
var eventhandlermousedown = function(event) {
console.log('todo: reject right clicks');
// console.log('a', document.body.scrollLeft);
// set
// model.anchor = [event.offsetX - offset[0], event.offsetY - offset[1]];
model.anchor = [0,0];
var globalwithoffset = [event.pageX - bcrOffset.left - offset[0], event.pageY - bcrOffset.top - offset[1]];
local(model.state, globalwithoffset, model.anchor);
debugPoint([event.pageX, event.pageY], 1, 'red');
mousedownelement = element;
requestAnimationFrame(step);
};
var eventhandlertouchend = function(event) {
// clear
model.anchor = false;
requestAnimationFrame(step);
};
element.addEventListener('touchstart', eventhandlertouchstart, false);
element.addEventListener('mousedown', eventhandlermousedown, false);
element.addEventListener('touchend', eventhandlertouchend, false);
elements_tracked.push(element);
// assign some favorable properties to grabbable element.
element.style.webkitTouchCallout = 'none';
element.style.webkitUserSelect = 'none';
// TODO: figure out the proper values for these
element.style.MozUserSelect = 'none';
element.style.msUserSelect = 'none';
element.style.MsUserSelect = 'none';
}
document.addEventListener('mouseup', function() {
if (mousedownelement) {
mousedownelement.__GRAB__.anchor = false;
mousedownelement = false;
requestAnimationFrame(step);
}
}, false);
function GRAB_remove(element, cleartransform) {}
// unimpld
function GRAB_remove_all(cleartransform) {}
GRAB($('#content2')[0]);
(function() {
var requestAnimationFrame = window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.requestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
var now = function() { return window.performance ? performance.now() : Date.now(); };
var lasttime = 0;
var abs = Math.abs;
var dt = 0;
var scratch0 = [0,0];
var scratch1 = [0,0]; // memory pool
var step = function(time) {
dt = (time - lasttime) * 0.001;
if (time < 1e12) {
// highres timer
} else {
// ms since unix epoch
if (dt > 1e9) {
dt = 0;
}
}
// console.log('dt: ' + dt);
lasttime = time;
var foundnotstopped = false;
for (var i = 0; i < elements_tracked.length; ++i) {
var e = elements_tracked[i];
var data = e.__GRAB__;
if (data.anchor) {
global(data.state, data.anchor, scratch0);
scratch1[0] = scratch0[0] + data.offset[0];
scratch1[1] = scratch0[1] + data.offset[1];
//console.log("output of global", point);
debugPoint(scratch1,
0, 'blue', data.pageoffset);
} else {
scratch1[0] = -1000;
scratch1[1] = -1000;
debugPoint(scratch1, 0, 'blue');
}
// timestep is dynamic and based on reported time. clamped to 100ms.
if (dt > 0.3) {
//console.log('clamped from ' + dt + ' @' + now());
dt = 0.3;
}
vel_verlet_3(data.state, acc, dt);
e.style.webkitTransform = 'translate3d(' + data.state[0] + 'px,' + data.state[1] + 'px,0)' +
'rotateZ(' + data.state[2] + 'rad)';
}
requestAnimationFrame(step);
};
requestAnimationFrame(step);
Для полноты вот тестовая страница HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<title>symplectic integrator test page</title>
<script src="zepto.js"></script>
<script src="d3.v3.js"></script>
<style type='text/css'>
body {
position: relative;
margin: 80px;
}
#content {
width: 800px;
height: 40px;
display: inline-block;
background: lightgreen;
padding: 20px;
margin: 30px;
border: green dashed 1px;
}
#content2 {
top: 200px;
width: 600px;
height: 200px;
display: inline-block;
background: lightblue;
padding: 20px;
margin: 30px;
border: blue dashed 1px;
}
</style>
</head>
<body>
<div id='scrolling-placeholder' style='background-color: #eee; height: 1000px;'></div>
<label>dt:<input id='dt' type='number' step='0.001' value='0.016666666' /></label>
<label>ax:<input id='ax' type='number' step='0.25' value='0' /></label>
<label>ay:<input id='ay' type='number' step='0.25' value='0' /></label>
<label>t:<input id='az' type='number' step='0.01' value='0' /></label>
<button id='lock'>Set</button>
<button id='zerof' onclick='$("#ax,#ay,#az").val(0);'>Zero forces</button>
<button id='zerov'>Zero velocities</button>
<div>
<span id='content'>content</span>
<span id='content2'>content2</span>
</div>
<div id='debuglog'></div>
<script src="rb2.js"></script>
</body>
</html>
Это должно удовлетворить любые запросы "покажи нам код".
Теперь я бы не стал ставить на это свою жизнь, но я уверен, что по крайней мере хорошо справился с использованием rAF надлежащим образом. Я не злоупотребляю ничем, и к этому моменту я усовершенствовал код, чтобы он был очень легким в распределении памяти Javascript.
Так что, на самом деле, у Chrome нет абсолютно никаких причин брать это и пытаться вывести мой ноутбук на орбиту, как ракета. Нет причин.
В целом Safari, кажется, справляется с этим лучше (в конце концов, он не умирает), а также отмечу, что iOS обычно поддерживает перевод и вращение div 200x600px со скоростью 60 кадров в секунду.
Тем не менее, я признаю, что я не видел, чтобы Chrome действительно так умирал, если только он не записывал временную шкалу памяти.
Я просто почесал голову на этом этапе. Вероятно, это просто какое-то непреднамеренное, непредвиденное взаимодействие с этой конкретной функцией dev-инструмента (насколько я знаю, единственной в своем роде).
Затем я попробовал что-то новое, чтобы хотя бы помочь разобраться в этой проблеме с запуском дополнительной памяти на временной шкале:
Добавлены эти строки.
window.rafbuf = [];
var step = function(time) {
window.rafbuf.push(time);
Это в основном выходит из системы все время, что моя программа rAF (step()
функция) вызывается.
Когда он работает нормально, он записывает время примерно каждые 16,7 мс.
Я получил это:
Это ясно указывает на то, что он снова работает step()
с тем же входным параметром времени, по крайней мере, 22 раза, точно так же, как временная шкала пытается сказать мне
Так что я осмелюсь сказать вам, в Интернете, что это намеренное поведение.:)
2 ответа
Я думаю, что у вас есть проблема, потому что вы звоните requestAnimationFrame(step);
на каждом mousedown
а также mouseup
событие. Так как ваш step()
функция также (как и должно быть) вызывает requestAnimationFrame(step);
по сути, вы начинаете новый "цикл анимации" для каждого mousedown
а также mouseup
событие, и так как вы никогда не останавливаете их, они накапливаются.
Я вижу, что вы также запускаете "цикл анимации" в конце вашего кода. Если вы хотите сразу перерисовать событие мыши, вы должны переместить рисунок из step()
функцию и вызовите это непосредственно из обработчиков событий мыши.
То же самое, как это:
function redraw() {
// drawing logic
}
function onmousedown() {
// ...
redraw()
}
function onmouseup() {
// ...
redraw()
}
function step() {
redraw();
requestAnimationFrame(step);
}
requestAnimationFrame(step);
Я создал анимацию для http://www.testufo.com/ а также средство проверки согласованности requestAnimationFrame() по адресу http://www.testufo.com/animation-time-graph
Список веб-браузеров, которые поддерживают автоматическую синхронизацию requestAnimationFrame() с частотой обновления монитора компьютера (даже если она отличается от 60 Гц), приведен по адресу http://www.testufo.com/browser.html... Это означает, что Монитор с частотой 75 Гц requestAnimationFrame() теперь вызывается 75 раз в секунду в поддерживаемых браузерах, при условии, что веб-страница в настоящее время находится на переднем плане, и производительность процессора / графики это позволяет.
- Определенная версия Chrome 30 имела неприятную ошибку requestAnimationFrame(): http://www.blurbusters.com/blurbusters-helping-google-debug-chrome-30-animation-bugs/
- И похоже, что Chrome 32 Beta создает некоторые ошибки анимации анимации (опять же). https://code.google.com/p/chromium/issues/detail?id=317898
Chrome 29 и 31 работает нормально, как и более новые версии Chrome 30. К счастью, Chrome 33 Canary, кажется, более полно решил проблему, которую я вижу, насколько я знаю. Он запускает анимации гораздо более плавно, без лишних вызовов requestAnimationFrame().
Также я заметил, что управление питанием (замедление / дросселирование процессора также экономит заряд батареи) может нанести ущерб скорости обратного вызова requestAnimationFrame() ... Это проявляется как странные скачки вверх / вниз во время рендеринга кадров ( http: // www. testufo.com/#test=animation-time-graph&measure=rendering)