public void SynchronizationContextCaptured() { var syncContext = SingleThreadedTestSynchronizationContext.New(); SynchronizationContext.SetSynchronizationContext(syncContext); TaskCompletionSource <object> callbackResult = new TaskCompletionSource <object>(); var frame = SingleThreadedTestSynchronizationContext.NewFrame(); var callback = new Action <GenericParameterHelper>( p => { try { Assert.NotNull(SynchronizationContext.Current); callbackResult.SetResult(null); } catch (Exception e) { callbackResult.SetException(e); } frame.Continue = false; }); var progress = new ProgressWithCompletion <GenericParameterHelper>(callback); IProgress <GenericParameterHelper> reporter = progress; Task.Run(delegate { reporter.Report(new GenericParameterHelper(1)); }); SingleThreadedTestSynchronizationContext.PushFrame(syncContext, frame); callbackResult.Task.GetAwaiter().GetResult(); }
protected void SimulateUIThread(Func <Task> testMethod) { Verify.Operation(this.originalThreadManagedId == Environment.CurrentManagedThreadId, "We can only simulate the UI thread if you're already on it (the starting thread for the test)."); Exception failure = null; this.dispatcherContext.Post(async delegate { try { await testMethod(); } catch (Exception ex) { failure = ex; } finally { this.testFrame.Continue = false; } }, null); SingleThreadedTestSynchronizationContext.PushFrame(this.dispatcherContext, this.testFrame); if (failure != null) { // Rethrow original exception without rewriting the callstack. ExceptionDispatchInfo.Capture(failure).Throw(); } }
/// <summary> /// Runs an asynchronous task synchronously, using just the current thread to execute continuations. /// </summary> internal static void Run(Func <Task> func) { if (func == null) { throw new ArgumentNullException(nameof(func)); } var prevCtx = SynchronizationContext.Current; try { var syncCtx = SingleThreadedTestSynchronizationContext.New(); SynchronizationContext.SetSynchronizationContext(syncCtx); var t = func(); if (t == null) { throw new InvalidOperationException(); } var frame = SingleThreadedTestSynchronizationContext.NewFrame(); t.ContinueWith(_ => { frame.Continue = false; }, TaskScheduler.Default); SingleThreadedTestSynchronizationContext.PushFrame(syncCtx, frame); t.GetAwaiter().GetResult(); } finally { SynchronizationContext.SetSynchronizationContext(prevCtx); } }
protected void ExecuteOnDispatcher(Func <Task> action) { if (!SingleThreadedTestSynchronizationContext.IsSingleThreadedSyncContext(SynchronizationContext.Current)) { SynchronizationContext.SetSynchronizationContext(SingleThreadedTestSynchronizationContext.New()); } var frame = SingleThreadedTestSynchronizationContext.NewFrame(); Exception failure = null; SynchronizationContext.Current.Post( async _ => { try { await action(); } catch (Exception ex) { failure = ex; } finally { frame.Continue = false; } }, null); SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current, frame); if (failure != null) { ExceptionDispatchInfo.Capture(failure).Throw(); } }
protected void ExecuteOnDispatcher(Func <Task> action, bool staRequired = true) { Action worker = delegate { var frame = SingleThreadedTestSynchronizationContext.NewFrame(); Exception failure = null; SynchronizationContext.Current.Post( async _ => { try { await action(); } catch (Exception ex) { failure = ex; } finally { frame.Continue = false; } }, null); SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current, frame); if (failure != null) { ExceptionDispatchInfo.Capture(failure).Throw(); } }; if ((!staRequired || Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) && SingleThreadedTestSynchronizationContext.IsSingleThreadedSyncContext(SynchronizationContext.Current)) { worker(); } else { this.ExecuteOnSTA(() => { if (!SingleThreadedTestSynchronizationContext.IsSingleThreadedSyncContext(SynchronizationContext.Current)) { SynchronizationContext.SetSynchronizationContext(SingleThreadedTestSynchronizationContext.New()); } worker(); }); } }
public void ValueFactoryRequiresMainThreadHeldByOtherSync(bool passJtfToLazyCtor) { var ctxt = SingleThreadedTestSynchronizationContext.New(); SynchronizationContext.SetSynchronizationContext(ctxt); var context = new JoinableTaskContext(); var asyncPump = context.Factory; var originalThread = Thread.CurrentThread; var evt = new AsyncManualResetEvent(); var lazy = new AsyncLazy <object>( async delegate { // It is important that no await appear before this JTF.Run call, since // we're testing that the value factory is not invoked while the AsyncLazy // holds a private lock that would deadlock when called from another thread. asyncPump.Run(async delegate { await asyncPump.SwitchToMainThreadAsync(this.TimeoutToken); }); await Task.Yield(); return(new object()); }, passJtfToLazyCtor ? asyncPump : null); // mix it up to exercise all the code paths in the ctor. var backgroundRequest = Task.Run(async delegate { return(await lazy.GetValueAsync()); }); Thread.Sleep(AsyncDelay); // Give the background thread time to call GetValueAsync(), but it doesn't yield (when the test was written). var foregroundRequest = lazy.GetValueAsync(); var frame = SingleThreadedTestSynchronizationContext.NewFrame(); var combinedTask = Task.WhenAll(foregroundRequest, backgroundRequest); combinedTask.WithTimeout(UnexpectedTimeout).ContinueWith(_ => frame.Continue = false, TaskScheduler.Default); SingleThreadedTestSynchronizationContext.PushFrame(ctxt, frame); // Ensure that the test didn't simply timeout, and that the individual tasks did not throw. Assert.True(foregroundRequest.IsCompleted); Assert.True(backgroundRequest.IsCompleted); Assert.Same(foregroundRequest.GetAwaiter().GetResult(), backgroundRequest.GetAwaiter().GetResult()); }
protected void PushFrame() { SingleThreadedTestSynchronizationContext.PushFrame(this.dispatcherContext, this.testFrame); }
/// <summary> /// Runs a given scenario many times to observe memory characteristics and assert that they can satisfy given conditions. /// </summary> /// <param name="scenario">The delegate to invoke.</param> /// <param name="maxBytesAllocated">The maximum number of bytes allowed to be allocated by one run of the scenario. Use -1 to indicate no limit.</param> /// <param name="iterations">The number of times to invoke <paramref name="scenario"/> in a row before measuring average memory impact.</param> /// <param name="allowedAttempts">The number of times the (scenario * iterations) loop repeats with a failing result before ultimately giving up.</param> /// <param name="completeSynchronously"><c>true</c> to synchronously complete instead of yielding.</param> /// <returns>A task that captures the result of the operation.</returns> protected async Task CheckGCPressureAsync(Func <Task> scenario, int maxBytesAllocated, int iterations = 100, int allowedAttempts = GCAllocationAttempts, bool completeSynchronously = false) { const int quietPeriodMaxAttempts = 3; const int shortDelayDuration = 250; const int quietThreshold = 1200; // prime the pump for (int i = 0; i < 2; i++) { await MaybeShouldBeComplete(scenario(), completeSynchronously); } long waitForQuietMemory1, waitForQuietMemory2, waitPeriodAllocations, waitForQuietAttemptCount = 0; do { waitForQuietMemory1 = GC.GetTotalMemory(true); await MaybeShouldBlock(Task.Delay(shortDelayDuration), completeSynchronously); waitForQuietMemory2 = GC.GetTotalMemory(true); waitPeriodAllocations = Math.Abs(waitForQuietMemory2 - waitForQuietMemory1); this.Logger.WriteLine("Bytes allocated during quiet wait period: {0}", waitPeriodAllocations); }while (waitPeriodAllocations > quietThreshold || ++waitForQuietAttemptCount >= quietPeriodMaxAttempts); if (waitPeriodAllocations > quietThreshold) { this.Logger.WriteLine("WARNING: Unable to establish a quiet period."); } // This test is rather rough. So we're willing to try it a few times in order to observe the desired value. bool attemptWithNoLeakObserved = false; bool attemptWithinMemoryLimitsObserved = false; for (int attempt = 1; attempt <= allowedAttempts; attempt++) { this.Logger?.WriteLine("Iteration {0}", attempt); int[] gcCountBefore = new int[GC.MaxGeneration + 1]; int[] gcCountAfter = new int[GC.MaxGeneration + 1]; long initialMemory = GC.GetTotalMemory(true); #if NET46 || NETCOREAPP2_0 GC.TryStartNoGCRegion(8 * 1024 * 1024); #endif for (int i = 0; i <= GC.MaxGeneration; i++) { gcCountBefore[i] = GC.CollectionCount(i); } for (int i = 0; i < iterations; i++) { await MaybeShouldBeComplete(scenario(), completeSynchronously); } for (int i = 0; i < gcCountAfter.Length; i++) { gcCountAfter[i] = GC.CollectionCount(i); } long allocated = (GC.GetTotalMemory(false) - initialMemory) / iterations; #if NET46 || NETCOREAPP2_0 GC.EndNoGCRegion(); #endif attemptWithinMemoryLimitsObserved |= maxBytesAllocated == -1 || allocated <= maxBytesAllocated; long leaked = long.MaxValue; for (int leakCheck = 0; leakCheck < 3; leakCheck++) { // Allow the message queue to drain. if (completeSynchronously) { // If there is a dispatcher sync context, let it run for a bit. // This allows any posted messages that are now obsolete to be released. if (SingleThreadedTestSynchronizationContext.IsSingleThreadedSyncContext(SynchronizationContext.Current)) { var frame = SingleThreadedTestSynchronizationContext.NewFrame(); SynchronizationContext.Current.Post(state => frame.Continue = false, null); SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current, frame); } } else { await Task.Yield(); } leaked = (GC.GetTotalMemory(true) - initialMemory) / iterations; attemptWithNoLeakObserved |= leaked <= 3 * IntPtr.Size; // any real leak would be an object, which is at least this size. if (attemptWithNoLeakObserved) { break; } await MaybeShouldBlock(Task.Delay(shortDelayDuration), completeSynchronously); } this.Logger?.WriteLine("{0} bytes leaked per iteration.", leaked); this.Logger?.WriteLine("{0} bytes allocated per iteration ({1} allowed).", allocated, maxBytesAllocated); for (int i = 0; i <= GC.MaxGeneration; i++) { Assert.False(gcCountAfter[i] > gcCountBefore[i], $"WARNING: Gen {i} GC occurred {gcCountAfter[i] - gcCountBefore[i]} times during testing. Results are probably totally wrong."); } if (attemptWithNoLeakObserved && attemptWithinMemoryLimitsObserved) { // Don't keep looping. We got what we needed. break; } // give the system a bit of cool down time to increase the odds we'll pass next time. GC.Collect(); await MaybeShouldBlock(Task.Delay(shortDelayDuration), completeSynchronously); } Assert.True(attemptWithNoLeakObserved, "Leaks observed in every iteration."); Assert.True(attemptWithinMemoryLimitsObserved, "Excess memory allocations in every iteration."); }