2013-10-16 3 views
18

Verwandte Thema: requestAnimationFrame garbage collectionChrome request gibt

Ich habe zu glatten Animationen in einem Widget arbeiten, die ich für Touch-Geräte bauen, und eines der Werkzeuge, die ich gefunden, mir zu helfen mit diesem die Chrome gewesen Speicher-Timeline-Bildschirm.

Es hat mir ein bisschen geholfen, meinen Speicherverbrauch im RAF-Loop zu bewerten, aber ich störe mich an einigen Aspekten des Verhaltens, das ich in Chrome 30 an dieser Stelle beobachte.

Wenn ich zuerst meine Seite betrete, auf der die rAF-Schleife läuft, sehe ich das. enter image description here

Sieht gut aus. Es sollte keinen Sägezahn geben, wenn ich meine Aufgabe erledigt und Objektzuweisungen in meiner inneren Schleife beseitigt habe. Dieses Verhalten stimmt mit dem verknüpften Thema überein, das heißt, dass Chrome ein integriertes Leck hat, wenn Sie rAF verwenden. (yikes!)

Es wird interessanter, wenn ich anfange, verschiedene Dinge auf der Seite zu tun.

enter image description here

Ich bin wirklich nichts anderes zu tun, nur vorübergehend zwei weitere Elemente hinzuzufügen, die erhalten CSS3 3D-Styles für ein paar Frames angewendet verwandeln und dann mit ihnen, dass ich aufhören zu interagieren.

Was wir hier sehen, ist Chrome berichtet, dass plötzlich jede rAF Zündung (16ms) Animation Frame Fired x 3 ergibt.

Diese Wiederholung und die Geschwindigkeit, mit der dies geschieht, werden monoton bis zur Seitenaktualisierung erhöht.

Sie können bereits in der zweiten screencap sehen, dass die Sägezahnneigung nach dem anfänglichen Sprung von Animation Frame Fired zu Animation Frame Fired x 3 dramatisch zugenommen hat.

kurze Zeit später hat es x 21 sprang:

enter image description here

Es scheint, dass mein Code eine ganze Reihe von zusätzlichen Zeiten ausgeführt wird, aber alle zusätzlichen mehrere Läufe wird nur Wärme verschwendet verworfene Berechnung.

Während ich die dritte screencap nahm, heizte sich mein Macbook ziemlich stark auf. Kurz danach, bevor ich in der Lage war, die Zeitleiste auf das Ende-Bit (etwa 8 Minuten) zu scrubben, um zu sehen, was die x-Nummer erreicht hatte, reagierte das Inspektorfenster vollständig, und ich wurde aufgefordert, dass meine Seite nicht mehr reagierte und es musste beendet werden.

Hier ist die Gesamtheit der Code auf der Seite ausgeführt wird:

// ============================================================================ 
// 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); 

Denn hier Vollständigkeit ist die Testseite 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> 

Das sollte keine „zeigen uns den Code“ Wünsche erfüllen .

Jetzt würde ich mein Leben nicht darauf wetten, aber ich bin mir ziemlich sicher, dass ich zumindest einen ordentlichen Job gemacht habe, rAF richtig zu benutzen. Ich missbrauche nichts, und ich habe zu diesem Zeitpunkt den Code verfeinert, um sehr leicht auf Javascript Speicherzuweisung zu sein.

Also, wirklich, es gibt absolut kein Grund für Chrome, dies zu nehmen und versuchen, meinen Laptop in den Orbit wie eine Rakete zu fahren. Kein Grund.

Safari im Allgemeinen scheint es besser zu handhaben (es stirbt schließlich nicht), und auch ich werde feststellen, dass iOS im Allgemeinen in der Lage ist, eine 200x600px div übersetzen und rotieren mit 60fps.

Ich gebe jedoch zu, dass ich Chrome nicht wirklich so sterben gesehen habe, es sei denn, ich habe es die Speicherzeitachse aufzeichnen.

Ich kratze mich gerade an diesem Punkt. Es ist wahrscheinlich nur eine unbeabsichtigte, unvorhergesehene Interaktion mit dieser speziellen Dev-Tool-Funktion (die einzige ihrer Art, meines Wissens).

So dann habe ich versucht, etwas Neues zumindest zu helfen, dieses Problem mit der Extra-Callback-firing Speicherzeitleiste zu untersuchen:

hinzugefügt diese Zeilen.

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

Diese im Grunde abmeldet alle Zeiten, die meine rAF Routine (die step() Funktion) aufgerufen wird.

Wenn es normal läuft, schreibt es ungefähr alle 16,7 ms eine Zeit auf.

Ich habe diese:

enter image description here

, dass es deutlich zeigt step() mit der gleichen Zeit Eingangsparameter mindestens 22 mal, genau wie die Timeline versucht, mich erneut laufen zu erzählen.

Also ich wage es dir, Internet, mir zu sagen, dass dies das beabsichtigte Verhalten ist. :)

+3

+ 1, aber ich fühle mich wäre es mehr Interesse von Menschen Garner, wenn die Länge der Frage ein wenig kürzer –

+0

@Onaseriousnote ist ich Vorschläge nehme –

+0

Das von Interesse sein könnten : https://groups.google.com/forum/#!topic/google-chrome-developer-tools/S_mKrF42a4Y – jedierikb

Antwort

2

habe ich die Animationen für http://www.testufo.com und auch eine request() Konsistenzprüfung bei http://www.testufo.com/animation-time-graph

Die Liste des Web-Browser, die automatische Synchronisation von request() auf den Computer-Monitor der Bildwiederholfrequenz unterstützen (auch wenn andere als 60Hz), ist gelistet unter http://www.testufo.com/browser.html ... Dies bedeutet, dass auf einem 75Hz-Monitor requestAnimationFrame() jetzt 75 Mal pro Sekunde auf unterstützten Browsern aufgerufen wird, vorausgesetzt, die Webseite befindet sich gerade im Vordergrund und die CPU/Grafikleistung erlaubt dies.

Chrome 29 und 31 funktioniert gut, wie neuere tut Versionen von Chrome 30. Glücklicherweise Chrom 33 Canary scheint mehr zu haben vollständig das Problem, das ich so weit sehen fixiert, wie ich weiß. Es läuft Animationen viel reibungsloser, ohne unnötige Aufrufe anAnAnationFrame().

Auch ich habe bemerkt, Power-Management (CPU-Verlangsamung/Throttling zu speichern Akku-Energie) kann Chaos auf die Rückrufrate von requestAnimationFrame() ...Sie manifestiert sich als seltsame Spitzen nach oben/unten in Frame Rendering Zeiten (http://www.testufo.com/#test=animation-time-graph&measure=rendering)

+0

Ich sehe nicht, wie sich das alles auf das völlig falsche Verhalten beim Ausführen des rAF-Callbacks bezieht, viele, viele zusätzliche mal pro Refresh-Tick. Auf jeden Fall scheint noch viel Arbeit übrig zu sein, bevor rAF so kugelsicher ist wie setTimeout. –

3

Ich denke, Sie ein Problem haben, weil Sie requestAnimationFrame(step); auf jedem mousedown und mouseup Ereignis nennen. Da Ihre step() Funktion auch (wie es sollte) ruft requestAnimationFrame(step); Sie im Wesentlichen neue "Animationsschleife" für jede mousedown und mouseup Ereignis starten und da Sie nie aufhören sie sie akkumulieren.

Ich kann sehen, dass Sie auch "Animationsschleife" am Ende Ihres Codes starten. Wenn Sie bei Mausereignis sofort neu zeichnen möchten, sollten Sie die Zeichnung aus der step() Funktion entfernen und diese direkt von Maus-Event-Handlern aufrufen.

Samething wie folgt aus:

function redraw() { 
    // drawing logic 
} 
function onmousedown() { 
    // ... 
    redraw() 
} 
function onmouseup() { 
    // ... 
    redraw() 
} 

function step() { 
    redraw(); 
    requestAnimationFrame(step); 
} 
requestAnimationFrame(step); 
+0

Ja, Ihr Pseudocode-Beispiel ist eine sehr gute Destillation der richtigen Art, die Rendering-Schleife rAF zu verwalten, und tatsächlich ist das Starten von zusätzlichen "Threads" beim Rendern per Mausklick falsch und führt zu einer Situation, die schwer zu entwirren ist. .. Ich muss diesen Code noch einmal überprüfen und herausfinden, ob dies ein wesentlicher Faktor für die Probleme ist, die ich erlebt habe. –