/// <summary> /// Execute one iteration of time-granting loop. /// </summary> /// <remarks> /// The steps are as follows: /// (1) remove and forget all slave handles that requested detaching /// (2) check if there are any blocked slaves; if so DO NOT grant a time interval /// (2.1) if there are no blocked slaves grant a new time interval to every slave /// (3) wait for all slaves that are relevant in this execution (it can be either all slaves or just blocked ones) until they report back /// (4) (optional) sleep if the virtual time passed faster than a real one; this step is executed if <see cref="AdvanceImmediately"> is not set and <see cref="Performance"> is low enough /// (5) update elapsed virtual time /// (6) execute sync hook and delayed actions if any /// </remarks> /// <param name="virtualTimeElapsed">Contains the amount of virtual time that passed during execution of this method. It is the minimal value reported by a slave (i.e, some slaves can report higher/lower values).</param> /// <param name="timeLimit">Maximum amount of virtual time that can pass during the execution of this method. If not set, current <see cref="Quantum"> is used.</param> /// <returns> /// True if sync point has just been reached or False if the execution has been blocked. /// </returns> protected bool InnerExecute(out TimeInterval virtualTimeElapsed, TimeInterval?timeLimit = null) { if (updateNearestSyncPoint) { NearestSyncPoint += timeLimit.HasValue ? TimeInterval.Min(timeLimit.Value, Quantum) : Quantum; updateNearestSyncPoint = false; this.Trace($"Updated NearestSyncPoint to: {NearestSyncPoint}"); } DebugHelper.Assert(NearestSyncPoint.Ticks >= ElapsedVirtualTime.Ticks, $"Nearest sync point set in the past: EVT={ElapsedVirtualTime} NSP={NearestSyncPoint}"); isBlocked = false; var quantum = NearestSyncPoint - ElapsedVirtualTime; this.Trace($"Starting a loop with #{quantum.Ticks} ticks"); virtualTimeElapsed = TimeInterval.Empty; using (sync.LowPriority) { handles.LatchAllAndCollectGarbage(); var shouldGrantTime = handles.AreAllReadyForNewGrant; this.Trace($"Iteration start: slaves left {handles.ActiveCount}; will we try to grant time? {shouldGrantTime}"); elapsedAtLastGrant = stopwatch.Elapsed; if (handles.ActiveCount > 0) { var executor = new PhaseExecutor <LinkedListNode <TimeHandle> >(); if (shouldGrantTime && quantum != TimeInterval.Empty) { executor.RegisterPhase(s => ExecuteGrantPhase(s, quantum)); } if (!(shouldGrantTime && quantum == TimeInterval.Empty)) { executor.RegisterPhase(ExecuteWaitPhase); } if (ExecuteInSerial) { executor.ExecuteInSerial(handles.WithLinkedListNode); } else { executor.ExecuteInParallel(handles.WithLinkedListNode); } var commonElapsedTime = handles.CommonElapsedTime; DebugHelper.Assert(commonElapsedTime >= ElapsedVirtualTime, $"A slave reports time from the past! The current virtual time is {ElapsedVirtualTime}, but {commonElapsedTime} has been reported"); virtualTimeElapsed = commonElapsedTime - ElapsedVirtualTime; } else { this.Trace($"There are no slaves, updating VTE by {quantum.Ticks}"); // if there are no slaves just make the time pass virtualTimeElapsed = quantum; // here we must trigger `TimePassed` manually as no handles has been updated so they won't reflect the passed time TimePassed?.Invoke(quantum); } handles.UnlatchAll(); State = TimeSourceState.Sleeping; var elapsedThisTime = stopwatch.Elapsed - elapsedAtLastGrant; if (!AdvanceImmediately) { var scaledVirtualTicksElapsed = virtualTimeElapsed.WithScaledTicks(1 / Performance).ToTimeSpan() - elapsedThisTime; sleeper.Sleep(scaledVirtualTicksElapsed); } lock (hostTicksElapsed) { this.Trace($"Updating virtual time by {virtualTimeElapsed.InMicroseconds} us"); this.virtualTicksElapsed.Update(virtualTimeElapsed.Ticks); this.hostTicksElapsed.Update(TimeInterval.FromTimeSpan(elapsedThisTime).Ticks); } } if (!isBlocked) { ExecuteSyncPhase(); updateNearestSyncPoint = true; } else { BlockHook?.Invoke(); } State = TimeSourceState.Idle; this.Trace($"The end of {nameof(InnerExecute)} with result={!isBlocked}"); return(!isBlocked); }
/// <summary> /// Grants time interval to a single handle. /// </summary> private void ExecuteGrantPhase(LinkedListNode <TimeHandle> handle, TimeInterval quantum) { State = TimeSourceState.ReportingElapsedTime; handle.Value.GrantTimeInterval(quantum); }
/// <summary> /// Used by the slave to requests a new time interval from the source. /// This method blocks current thread until the time interval is granted. /// </summary> /// <remarks> /// This method will return immediately when the handle is disabled or detached. /// It is illegal to call this method twice in a row if the first call was successful (returned true). It must always be followed by calling <see cref="ReportBackAndContinue"> or <see cref="ReportBackAndBreak">. /// </remarks> /// <returns> /// True if the interval was granted or False when this call was interrupted as a result of detaching or disabling. /// If it returned true, <paramref name="interval"> contains the amount of virtual time to be used by the sink. It is the sum of time interval granted by the source (using <see cref="GrantInterval">) and a time left reported previously by <see cref="ReportBackAndContinue"> or <see cref="ReportBackAndBreak">. /// If it returned false, the time interval is not granted and it is illegal to report anything back using <see cref="ReportBackAndContinue"> or <see cref="ReportBackAndBreak">. /// </returns> public bool RequestTimeInterval(out TimeInterval interval) { this.Trace(); lock (innerLock) { DebugHelper.Assert(!sinkSideInProgress, "Requested a new time interval, but the previous one is still processed."); var result = true; if (!Enabled) { result = false; } else if (isBlocking && SourceSideActive) { if (changingEnabled) { // we test `changingEnabled` here to avoid starvation: // in order to change state of `Enabled` property the handle must not be latched, // so the operation blocks until `latchLevel` drops down to 0; // calling this method (`RequestTimeInterval`) when the handle is in a blocking state results // in latching it temporarily until `WaitUntilDone` is called; // this temporary latching/unlatching together with normal latching/unlatching in a short loop // can cause `latchLevel` to fluctuate from 1 to 2 never allowing the operation modifying `Enabled` to finish result = false; } else { // we check SourceSideActive here as otherwise unblocking will not succeed anyway DebugHelper.Assert(!grantPending, "New grant not expected when blocked."); this.Trace("Asking time source to unblock the time handle"); // latching here is to protect against disabling Enabled that would lead to making IsBlocking false while waiting for unblocking this handle Latch(); Monitor.Exit(innerLock); // it is necessary to leave `innerLock` since calling `UnblockHandle` could lead to a deadlock on `handles` collection var isHandleUnblocked = TimeSource.UnblockHandle(this); Monitor.Enter(innerLock); if (!isHandleUnblocked) { Unlatch(); this.Trace("Unblocking handle is not allowed, quitting"); result = false; } else { this.Trace("Handle unblocked"); // since we are latched here, latchLevel 1 means that the handle is not currently latched by anybody else // why? this is needed as we change the value of isBlocking - this should not happen when the handle is latched by another thread this.Trace("About to wait until the latch reduces to 1"); innerLock.WaitWhile(() => latchLevel > 1, "Waiting for reducing the latch to 1"); isBlocking = false; DebugHelper.Assert(!deferredUnlatch, "Unexpected value of deferredUnlatch"); deferredUnlatch = true; recentlyUnblocked = true; } } } else if (!grantPending) { // wait until a new time interval is granted or this handle is disabled/deactivated innerLock.WaitWhile(() => !grantPending && Enabled && SourceSideActive, "Waiting for a time grant"); result = grantPending; } if (!result) { interval = TimeInterval.Empty; } else { interval = intervalGranted + timeResiduum; timeResiduum = TimeInterval.Empty; sinkSideInProgress = true; grantPending = false; } this.Trace($"{result}, {interval.Ticks}"); return(result); } }