1 //
2 // Copyright (c) 2010-2023 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 using System;
8 using System.Threading;
9 using Antmicro.Migrant;
10 using Antmicro.Renode.Logging;
11 using Antmicro.Renode.Debugging;
12 
13 namespace Antmicro.Renode.Time
14 {
15     /// <summary>
16     /// Represents a main time source generating the time flow.
17     /// </summary>
18     /// <remarks>
19     /// This time source can be set to run for a specified time, specified number of sync points or indefinitely.
20     /// </remarks>
21     public class MasterTimeSource : TimeSourceBase, IDisposable, ITimeDomain
22     {
23         /// <summary>
24         /// Creates new master time source instance.
25         /// </summary>
MasterTimeSource()26         public MasterTimeSource()
27         {
28             locker = new object();
29         }
30 
31         /// <summary>
32         /// Disposes all slaves and stops underlying dispatcher thread.
33         /// </summary>
Dispose()34         public override void Dispose()
35         {
36             this.Trace("Disposing...");
37             lock(locker)
38             {
39                 if(isDisposed)
40                 {
41                     this.Trace("Already disposed");
42                     return;
43                 }
44                 isDisposed = true;
45                 // `Dispose` must be called before `Stop` as the latter waits for all `slaves` to finish (naturally or as a result of `Dispose`)
46                 base.Dispose();
47                 Stop();
48             }
49             this.Trace("Disposed");
50         }
51 
52         /// <summary>
53         /// Run the time source for a specified interval of virtual time.
54         /// </summary>
55         /// <remarks>
56         /// This method is blocking. It can be interrupted by disposing the time source.
57         /// </remarks>
58         /// <param name="period">Amount of virtual time to pass.</param>
RunFor(TimeInterval period)59         public void RunFor(TimeInterval period)
60         {
61             EnsureDispatcherExited();
62 
63             using(ObtainStartedState())
64             using(this.ObtainSourceActiveState())
65             {
66                 while(!isDisposed && period.Ticks > 0)
67                 {
68                     WaitIfBlocked();
69                     InnerExecute(out var timeElapsed, period);
70                     period -= timeElapsed;
71                 }
72             }
73         }
74 
75         /// <summary>
76         /// Run the time source for a specified number of synchronization points.
77         /// </summary>
78         /// <remarks>
79         /// This method is blocking. It can be interrupted by disposing the time source.
80         /// </remarks>
81         /// <param name="numberOfSyncPoints">Number of synchronization points to pass (default 1).</param>
Run(uint numberOfSyncPoints = 1)82         public void Run(uint numberOfSyncPoints = 1)
83         {
84             EnsureDispatcherExited();
85 
86             using(ObtainStartedState())
87             using(this.ObtainSourceActiveState())
88             {
89                 for(var i = 0u; i < numberOfSyncPoints; i++)
90                 {
91                     bool syncPointReached;
92                     do
93                     {
94                         if(isDisposed)
95                         {
96                             break;
97                         }
98                         syncPointReached = InnerExecute(out var notused);
99                     }
100                     while(!syncPointReached);
101                 }
102             }
103         }
104 
105         /// <summary>
106         /// Start the time-dispatching thread that provides new time grants in the background loop.
107         /// </summary>
108         /// <remarks>
109         /// This method is non-blocking. In order to stop the thread call <see cref="Stop"> method.
110         /// </remarks>
Start()111         public new void Start()
112         {
113             this.Trace("Starting...");
114             lock(locker)
115             {
116                 if(!base.Start())
117                 {
118                     this.Trace();
119                     return;
120                 }
121                 // Make sure the previous instance of the dispatcher thread has finished.
122                 // Otherwise it could keep running after we started the new one and cause
123                 // a tricky crash down the line.
124                 dispatcherThread?.Join();
125                 // Get a fresh cancellation token for the new thread.
126                 dispatcherThreadCanceller?.Dispose();
127                 dispatcherThreadCanceller = new CancellationTokenSource();
128                 dispatcherThread = new Thread(() => Dispatcher(dispatcherThreadCanceller.Token)) { Name = "MasterTimeSource Dispatcher", IsBackground = true };
129                 dispatcherThread.Start();
130                 this.Trace("Started");
131             }
132         }
133 
134         /// <summary>
135         /// Stop the time-dispatching thread.
136         /// Note that if this is called on the dispatcher thread itself from a time callback,
137         /// the thread will continue running for a moment after this function returns, but it
138         /// will not begin a new iteration of InnerExecute.
139         /// </summary>
Stop()140         public new void Stop()
141         {
142             this.Trace("Stopping...");
143             lock(locker)
144             {
145                 base.Stop();
146                 // Cancel the currently-running dispatcher thread, if any.
147                 dispatcherThreadCanceller?.Cancel();
148                 // If we're on the dispatcher thread, we are currently in the process of stopping
149                 // initiated by the dispatcher itself, for example as part of a hook callback.
150                 // In this case we can't join our own thread.
151                 if(dispatcherThread != null && dispatcherThread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId)
152                 {
153                     this.Trace("Waiting for dispatcher thread");
154                     EnsureDispatcherExited();
155                 }
156                 this.Trace("Stopped");
157             }
158         }
159 
160         /// <see cref="ITimeSource.Domain">
161         /// <remarks>
162         /// The object of type <see cref="MasterTimeSource"> defines it's own time domain.
163         /// </remarks>
164         public override ITimeDomain Domain => this;
165 
Dispatcher(CancellationToken token)166         private void Dispatcher(CancellationToken token)
167         {
168 #if DEBUG
169             using(this.TraceRegion("Dispatcher loop"))
170 #endif
171             using(ObtainSourceActiveState())
172             using(TimeDomainsManager.Instance.RegisterCurrentThread(() => new TimeStamp(NearestSyncPoint, Domain)))
173             {
174                 try
175                 {
176                     // The token will be canceled when stopping, just after isStarted gets cleared,
177                     // with the crucial difference that the token is NOT shared between threads.
178                     while(!token.IsCancellationRequested)
179                     {
180                         WaitIfBlocked();
181                         InnerExecute(out var notused);
182                     }
183                 }
184                 catch(Exception e)
185                 {
186                     this.Trace(LogLevel.Error, $"Got an exception: {e.Message} @ {e.StackTrace}");
187                     throw;
188                 }
189             }
190         }
191 
EnsureDispatcherExited()192         private void EnsureDispatcherExited()
193         {
194             // We check isStarted to make sure the dispatcher thread is not supposed to be running
195             // and then wait for it to exit if needed. When the time source is paused from within
196             // the dispatcher, the thread object is left behind so that we can wait for it to exit
197             // as needed before further time source operations.
198             DebugHelper.Assert(!isStarted, "Dispatcher thread should not be set to run at this moment");
199             dispatcherThread?.Join();
200             dispatcherThread = null;
201         }
202 
203         private bool isDisposed;
204         [Transient]
205         private Thread dispatcherThread;
206         [Transient]
207         private CancellationTokenSource dispatcherThreadCanceller;
208         private readonly object locker;
209     }
210 }
211