Problems with ChromeAnimationFrame

Related topic: requestAnimationFrame garbage collection

I'm working on smooth animations in widgets that I create for touch devices, and one of the tools I found to help me with this was the Chrome timeline history screen.

This helped me a little to estimate the memory consumption in the rAF cycle, but I am worried about some aspects of the behavior that I observe in Chrome 30 at this point.

When I first enter my page in which the rAF loop works, I see this. enter image description here

Looks good. There should not be a sawtooth if I did my job and eliminated the selection of objects in my inner loop. This behavior is compatible with the related theme, i.e. Chrome has a built-in leak whenever you use rAF. (Yikes!)

It becomes more interesting when I start doing different things on the page.

enter image description here

I don't do anything else, just temporarily adding two more elements that get the 3D-3D CSS3 styles that apply to multiple frames, and then I stop interacting with them.

What we see here is a Chrome message that suddenly every RAF start (16 ms) leads to Animation Frame Fired x 3 .

This is repeated, and the speed with which he does this, monotonously increases to refresh the page.

You can already see in the second screencap when the pilot ramp sharply increased after this initial jump from Animation Frame Fired to Animation Frame Fired x 3 .

In a short time he moved to x 21 :

enter image description here

It would seem that my code runs a whole bunch of extra times, but all the extra few starts are just lost heat, discarded computation.

While I was shooting the third screencap, my Macbook was very hot. Soon after, before I could clear the graph to the end (about 8 minutes) to see that the number x increased, the inspector window did not respond completely, and I was asked that my page stop responding and had to stop.

Here is all the code running on the page:

 // ============================================================================ // 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] // [ sc 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]); // [ cs ] [x - px] // [ -sc ] * [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 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 not clear what the // semantics for defining behavior are. It 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); 

For completeness, here is the HTML test page:

 <!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> 

This should satisfy any "show us code" requests.

Now I would not put my life on this, but I am quite sure that I have done at least a good job of using rAF correctly. I am not abusing anything, and by this moment I have refined the code to be very easy in allocating Javascript memory.

So, really, there’s absolutely no reason for Chrome to take this and try to put my laptop into orbit like a rocket. There is no reason.

Safari as a whole seems to handle this better (it doesn't end up dying in the end), and I should also note that iOS usually supports 200x600px divs, translating and rotating at 60 frames per second.

However, I admit that I did not see Chrome really die as if I were not recording the memory timeline.

I'm just scratching my head now. This is probably just an unintended unintended interaction with this feature of the Dev tool (the only thing I know).

So, I tried something new to at least help investigate this problem with alternating memory time intervals:

Added these lines.

 window.rafbuf = []; var step = function(time) { window.rafbuf.push(time); 

This is basically logged all the time when my rAF procedures are called ( step() function).

When it works fine, it records the time approximately every 16.7 ms.

I got it:

enter image description here

This clearly indicates the repeated execution of step() with the same time input parameter at least 22 times, just like the time line trying to tell me.

So, I dare to tell you the Internet, tell me that this is the intended behavior. :)

+18
javascript html google-chrome requestanimationframe
Oct 16 '13 at 4:59 on
source share
2 answers

I think you have a problem because you are calling requestAnimationFrame(step); for each mousedown and mouseup . Since your step() function also (as it should) calls requestAnimationFrame(step); , you actually start a new "animation loop" for each mousedown and mouseup , and since you never stop them, they accumulate.

I see that you also begin the "animation loop" at the end of your code. If you want to redraw immediately on a mouse event, you must move the drawing from the step() function and call it directly from the mouse event handlers.

Something like that:

 function redraw() { // drawing logic } function onmousedown() { // ... redraw() } function onmouseup() { // ... redraw() } function step() { redraw(); requestAnimationFrame(step); } requestAnimationFrame(step); 
+3
Dec 09 '14 at 21:18
source share

I created an animation for http://www.testufo.com , as well as checking the sequence of requestAnimationFrame () at http://www.testufo.com/animation-time-graph

The list of web browsers that support automatic synchronization of requestAnimationFrame () with a refresh rate of the computer monitor (even if it differs from 60 Hz) is listed in http://www.testufo.com/browser.html ... This means that the monitor with a frequency of 75 Hz, requestAnimationFrame () is now called 75 times per second in supported browsers if the web page is currently in the foreground, and CPU / graphics performance allows this.

Chrome 29 and 31 work just fine, as do the new versions of Chrome 30. Fortunately, Chrome 33 Canary seems to have more accurately fixed the problem that I see, as far as I know. It runs animations much more smoothly, without extra calls to requestAnimationFrame ().

I also noticed that power management (slowing down / throttling the processor also saves battery power) can be detrimental to the requestAnimationFrame () callback speed ... It manifests itself as strange up / down bursts in the Frame Rendering Times ( http: //www.testufo .com / # test = animation-time-graph & measure = rendering )

+2
Nov 13 '13 at 2:01
source share



All Articles