2013-10-16 14 views
18

Argomento correlato: requestAnimationFrame garbage collectionChrome requestAnimationFrame emette

Ho lavorato verso animazioni fluide in un widget che sto costruendo per i dispositivi touch, e uno degli strumenti che ho trovato per aiutarmi con questo è stato il Chrome Schermata Timeline memoria.

Mi ha aiutato un po 'a valutare il consumo di memoria nel ciclo rAF, ma a questo punto sono infastidito da alcuni aspetti del comportamento che sto osservando in Chrome 30.

All'inizio della mia pagina, che ha il ciclo rAF in esecuzione, vedo questo. enter image description here

Sembra a posto. Non ci dovrebbe essere un dente di sega se ho fatto il mio lavoro ed eliminato le allocazioni di oggetti nel mio anello interno. Questo comportamento è coerente con l'argomento collegato, vale a dire che Chrome ha una perdita incorporata ogni volta che si utilizza rAF. (yikes!)

Diventa più interessante quando inizio a fare varie cose nella pagina.

enter image description here

non sto davvero facendo qualcosa di diverso, solo l'aggiunta temporaneamente altri due elementi che ottenere CSS3 3D trasforma stili applicati per alcuni fotogrammi, e poi mi fermo interagire con loro.

Quello che vediamo qui è che Chrome segnala che all'improvviso ogni infornamento di rAF (16 ms) risulta in Animation Frame Fired x 3.

Questo ripetendo, e la velocità con cui lo fa, aumenta monotonicamente fino all'aggiornamento della pagina.

È già possibile vedere nel secondo screencap che la pendenza a dente di sega è aumentata notevolmente dopo il salto iniziale da Animation Frame Fired a Animation Frame Fired x 3.

Poco dopo è saltato a x 21:

enter image description here

Sembrerebbe che il mio codice viene eseguito un sacco di tempi supplementari, ma tutte le esecuzioni multiple in più è solo sprecato il calore , calcolo scartato.

Mentre stavo prendendo il terzo screencap, il mio Macbook si stava scaldando piuttosto male. Poco dopo, prima di essere in grado di scorrere la timeline fino alla fine (circa 8 minuti) per vedere a che cosa era aumentato il numero x, la finestra dell'ispettore non rispondeva del tutto e mi è stato chiesto che la mia pagina non rispondesse e dovessi essere terminato.

Ecco la totalità del codice in esecuzione nella pagina:

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

Per completezza qui è la pagina di prova 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> 

Questo dovrebbe soddisfare qualsiasi "ci mostrano il codice" richieste .

Ora non scommetterei la mia vita su di esso, ma sono abbastanza certo che ho fatto almeno un buon lavoro di usare rAF in modo corretto. Non sto abusando di nulla e, a questo punto, ho perfezionato il codice per essere molto leggero sull'assegnazione della memoria Javascript.

Quindi, davvero, c'è assolutamente, nessun motivo per cui Chrome lo fa e tenta di utilizzare il mio laptop in orbita come un razzo. Nessuna ragione.

Safari in generale sembra gestire meglio (non alla fine muoiono), e anche io noterà che IOS è generalmente in grado di mantenere una traduzione 200x600px div e rotazione a 60fps.

Tuttavia, devo ammettere che non ho visto Chrome davvero morire in questo modo a meno che non li ho registrando la timeline di memoria.

Mi sto solo graffiando la testa a questo punto. Probabilmente si tratta solo di un'interazione involontaria e imprevedibile con questa particolare funzionalità degli strumenti di sviluppo (l'unica del suo genere, per quanto ne so).

Allora ho provato qualcosa di nuovo da almeno aiutare a indagare la questione con la linea temporale memoria extra-callback-combustione:

Aggiunto queste righe.

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

Questo registra praticamente fuori tutte le volte che la mia routine RAF (la funzione step()) viene chiamata.

quando è in esecuzione normalmente si scrive giù un tempo all'incirca ogni 16,7 ms.

ho ottenuto questo:

enter image description here

Questo indica chiaramente che è ri-esecuzione step() con lo stesso parametro di ingresso tempo di almeno 22 volte, proprio come la linea temporale sta cercando di dirmi.

Quindi ti sfido, internet, a dirmi che questo è il comportamento previsto. :)

+3

+ 1, ma sento che sarebbe ottenere una maggiore attenzione da parte di persone se la lunghezza della domanda è un po 'più corto –

+0

@Onaseriousnote sto prendendo suggerimenti –

+0

Questo potrebbe essere di interesse : https://groups.google.com/forum/#!topic/google-chrome-developer-tools/S_mKrF42a4Y – jedierikb

risposta

2

ho creato le animazioni per http://www.testufo.com e anche un requestAnimationFrame() controllo di coerenza a http://www.testufo.com/animation-time-graph

L'elenco dei browser web che supportano la sincronizzazione automatica dei requestAnimationFrame() per la frequenza di aggiornamento del monitor del computer (anche se diverso 60Hz), è elencato a http://www.testufo.com/browser.html ... Ciò significa che su un monitor a 75 Hz, requestAnimationFrame() viene ora chiamato 75 volte al secondo sui browser supportati, a condizione che la pagina Web sia attualmente in primo piano e le prestazioni della CPU/grafica lo consentano.

Chrome 29 e 31 funziona bene, così come le versioni più recenti di Chrome 30. Per fortuna, cromo 33 Canary sembra essere più pienamente risolto il problema che vedo per quanto ne so. Esegue animazioni molto più agevolmente, senza chiamate inutili per requestAnimationFrame().

Inoltre, ho notato che la gestione dell'alimentazione (rallentamento/limitazione della CPU consente di risparmiare energia della batteria) può causare il caos sulla velocità di richiamata di requestAnimationFrame() ...Essa si manifesta come strani picchi verso l'alto/verso il basso in un frame i tempi di rendering (http://www.testufo.com/#test=animation-time-graph&measure=rendering)

+0

Non vedo come tutto ciò si riferisca al comportamento completamente scorretto nell'esecuzione della richiamata di rAF molti altri in più tick per refresh. In ogni caso, sembra che ci sia ancora molto lavoro da fare prima che rAF sia a prova di proiettile come setTimeout. –

3

io pensi di avere un problema, perché si chiama requestAnimationFrame(step); su ogni mousedown e mouseup evento. Dal momento che la tua funzione step() chiama anche requestAnimationFrame(step);, in sostanza, si avvia un nuovo "ciclo di animazione" per ogni evento mousedown e mouseup e dal momento che non si fermano mai, si accumulano.

Posso vedere che si avvia anche "loop di animazione" alla fine del codice. Se vuoi ridisegnare immediatamente l'evento del mouse, devi spostare il disegno dalla funzione step() e chiamarlo direttamente dai gestori di eventi del mouse.

samething come questo:

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

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

Sì, il tuo esempio di pseudocodice è un ottimo distillato del modo giusto per gestire il ciclo di rendering rAF, e in effetti iniziare "thread" extra di rendering tramite clic del mouse è sbagliato e porta a una situazione che può essere difficile da svelare. .. Avrò bisogno di rivisitare questo codice e capire se questo è un fattore significativo nei problemi che stavo vivendo. –