File: System\Windows\Media\Animation\TimeManager.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#if DEBUG
#define TRACE
#endif // DEBUG
 
using System.ComponentModel;
using MS.Utility;
using System.Windows.Threading;
 
namespace System.Windows.Media.Animation
{
    /// <summary>
    /// The object that controls an entire timing tree.
    /// </summary>
    /// <remarks>
    /// <p>A time manager controls the flow of time in a timing tree. The clock is
    /// updated periodically by the rendering system, at which time the progress value
    /// of all active timelines is updated according to the elapsed time. This elapsed time
    /// can be controlled by the application by specifying a custom reference clock in
    /// the constructor.</p>
    /// </remarks>
    internal sealed class TimeManager : DispatcherObject
    {
        #region External interface
 
        #region Construction
 
        /// <summary>
        /// Creates a time manager object in the stopped state.
        /// </summary>
        /// <remarks>
        /// This constructor causes the time manager to use a default system clock to drive
        /// the timing engine. To start the clock moving, call the
        /// <see cref="TimeManager.Start"/> method.
        /// </remarks>
        public TimeManager()
            : this(null)
        {
        }
 
        /// <summary>
        /// Creates a time manager object in the stopped state.
        /// </summary>
        /// <param name="clock">
        /// An interface to an object that provides real-time clock values to the time
        /// manager. The manager will query this interface whenever it needs to know the
        /// current time. This parameter may be null.
        /// </param>
        /// <remarks>
        /// If the clock parameter is null, the time manager uses a default system clock
        /// to drive the timing engine. To start the clock moving, call the
        /// <see cref="TimeManager.Start"/> method.
        /// </remarks>
        public TimeManager(IClock clock)
        {
            _eventQueue = new Queue<WeakReference>();
            Clock = clock;
            _timeState = TimeState.Stopped;
            _lastTimeState = TimeState.Stopped;
            _globalTime = new TimeSpan(-1);
            _lastTickTime = new TimeSpan(-1);
            _nextTickTimeQueried = false;
            _isInTick = false;
            ParallelTimeline timeManagerTimeline = new ParallelTimeline(new TimeSpan(0), Duration.Forever);
            timeManagerTimeline.Freeze();
            _timeManagerClock = new ClockGroup(timeManagerTimeline);
            _timeManagerClock.MakeRoot(this);
        }
 
        #endregion // Construction
 
        #region Properties
 
        /// <summary>
        /// Accesses the reference clock used by this time manager to obtain
        /// real-world clock values.
        /// </summary>
        public IClock Clock
        {
            get
            {
//                 VerifyAccess();
                return _systemClock;
            }
            set
            {
//                 VerifyAccess();
                if (value != null)
                {
                    _systemClock = value;
                }
                else
                {
                    if (MediaContext.IsClockSupported)
                    {
                        _systemClock = (IClock)MediaContext.From(Dispatcher);
                    }
                    else
                    {
                        _systemClock = (IClock)new GTCClock();
                    }
                }
            }
        }
 
        /// <summary>
        /// The current position of the clock, relative to the starting time.
        /// </summary>
        public Nullable<TimeSpan> CurrentTime
        {
            get
            {
//                 VerifyAccess();
 
                if (_timeState == TimeState.Stopped)
                {
                    return null;
                }
                else
                {
                    return _globalTime;
                }
            }
        }
 
        /// <summary>
        /// True if the structure of the timing tree has changed since the last
        /// tick.
        /// </summary>
        public bool IsDirty
        {
            get
            {
//                 VerifyAccess();
 
                return _isDirty;
            }
        }
 
        #endregion // Properties
 
        #region Methods
 
        /// <summary>
        /// Pauses the clock.
        /// </summary>
        /// <remarks>
        /// To start the clock again, call the <see cref="Resume"/> method. This method has
        /// no effect if the clock is not in the running state.
        /// </remarks>
        public void Pause()
        {
//             VerifyAccess();
            if (_timeState == TimeState.Running)
            {
                // Record the pause time
                _pauseTime = _systemClock.CurrentTime;
 
                // Stop the tree from responding to ticks
                _timeState = TimeState.Paused;
            }
        }
 
        /// <summary>
        /// Resets the time manager and sets a new start time.
        /// </summary>
        /// <remarks>
        /// Calling this method is equivalent to calling <see cref="Stop"/> followed
        /// by <see cref="Start"/>.
        /// </remarks>
        /// <seealso cref="TimeManager.Start"/>
        /// <seealso cref="TimeManager.Stop"/>
        public void Restart()
        {
//             VerifyAccess();
            // Remember the current state
            TimeState oldState = _timeState;
 
            // Stop and start
            Stop();
            Start();
 
            // Go back to the old state
            _timeState = oldState;
 
            // If we were paused, update the pause position to the start position
            if (_timeState == TimeState.Paused)
            {
                _pauseTime = _startTime;
            }
        }
 
        /// <summary>
        /// Resumes the clock.
        /// </summary>
        /// <remarks>
        /// This function has no effect if the clock was not in the paused state. If the
        /// clock is stopped rather than paused, call the <see cref="Start"/> method
        /// instead.
        /// </remarks>
        public void Resume()
        {
//             VerifyAccess();
            if (_timeState == TimeState.Paused)
            {
                // Adjust the starting time
                _startTime = _startTime + _systemClock.CurrentTime - _pauseTime;
 
                // Restart the tree
                _timeState = TimeState.Running;
 
                // See if we need to tick sooner now that we are running
                if (GetNextTickNeeded() >= TimeSpan.Zero)
                {
                    NotifyNewEarliestFutureActivity();
                }
            }
        }
 
        /// <summary>
        /// Seeks the global clock to a new position.
        /// </summary>
        /// <param name="offset">
        /// The seek offset, in milliseconds. The meaning of this parameter depends on the value of
        /// the origin parameter.
        /// </param>
        /// <param name="origin">
        /// The meaning of the offset parameter. See the <see cref="TimeSeekOrigin"/> enumeration
        /// for possible values.
        /// </param>
        /// <remarks>
        /// <p>This method seeks the global clock. The timelines of all timelines in the timing tree are
        /// also updated accordingly. Any events that those timelines would have fired in between the old
        /// and new clock positions are skipped.</p>
        ///
        /// <p>Because the global clock is infinite there is no defined end point, therefore seeking
        /// from the end position has no effect.</p>
        /// </remarks>
        public void Seek(int offset, TimeSeekOrigin origin)
        {
            if (_timeState >= TimeState.Paused)
            {
                switch (origin)
                {
                    case TimeSeekOrigin.BeginTime:
                        // Use the offset as the new time;
                        break;
 
                    case TimeSeekOrigin.Duration:
                        // Undefined for the global clock
                        return;
 
                    default:
                        // Invalid enum argument
                        throw new InvalidEnumArgumentException(SR.Format(SR.Enum_Invalid, "TimeSeekOrigin"));
                }
 
                // Truncate to the beginning
                if (offset < 0)
                {
                    offset = 0;
                }
 
                TimeSpan offsetTime = TimeSpan.FromMilliseconds((double)offset);
 
                // Only do the work if we actually moved
                if (offsetTime != _globalTime)
                {
                    // Set the new global time
                    _globalTime = offsetTime;
 
                    // Adjust the starting time for future ticks
                    _startTime =_systemClock.CurrentTime - _globalTime;
 
                    // Update the timing tree accordingly
                    _timeManagerClock.ComputeTreeState();
                }
            }
        }
 
        /// <summary>
        /// Starts the time manager at the current time.
        /// </summary>
        /// <remarks>
        /// The current time is determined by querying the reference clock for this
        /// time manager.
        /// </remarks>
        public void Start()
        {
//             VerifyAccess();
            if (_timeState == TimeState.Stopped)
            {
                // Reset the state of the timing tree
                _lastTickTime = TimeSpan.Zero;
 
                // Start the tree
                _startTime =_systemClock.CurrentTime;
                _globalTime = TimeSpan.Zero;
                _timeState = TimeState.Running;
                _timeManagerClock.RootActivate();
            }
        }
 
        /// <summary>
        /// Stops the clock.
        /// </summary>
        /// <remarks>
        /// After the clock is stopped, it can be restarted with a call to <see cref="Start"/>.
        /// This method has no effect if the clock was not in the running or paused state.
        /// </remarks>
        /// <seealso cref="TimeManager.Start"/>
        /// <seealso cref="TimeManager.Stop"/>
        public void Stop()
        {
//             VerifyAccess();
            if (_timeState >= TimeState.Paused)
            {
                _timeManagerClock.RootDisable();
                _timeState = TimeState.Stopped;
            }
        }
 
        /// <summary>
        /// Moves the clock forward to the current time and updates the state of
        /// all timing objects based on the time change.
        /// </summary>
        /// <remarks>
        /// The associated reference clock is used to determine the current time.
        /// The new position of the clock will be equal to the difference between the
        /// starting system time and the current system time. The time manager requires
        /// the system time to move forward.
        /// </remarks>
        public void Tick()
        {
            try
            {
#if DEBUG
                // On a regular interval, clean up our tables of known
                // timelines and clocks
                if (++_frameCount >= 1000) // Should be about once every 10s
                {
                    Timeline.CleanKnownTimelinesTable();
                    System.Windows.Media.Animation.Clock.CleanKnownClocksTable();
                    _frameCount = 0;
                }
#endif // DEBUG
                // Don't need to worry about needing a tick sooner
                _nextTickTimeQueried = false;
 
                // Mark the tree as clean immediately. If any changes occur during
                // processing of the tick, the tree will be marked as dirty again.
                _isDirty = false;
 
                // No effect unless we are in the running state
                if (_timeState == TimeState.Running)
                {
                    // Figure out the current time
                    _globalTime = GetCurrentGlobalTime();
 
                    // Start the tick
                    _isInTick = true;
                }
 
                // Trace the start of the tick and pass along the absolute time to which
                // we are ticking.
                EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnimation | EventTrace.Keyword.KeywordPerf, EventTrace.Event.WClientTimeManagerTickBegin, (_startTime + _globalTime).Ticks / TimeSpan.TicksPerMillisecond);
 
                // Run new property querying logic on the timing tree
                if (_lastTimeState == TimeState.Stopped && _timeState == TimeState.Stopped)  // We were stopped the whole time
                {
                    _currentTickInterval = TimeIntervalCollection.CreateNullPoint();
                }
                else  // We were not stopped at some time, so process the tick interval
                {
                    _currentTickInterval = TimeIntervalCollection.CreateOpenClosedInterval(_lastTickTime, _globalTime);
 
                    // If at either tick we were stopped, add the null point to represent that
                    if (_lastTimeState == TimeState.Stopped || _timeState == TimeState.Stopped)
                    {
                        _currentTickInterval.AddNullPoint();
                    }
                }
 
                // Compute the tree state, using _currentTickInterval to compute the events that occured
                _timeManagerClock.ComputeTreeState();
 
                // Cache TimeManager state at this time
                _lastTimeState = _timeState;
 
                // When the tick is done, we raise timing events
                RaiseEnqueuedEvents();
            }
            finally
            {
                _isInTick = false;
 
                // Cache the tick time
                _lastTickTime = _globalTime;
 
                //trace the end of the tick
                EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordAnimation | EventTrace.Keyword.KeywordPerf, EventTrace.Event.WClientTimeManagerTickEnd);
            }
 
            // At the end of every tick clean up GC'ed clocks, if necessary
            CleanupClocks();
        }
 
        // Get the maximumum desired framerate that has been set in one of our
        // top-level clock.
        internal int GetMaxDesiredFrameRate()
        {
            return _timeManagerClock.GetMaxDesiredFrameRate();
        }
 
        #endregion // Methods
 
        #endregion // External interface
 
        #region Internal implementation
 
        #region Types
 
        /// <summary>
        /// A clock implementation that uses GetTickCount to get real-world clock values.
        /// </summary>
        internal class GTCClock : IClock
        {
            #region Internal implementation
 
            #region Methods
            /// <summary>
            /// Creates a GTCClock object.
            /// </summary>
            internal GTCClock()
            {
            }
            /// <summary>
            /// Gets the current real-world time.
            /// </summary>
            TimeSpan IClock.CurrentTime
            {
                get
                {
                    return TimeSpan.FromTicks(DateTime.Now.Ticks);
                }
            }
            #endregion // Methods
 
            #endregion // Internal implementation
        }
 
 
        /// <summary>
        /// This is used by DrtTiming in order to specify the CurrentTime
        /// as it pleases.  Note this class is internal
        /// </summary>
        internal class TestTimingClock : IClock
        {
            #region Internal implementation
 
            #region Methods
 
            /// <summary>
            /// Gets and sets the CurrentTime
            /// </summary>
            public TimeSpan CurrentTime
            {
                get
                {
                    return _currentTime;
                }
 
                set
                {
                    _currentTime = value;
                }
            }
 
            #endregion // Methods
            #region Data
            private TimeSpan _currentTime;
            #endregion // Data
 
            #endregion // Internal implementation
        }
 
        #endregion // Types
 
        #region Methods
 
        /// <summary>
        /// Removes references to garbage-collected clocks.
        /// </summary>
        private void CleanupClocks()
        {
            // We only run the clean-up procedure if we previously detected
            // that some clocks were GC'ed. The detection occurs in the
            // finalizer thread, perhaps concurrently with the execution of
            // this method. See the comments in the ScheduleClockCleanup
            // method for why we don't need to lock to protect against that
            // concurrency.
 
            if (_needClockCleanup)
            {
                // We are about to clean up, so clear the bit first. That way
                // if another thread sets the bit again then at worst we
                // have to run through the process one more time, with no ill
                // consequences other than a small run-time hit. If we wait to
                // set the bit until after we actually cleaned up we may fail
                // to detect any clocks that are GC'ed while we are cleaning
                // up.
                _needClockCleanup = false;
                _timeManagerClock.RootCleanChildren();
            }
        }
 
        /// <summary>
        /// Puts a Clock into a queue for firing events.
        /// </summary>
        /// <param name="sender">
        /// The object sending the event.
        /// </param>
        /// <remarks>
        /// The time at which the event is actually raised is implementation-specific and is
        /// subject to change at any time.
        /// </remarks>
        internal void AddToEventQueue(Clock sender)
        {
            _eventQueue.Enqueue(sender.WeakReference);
        }
 
        /// <summary>
        /// Converts the current real world time into clock time.
        /// </summary>
        /// <returns>
        /// The global clock time.
        /// </returns>
        /// <remarks>
        /// This function uses the custom clock provided in the TimeManager constructor, if any.
        /// </remarks>
        internal TimeSpan GetCurrentGlobalTime()
        {
            // The conversion depends on our current state
            switch (_timeState)
            {
                case TimeState.Stopped:
 
                    // If we are stopped, the current global time is always zero
                    return TimeSpan.Zero;
 
                case TimeState.Paused:
 
                    // If we are paused, the real-world clock appears to us as if it's
                    // stopped at the pause time
                    return _pauseTime - _startTime;
 
                case TimeState.Running:
 
                    // If we are running, use the actual real-world clock.
                    // However, keep time "frozen" while processing a tick or
                    // by request.
                    if (_isInTick || _lockTickTime)
                    {
                        return _globalTime;
                    }
                    else
                    {
                        return _systemClock.CurrentTime - _startTime;
                    }
 
                default:
 
                    Debug.Assert(false, "Unrecognized TimeState enumeration value");
                    return TimeSpan.Zero;
            }
        }
 
        /// <summary>
        /// Locks the current time for successive Tick calls.
        /// </summary>
        internal void LockTickTime()
        {
            _lockTickTime = true;
        }
 
        /// <summary>
        /// Called by timelines to tell the time manager that a future activity
        /// was scheduled at the head of the list.
        /// </summary>
        /// <remarks>
        /// When a new future activity is added to the head, it means that the next
        /// tick is needed earlier than was thought before the activity was scheduled.
        /// This means that if the NextTickNeeded property was queried we need to
        /// notify the querying module that the value of the property needs to be
        /// queried again.
        /// </remarks>
        internal void NotifyNewEarliestFutureActivity()
        {
            if (_nextTickTimeQueried && _userNeedTickSooner != null)
            {
                _nextTickTimeQueried = false;
                _userNeedTickSooner(this, EventArgs.Empty);
            }
        }
 
        /// <summary>
        /// Raises any events stored in the queue.
        /// </summary>
        private void RaiseEnqueuedEvents()
        {
            while (_eventQueue.Count > 0)
            {
                WeakReference instance = _eventQueue.Dequeue();
                Clock clock = (Clock)instance.Target;
 
                if (clock != null)
                {
                    clock.RaiseAccumulatedEvents();
                }
            }
        }
 
        /// <summary>
        /// Schedules a clean-up pass to look for GC'ed clocks.
        /// </summary>
        /// <remarks>
        /// This method can be called from any thread. In particular, it is
        /// safe to call this method from another object's finalizer.
        /// </remarks>
        internal void ScheduleClockCleanup()
        {
            // To clean up GC'ed clocks we set a bit when a finalizer runs (in
            // the finalizer thread). Later, in the UI thraed, we run the
            // CleanupClocks method which checks whether this bit is set and
            // executes the clean-up procedure if it is. This can be done
            // entirely without locks, even on multiprocessor machines,
            // because of the sequence in which the variable is accessed. In
            // particular, note that this method sets the bit unconditionally,
            // which means we can essentially treat it as atomic.
            //
            // Consider the possible sequences:
            //
            //  1.  The bit is already set when this method runs
            //      In that case we are definitely in a safe situation because
            //      we won't change the value of the bit, so this call is a
            //      no-op.
            //
            //  2.  The bit is not set when this method runs
            //      In that case we need to consider what the UI thread may do.
            //      Other threads running this method are irrelevant because
            //      the method is essentially atomic, and thanks to UI context
            //      affinity there can only be one UI thread running. If that
            //      thread is running CleanupClocks at the same time that this
            //      thread is running this method then there are three cases:
            //      a)  We set the bit before CleanupClocks checks the bit
            //          This is obviously safe -- the other method will run the
            //          clean-up procedure immediately.
            //      b)  We set the bit after CleanupClocks checks the bit but
            //          before it clears it. This is actually case 1, because
            //          CleanupClocks only does anything if the bit was already
            //          set. Case 1 is a no-op, so we are safe.
            //      c)  We set the bit after CleanupClocks checks and clears
            //          the bit. This may happen before, during or after the
            //          clean-up procedure, but all cases are safe because this
            //          bit won't be checked again by this invocation of
            //          CleanupClocks. All that will happen is that we will
            //          need to run CleanupClocks again at some point in the
            //          future. In the worst case this is nothing more than a
            //          small waste of time because the clock whose
            //          finalization we are detecting may be cleaned up already
            //          by the current invocation of CleanupClocks. However,
            //          the operation is still safe.
            //
            // We are trading off the cost of locking every time against the
            // cost of an occasional unnecessary clean-up procedure. The latter
            // seems a lot less likely, and therefore smaller in the long run.
 
            _needClockCleanup = true;
        }
 
        /// <summary>
        /// Marks the timing tree as dirty. The tree is "cleaned" on the following
        /// tick.
        /// </summary>
        internal void SetDirty()
        {
            _isDirty = true;
        }
 
        /// <summary>
        /// Unlocks the current time such that the next tick will query the
        /// system time.
        /// </summary>
        internal void UnlockTickTime()
        {
            _lockTickTime = false;
        }
 
        #endregion // Methods
 
        #region Properties
 
        /// <summary>
        /// The current global time.
        /// </summary>
        internal TimeSpan InternalCurrentGlobalTime
        {
            get
            {
                return _globalTime;
            }
        }
 
        /// <summary>
        /// Whether the TimeManager is stopped
        /// </summary>
        internal bool InternalIsStopped
        {
            get
            {
                return (_timeState == TimeState.Stopped);
            }
        }
 
        internal TimeIntervalCollection InternalCurrentIntervals
        {
            get
            {
                return _currentTickInterval;
            }
            set
            {
                _currentTickInterval = value;  // This allows temporarily setting custom interval values
        }
        }
 
        /// <summary>
        /// The time until the state of any time clock in the tree is expected
        /// to change. If the method returns a negative value, no state changes
        /// are expected any time in the future and therefore a tick will never
        /// be needed.
        /// </summary>
        /// <remarks>
        /// This method is only a hint. Even if timelines don't change state,
        /// their progress values need to be updated regularly, requiring a tick at
        /// some minimum interval. In addition, certain interactive events can
        /// cause activity to be scheduled earlier than the previously known
        /// earliest activity, in which case a tick is needed sooner. To detect
        /// this condition, hook the <see cref="NeedTickSooner"/> event.
        /// </remarks>
        internal TimeSpan GetNextTickNeeded()
        {
//             VerifyAccess();
            _nextTickTimeQueried = true;
 
            if (_timeState == TimeState.Running)
            {
                // We are running, see how soon the tree needs updating
                TimeSpan? nextTickNeededTime = _timeManagerClock.InternalNextTickNeededTime;
 
                if (nextTickNeededTime.HasValue)
                {
                    // Get the time the time manager has been running as of this moment.
                    TimeSpan timeManagerCurrentTime = _systemClock.CurrentTime - _startTime;
 
                    // Calculate how long from "now" we'll need another tick.
                    TimeSpan nextTickNeededTimeFromCurrentTime = nextTickNeededTime.Value - timeManagerCurrentTime;
 
                    if (nextTickNeededTimeFromCurrentTime <= TimeSpan.Zero)
                    {
                        // We've already past the time at which we need another
                        // tick so tick again ASAP.
                        return TimeSpan.Zero;
                    }
                    else
                    {
                        // Return the time until we need another tick
                        return nextTickNeededTimeFromCurrentTime;
                    }
                }
                else  // The tree does not need any ticks in the future
                {
                    return TimeSpan.FromTicks(-1);
                }
            }
            else
            {
                // Our state is not Running, so we don't have any ticks coming up
                return TimeSpan.FromTicks(-1);
            }
        }
 
        /// <summary>
        /// Unchecked internal access to the time shift since the last tick.
        /// </summary>
        internal TimeSpan LastTickDelta
        {
            get
            {
                return _globalTime - _lastTickTime;
            }
        }
 
        /// <summary>
        /// Internal access to the time since the last tick.
        /// </summary>
        internal TimeSpan LastTickTime
        {
            get
            {
                return _lastTickTime;
            }
        }
 
        /// <summary>
        /// Returns the clock that is the root of the timing tree managed by
        /// this time manager.
        /// </summary>
        internal ClockGroup TimeManagerClock
        {
            get
            {
//                 VerifyAccess();
                return _timeManagerClock;
            }
        }
 
        /// <summary>
        /// The present state of the time manager.
        /// </summary>
        internal TimeState State
        {
            get
            {
                return _timeState;
            }
        }
 
        #endregion // Properties
 
        #region Events
 
        /// <summary>
        /// Raised when a change has been made to the timing engine that may require
        /// it to be ticked sooner than was previously expected.
        /// </summary>
        internal event EventHandler NeedTickSooner
        {
            add
            {
//                 VerifyAccess();
                _userNeedTickSooner += value;
            }
            remove
            {
//                 VerifyAccess();
                _userNeedTickSooner -= value;
            }
        }
 
        #endregion // Events
 
        #region Debugging instrumentation
 
#if DEBUG
 
        /// <summary>
        /// Dumps the internal state of the whole timing tree
        /// </summary>
        internal void Dump()
        {
            _timeManagerClock.Dump();
        }
 
#endif // DEBUG
 
        #endregion // Debugging instrumentation
 
        #region Data
 
        #region Timing data
 
        private TimeState                   _timeState;
        private TimeState                   _lastTimeState;
 
        private IClock                      _systemClock;
 
        private TimeSpan                    _globalTime;
        private TimeSpan                    _startTime;
        private TimeSpan                    _lastTickTime;
        private TimeSpan                    _pauseTime;
 
        private TimeIntervalCollection      _currentTickInterval;
        private bool                        _nextTickTimeQueried,
                                            _isDirty,
                                            _isInTick,
                                            _lockTickTime;
        private EventHandler                _userNeedTickSooner;
        private ClockGroup                  _timeManagerClock;
        private Queue<WeakReference>        _eventQueue;
 
        #endregion // Timing data
 
        #region Linking data
 
        private bool                        _needClockCleanup;
 
        #endregion // Linking data
 
        #region Debug data
 
#if DEBUG
 
        private int                         _frameCount;
 
#endif // DEBUG
 
        #endregion // Debug data
 
        #endregion // Data
 
        #endregion // Internal implementation
    }
}