Skip to content

Commit

Permalink
allow TimeSpanDataQueue to allocate more memory if needed, see #354
Browse files Browse the repository at this point in the history
  • Loading branch information
jbphet committed Jun 11, 2021
1 parent 30b029d commit 58d0fa9
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 85 deletions.
143 changes: 70 additions & 73 deletions js/common/model/TimeSpanDataQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,103 +8,82 @@
*/

import statesOfMatter from '../../statesOfMatter.js';
import SOMConstants from '../SOMConstants.js';

// constants
const MIN_EXPECTED_DT = SOMConstants.NOMINAL_TIME_STEP;

class TimeSpanDataQueue {

/**
* {number} length - max number of entries in the queue
* {number} maxTimeSpan - max span of time that can be stored, in seconds
* {number} minExpectedDt - the minimum expected dt (delta time) value in seconds, used to allocate memory
*/
constructor( length, maxTimeSpan ) {
assert && assert( length > 0, 'length must be positive' );
this.length = length;
this.maxTimeSpan = maxTimeSpan;

// @private - array where entries are kept
this.dataQueue = new Array( length );
constructor( maxTimeSpan, minExpectedDt = MIN_EXPECTED_DT ) {

// initialize the data array with a set of reusable objects so that subsequent allocations are not needed
for ( let i = 0; i < length; i++ ) {
this.dataQueue[ i ] = { deltaTime: 0, value: null };
}

// @public (read-only) {number} - the total of all values currently in the queue
// @public (read-only) - the total of all values currently in the queue
this.total = 0;

// @private - variables used to make this thing work
this.head = 0;
this.tail = 0;
// @private - the amount of time currently represent by the entries in the queue
this.timeSpan = 0;

// @private
this.maxTimeSpan = maxTimeSpan;

// @private {DataQueueEntry[]} - data queue, which is an array where entries are kept that track times and values
this.dataQueue = [];

// The queue length is calculated based on the specified time span and the expected minimum dt value. There is some
// margin built into this calculation. This value is used to pre-allocate all of the memory which is expected to
// be needed, and thus optimize performance.
const maxExpectedDataQueueLength = Math.ceil( maxTimeSpan / minExpectedDt * 1.1 );

// @private {DataQueueEntry[]} - a pool of unused data queue entries, pre-allocated for performance
this.unusedDataQueueEntries = [];
for ( let i = 0; i < maxExpectedDataQueueLength; i++ ) {
this.unusedDataQueueEntries.push( new DataQueueEntry( 0, null ) );
}
}

/**
* Add a new value with associated delta time. This automatically removes data values that go beyond the max time
* span, and also updates the total value and the current time span.
* Add a new value with associated dt (delta time). This automatically removes data values that go beyond the max
* time span from the queue, and also updates the total value and the current time span.
* @param {number} value
* @param {number} dt
* @public
*/
add( value, dt ) {

assert && assert( dt < this.maxTimeSpan, 'dt value is greater than max time span' );
const nextHead = ( this.head + 1 ) % this.length;

// TODO: The following 'if' clause and its contents are temporary debug output, see https://github.com/phetsims/states-of-matter/issues/354.
if ( nextHead === this.tail ) {
console.log( '=======' );
console.log( `this.length = ${this.length}` );
console.log( `this.maxTimeSpan = ${this.maxTimeSpan} seconds` );
console.log( `this.head = ${this.head}` );
console.log( `this.tail = ${this.tail}` );
console.log( 'data queue contents:' );
let totalContainedTime = 0;
let minDt = Number.POSITIVE_INFINITY;
let maxDt = Number.NEGATIVE_INFINITY;
_.times( this.length - 1, index => {
let indexToPrint = ( this.head - ( index + 1 ) ) % this.length;
if ( indexToPrint < 0 ) {
indexToPrint += this.length;
}
const dt = this.dataQueue[ indexToPrint ].deltaTime;
console.log( '-------' );
console.log( ` index: ${indexToPrint}` );
console.log( ` dt: ${dt}` );
console.log( ` value: ${this.dataQueue[ indexToPrint ].value}` );
minDt = Math.min( minDt, dt );
maxDt = Math.max( maxDt, dt );
totalContainedTime += dt;
} );
console.log( '\n' );
console.log( `totalContainedTime = ${totalContainedTime}` );
console.log( `minDt = ${minDt}` );
console.log( `maxDt = ${maxDt}` );
}
assert && assert( nextHead !== this.tail, 'no space left in moving time window' );
assert && assert( dt < this.maxTimeSpan, 'dt value is greater than max time span, this won\'t work' );

// in non-debug mode ignore requests that would exceed the capacity
if ( nextHead === this.tail ) {
return;
// Add the new data item to the queue.
let dataQueueEntry;
if ( this.unusedDataQueueEntries.length > 0 ) {
dataQueueEntry = this.unusedDataQueueEntries.pop();
dataQueueEntry.dt = dt;
dataQueueEntry.value = value;
}
else {

// Add the new data item to the queue.
this.dataQueue[ this.head ].value = value;
this.dataQueue[ this.head ].deltaTime = dt;
// The pool has run dry, allocate a new entry. Ideally, this should never happen, because we try to pre-allocate
// everything needed, but it can occur in cases where the browser is running at a different frequency than the
// expected nominal rate. See https://github.com/phetsims/states-of-matter/issues/354.
dataQueueEntry = new DataQueueEntry( dt, value );
}
this.dataQueue.push( dataQueueEntry );
this.timeSpan += dt;
this.total += value;
this.head = nextHead;

// Remove the oldest data items until we are back within the maximum time span.
// Check if the total time span represented by the items in the queue exceeds the max time and, if so, remove items.
while ( this.timeSpan > this.maxTimeSpan ) {
const nextTail = ( this.tail + 1 ) % this.length;
if ( nextTail === nextHead ) {

// nothing more can be removed, so bail
assert && assert( false, 'time span exceeded, but nothing appears to be in the queue - probably a bug' );
break;
}
this.total -= this.dataQueue[ this.tail ].value;
this.timeSpan -= this.dataQueue[ this.tail ].deltaTime;
this.tail = nextTail;
assert && assert( this.dataQueue.length > 0, 'data queue is empty but max time is exceeded, there must ba a bug' );
const removedDataQueueEntry = this.dataQueue.shift();
this.timeSpan -= removedDataQueueEntry.dt;
this.total -= removedDataQueueEntry.value;

// Mark the removed entry as unused and return it to the pool.
removedDataQueueEntry.value = null;
this.unusedDataQueueEntries.push( removedDataQueueEntry );
}
}

Expand All @@ -113,10 +92,28 @@ class TimeSpanDataQueue {
* @public
*/
clear() {
this.head = 0;
this.tail = 0;
this.total = 0;
this.timeSpan = 0;
this.dataQueue.forEach( dataQueueItem => {
dataQueueItem.value = null;
this.unusedDataQueueEntries.push( dataQueueItem );
} );
this.dataQueue.length = 0;
}
}

/**
* simple inner class that defines the entries that go into the data queue
*/
class DataQueueEntry {

/**
* @param {number} dt - delta time, in seconds
* @param {number|null} value - the value for this entry, null if this entry is unused
*/
constructor( dt, value ) {
this.dt = dt;
this.value = value;
}
}

Expand Down
28 changes: 16 additions & 12 deletions js/common/model/engine/AbstractVerletAlgorithm.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@

import NumberProperty from '../../../../../axon/js/NumberProperty.js';
import statesOfMatter from '../../../statesOfMatter.js';
import SOMConstants from '../../SOMConstants.js';
import TimeSpanDataQueue from '../TimeSpanDataQueue.js';

// Constants that control the pressure calculation. The size of the pressure accumulator assumes a max sim rate of
// 1 / 60, which derives from the standard 60 FPS rate at which browsers currently run. May need to go up someday.
// Constants that control the pressure calculation.
const PRESSURE_CALC_TIME_WINDOW = 12; // in seconds, empirically determined to be responsive but not jumpy
const NOMINAL_DT = 1 / 60; // in seconds, assuming 60 fps
const PRESSURE_ACCUMULATOR_LENGTH = Math.ceil( PRESSURE_CALC_TIME_WINDOW / NOMINAL_DT * 1.1 );
const NOMINAL_DT = SOMConstants.NOMINAL_TIME_STEP; // in seconds

// constants that control when the container explodes
const EXPLOSION_PRESSURE = 41; // in model units, empirically determined
Expand Down Expand Up @@ -46,8 +45,8 @@ class AbstractVerletAlgorithm {
// during execution of the Verlet algorithm, must be cleared by the client.
this.lidChangedParticleVelocity = false;

// @private {TimeSpanDataQueue}, moving time window queue for tracking the pressure data
this.pressureAccumulatorQueue = new TimeSpanDataQueue( PRESSURE_ACCUMULATOR_LENGTH, PRESSURE_CALC_TIME_WINDOW );
// @private {TimeSpanDataQueue} - moving time window queue for tracking the pressure data
this.pressureAccumulatorQueue = new TimeSpanDataQueue( PRESSURE_CALC_TIME_WINDOW );

// @private, tracks time above the explosion threshold
this.timeAboveExplosionPressure = 0;
Expand Down Expand Up @@ -283,7 +282,9 @@ class AbstractVerletAlgorithm {
this.pressureAccumulatorQueue.add( pressureThisStep, dt );

// Get the pressure value, but make sure it doesn't go below zero, because we have seen instances of that due
// to floating point errors, see https://github.com/phetsims/states-of-matter/issues/240.
// to floating point errors, see https://github.com/phetsims/states-of-matter/issues/240. We use the total
// time window in this calculation rather that the time span in the queue because it creates a nice ramp-up
// effect.
const newPressure = Math.max( this.pressureAccumulatorQueue.total / PRESSURE_CALC_TIME_WINDOW, 0 );

if ( newPressure > EXPLOSION_PRESSURE ) {
Expand Down Expand Up @@ -318,14 +319,17 @@ class AbstractVerletAlgorithm {
'this method is intended for use during state setting only'
);

// get rid of any accumulated values
// Get rid of any accumulated values.
this.pressureAccumulatorQueue.clear();

// calculate the instantaneous sample value needed to make the overall pressure be the needed value
const pressureSampleInstantaneousValue = pressure * PRESSURE_CALC_TIME_WINDOW / PRESSURE_ACCUMULATOR_LENGTH;
// Calculate the number of "artificial" samples to insert into the time span queue.
const numberOfSamples = PRESSURE_CALC_TIME_WINDOW / NOMINAL_DT;

// add the entries to the accumulator
_.times( PRESSURE_ACCUMULATOR_LENGTH, () => {
// Calculate the instantaneous sample value needed to make the overall pressure be the needed value.
const pressureSampleInstantaneousValue = pressure * PRESSURE_CALC_TIME_WINDOW / numberOfSamples;

// Add the entries to the accumulator.
_.times( numberOfSamples, () => {
this.pressureAccumulatorQueue.add( pressureSampleInstantaneousValue, NOMINAL_DT );
} );
}
Expand Down

0 comments on commit 58d0fa9

Please sign in to comment.