Chrome requestAnimationFrame问题
Chrome requestAnimationFrame问题
相关主题:requestAnimationFrame垃圾回收\n我一直在为我正在为触摸设备构建的小部件中的平滑动画而努力,我发现的其中一个工具是Chrome的内存时间轴屏幕。\n这帮助我在rAF循环中评估内存消耗,但是我对我在Chrome 30中观察到的行为的一些方面感到困扰。\n当我最初进入我的页面,其中包含rAF循环运行,我看到了这个。\n\n看起来还好。如果我尽到责任,消除了内部循环中的对象分配,就不应该有锯齿状。这种行为与链接的主题一致,也就是说,每当使用rAF时,Chrome都有内置泄漏。(天哪!)\n当我开始在页面上做各种事情时,情况变得更有趣。\n\n我并没有做任何不同的事情,只是临时添加了两个元素,为它们应用了CSS3 3D变换样式,持续几帧,然后我停止与它们交互。\n在这里,Chrome报告说每次rAF触发(16ms)都会导致Animation Frame Fired x 3
。\n这种重复以及它的速率单调增加,直到页面刷新。\n在第二个截图中,你已经可以看到锯齿状斜率在从Animation Frame Fired
到Animation Frame Fired x 3
的初始跳跃后有了显著增加。\n不久之后,它跳到了x 21
:\n\n看起来我的代码运行了很多次,但所有额外的多次运行只是浪费的计算。当我拍摄第三个截图时,我的Macbook变得非常热。不久之后,在我能够滚动时间轴到最后一点(约8分钟)查看x
数字增加到多少之前,检查器窗口变得完全无响应,我被提示页面已不响应并且必须终止。\n这是页面上运行的全部代码:\n
// ============================================================================ // 版权所有(c)2013 Steven Lu // 根据以下条件,免费授予任何获得 // 本软件及相关文档文件(“软件”)的人, // 无限制地处理本软件,包括无限制地使用,复制,修改,合并,发布,分发,再许可, // 并出售本软件和允许其使用的人, // 前提是上述版权声明和本许可声明包含在 // 软件的所有副本或实质部分中。 // 本软件按“原样”提供,无论是明示的还是 // 暗示的,包括但不限于对适销性的保证, // 对特定目的和非侵权的任何保证。在任何情况下,作者或版权持有人对任何索赔,损害或其他责任概不负责 // 由合同,侵权行为或其他行为引起的,无论是在使用或其他方面 // 本软件或与之相关的使用或其他交易。 // ============================================================================ // 这是一个真正的速度Verlet积分器,这意味着发送 // 对于力和力矩,使用函数(而不是值)。如果提供的力 // 在当前时间步骤评估,则我认为我们只剩下了纯粹的欧拉积分。这是一个3 DOF积分器,用于2D刚体,但用于建模3D点 // 动态可能同样有用。 // 这个尝试通过就地操作状态来最小化内存浪费。 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; } // 速度独立加速度 --- 哎呀,这很快就要改变了 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; }; }); // 从角度获取sin和cos。 // 无需分配任何内容。 function getRotation(angle, cs) { cs[0] = Math.cos(angle); cs[1] = Math.sin(angle); } // 提供本地点[x,y]。 // 无需分配任何内容。 function global(bodystate, localpoint, returnpoint) { getRotation(bodystate[2], returnpoint); // 现在returnpoint包含角度的cosine+sine。 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); // 现在returnpoint包含角度的cosine+sine 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 }; }; // 帮助程序创建/分配位置调试器(处理单个点) // 此处的偏移量是boundingclientrect偏移量,并且需要window.scrollXY修正 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 = $('') .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( $('') .addClass('debugpointcontainer') .css({ position: 'absolute', top: 0, left: 0 }) .append(element) ); if (!hasDPOffsetRun) { // 确定附加到body的绝对元素的偏移量。 body的边距 // 是主要破坏者,倾向于把扳手扔到我们的东西里。 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]; } // 设置位置 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... } }; // 当加载库时,全局事件处理程序用于GRAB没有 // 安装。它是惰性安装的,当首次调用GRAB_global时,所以 // 如果您只调用GRAB,文档不会有任何处理程序 // 附加到它。这将保持未实现,因为不清楚定义行为的语义是什么。使用直接API更直接 // 更容易定义行为。 function GRAB_global(element, custom_behavior) { // 这是一个入口点,将初始化一个可抓取元素,所有的状态 // 通过其__GRAB__元素通过DOM访问,并且代码从未访问过DOM(除了通过初始分配之外)。 // 事件处理程序附加到文档,因此如果您的 // 网页依赖于阻止事件冒泡。 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);
\n为了完整起见,这里是测试页面的HTML:\n
symplectic integrator test page content content2
\n这应该满足任何要求“展示代码”的要求。\n现在我不会拿我的生命来打赌,但我相信我在使用rAF的正确方式上做得还不错。我没有滥用任何东西,而且到目前为止,我已经优化了代码,使其在Javascript内存分配方面非常轻量级。\n所以,真的,没有任何理由让Chrome像火箭一样将我的笔记本电脑带到太空中。没有理由。\nSafari似乎处理得更好(它不会最终死机),而且我还注意到iOS通常能够以60fps维持一个200x600像素的div进行平移和旋转。\n然而,我承认,除非我将其记录在内存时间轴上,否则我没有看到Chrome真正死机。\n我只是有点摸不着头脑。这可能只是与内存时间轴额外回调触发的这个特定开发工具功能的一些意外、未预见的交互作用。\n然后我尝试了一些新的方法,至少帮助调查这个内存时间轴额外回调触发的问题:\n添加了以下几行。\n
window.rafbuf = []; var step = function(time) { window.rafbuf.push(time);
\n这基本上记录了我的rAF例程(step()
函数)被调用的所有时间。\n当它正常运行时,大约每16.7毫秒记录一次时间。\n我得到了这个:\n\n这明显表明它至少重新运行了step()
22次,就像时间轴试图告诉我的那样。\n所以,互联网,我敢你告诉我这是预期的行为。 🙂
Chrome的requestAnimationFrame问题
在http://www.testufo.com上,我创建了动画,并在http://www.testufo.com/animation-time-graph上创建了一个requestAnimationFrame()的一致性检查器。
支持将requestAnimationFrame()自动同步到计算机显示器刷新率的网络浏览器列表(即使不是60Hz),在http://www.testufo.com/browser.html上列出... 这意味着在支持的浏览器上,如果网页当前在前台,并且CPU/图形性能允许,requestAnimationFrame()现在每秒调用75次(对于75Hz显示器)。
- Chrome 30的某个版本存在一个严重的requestAnimationFrame() bug:http://www.blurbusters.com/blurbusters-helping-google-debug-chrome-30-animation-bugs/
- 而Chrome 32 Beta似乎又出现了一些动画流畅性的bug:https://code.google.com/p/chromium/issues/detail?id=317898
Chrome 29和31运行正常,新版本的Chrome 30也一样。幸运的是,根据我所知,chrome 33 Canary似乎已经更完全地解决了我所看到的问题。它运行动画更加流畅,不会不必要地调用requestAnimationFrame()。
另外,我注意到电源管理(为了节省电池电量而进行的CPU减速/节能)可能会对requestAnimationFrame()的回调频率造成影响... 这表现为帧渲染时间的奇怪的上升/下降(http://www.testufo.com/#test=animation-time-graph&measure=rendering)
我看不出这与运行rAF回调的行为完全不正确有何关联。无论如何,在rAF与setTimeout一样健壮之前,似乎还有很多工作要做。
Chrome requestAnimationFrame issues
在Chrome中,使用requestAnimationFrame时可能会遇到一些问题。这些问题的出现原因是因为在每个mousedown和mouseup事件上调用requestAnimationFrame(step)。由于step()函数也会调用requestAnimationFrame(step),所以实际上会为每个mousedown和mouseup事件启动新的“动画循环”,并且由于从未停止它们,它们会累积。
我注意到您还在代码的末尾启动了“动画循环”。如果您想在鼠标事件上立即重绘,您应该将绘制操作移出step()函数,并直接从鼠标事件处理程序中调用它。
类似这样的代码:
function redraw() {
// 绘制逻辑
}
function onmousedown() {
// ...
redraw()
}
function onmouseup() {
// ...
redraw()
}
function step() {
redraw();
requestAnimationFrame(step);
}
requestAnimationFrame(step);
是的,您的伪代码示例非常好地概括了正确管理rAF渲染循环的方式。通过鼠标点击启动额外的渲染“线程”是错误的,会导致难以解决的情况...我需要重新检查这段代码,并找出它是否是我遇到的问题的重要因素。