// // Nenya library - tools for developing networked games // Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved // https://github.com/threerings/nenya // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.media.timer; import com.samskivert.util.Interval; import com.samskivert.util.Logger; import com.samskivert.swing.RuntimeAdjust; import com.threerings.media.MediaPrefs; /** * Calibrates timing values from a subclass' implementation of current against those returned by * System.currentTimeMillis. If the subclass timer is moving 1.25 times faster or .75 times slower * than currentTimeMillis, hold it to the values from currentTimeMillis. */ public abstract class CalibratingTimer implements MediaTimer { /** * Initializes this timer. Must be called before the timer is used. * * @param milliDivider - value by which current() must be divided to get milliseconds * @param microDivider - value by which current() must be divided to get microseconds */ protected void init (long milliDivider, long microDivider) { _milliDivider = milliDivider; _microDivider = microDivider; reset(); log.info("Using " + getClass() + " timer", "mfreq", _milliDivider, "ufreq", _microDivider, "start", _startStamp); } // documentation inherited from interface public long getElapsedMicros () { // Some machines will have set _microDivider to 0 if (_microDivider == 0) { // Just convert our millisecond value instead return getElapsedMillis() * 1000; } else { return elapsed() / _microDivider; } } // documentation inherited from interface public long getElapsedMillis () { return elapsed() / _milliDivider; } // documentation inherited from interface public void reset () { if (_calibrateInterval != null) { _calibrateInterval.cancel(); _calibrateInterval = null; } _startStamp = _priorCurrent = current(); _driftMilliStamp = System.currentTimeMillis(); _driftTimerStamp = current(); _calibrateInterval = new Interval(Interval.RUN_DIRECT) { @Override public void expired () { calibrate(); } }; _calibrateInterval.schedule(CALIBRATE_INTERVAL, CALIBRATE_INTERVAL, false); } /** Return the current value for this timer. */ public abstract long current (); /** * Returns the greatest drift ratio we've had to compensate for. */ public float getMaxDriftRatio () { return _maxDriftRatio; } /** * Clears out our remembered max drift. */ public void clearMaxDriftRatio () { _maxDriftRatio = 1.0F; } /** Returns the difference between _startStamp and current() */ protected long elapsed () { long current = current(); if (_driftRatio != 1.0) { long elapsed = current - _priorCurrent; _startStamp += (elapsed - (elapsed * _driftRatio)); } _priorCurrent = current; return current - _startStamp; } /** Calculates the drift factor from the time elapsed from the last calibrate call. */ protected void calibrate () { long currentTimer = current(); long currentMillis = System.currentTimeMillis(); long elapsedTimer = currentTimer - _driftTimerStamp; float elapsedMillis = currentMillis - _driftMilliStamp; float drift = elapsedMillis / (elapsedTimer / _milliDivider); if (_debugCalibrate.getValue()) { log.warning("Calibrating", "timer", elapsedTimer, "millis", elapsedMillis, "drift", drift, "timerstamp", _driftTimerStamp, "millistamp", _driftMilliStamp, "current", currentTimer); } if (elapsedTimer < 0) { log.warning("The timer has decided to live in the past, resetting drift" , "previousTimer", _driftTimerStamp, "currentTimer", currentTimer, "previousMillis", _driftMilliStamp, "currentMillis", currentMillis); _driftRatio = 1.0F; } else if (drift > MAX_ALLOWED_DRIFT_RATIO || drift < MIN_ALLOWED_DRIFT_RATIO) { log.warning("Calibrating", "drift", drift); // Ignore the drift if it's hugely out of range. That indicates general clock insanity, // and we just want to stay out of the way. if (drift < 100 * MAX_ALLOWED_DRIFT_RATIO && drift > MIN_ALLOWED_DRIFT_RATIO / 100) { _driftRatio = drift; } else { _driftRatio = 1.0F; } if (Math.abs(drift - 1.0) > Math.abs(_maxDriftRatio - 1.0)) { _maxDriftRatio = drift; } } else if (_driftRatio != 1.0) { log.warning("Calibrating", "drift", drift); // If we're within bounds now but we weren't before, reset _driftFactor and log it _driftRatio = 1.0F; } _driftMilliStamp = currentMillis; _driftTimerStamp = currentTimer; } /** current() value when the timer was started. */ protected long _startStamp; /** current() value when elapsed was called last. */ protected long _priorCurrent; /** Amount by which current() should be divided to get milliseconds. */ protected long _milliDivider; /** Amount by which current() should be divided to get microseconds. */ protected long _microDivider; /** currentTimeMillis() value from the last time we called calibrate. */ protected long _driftMilliStamp = System.currentTimeMillis(); /** current() value from the last time we called calibrate. */ protected long _driftTimerStamp; /** Ratio of currentTimeMillis to timer millis. */ protected float _driftRatio = 1.0F; /** The largest drift we've had to adjust for. */ protected float _maxDriftRatio = 1.0F; /** Interval that fires every five seconds to run the calibration. */ protected Interval _calibrateInterval; /** Used to log things. */ protected final Logger log = Logger.getLogger(CalibratingTimer.class); /** The largest drift ratio we'll allow without correcting. */ protected static final float MAX_ALLOWED_DRIFT_RATIO = 1.1F; /** The smallest drift ratio we'll allow without correcting. */ protected static final float MIN_ALLOWED_DRIFT_RATIO = 0.9F; /** Milliseconds between calibrate calls. */ protected static final int CALIBRATE_INTERVAL = 5000; /** A debug hook that toggles dumping of calibration values. */ protected static RuntimeAdjust.BooleanAdjust _debugCalibrate = new RuntimeAdjust.BooleanAdjust( "Toggles calibrations statistics", "narya.media.timer", MediaPrefs.config, false); }