protected void PushFrame()
 {
     SingleThreadedTestSynchronizationContext.PushFrame(this.dispatcherContext, this.testFrame);
 }
Example #2
0
        /// <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);
                GC.TryStartNoGCRegion(8 * 1024 * 1024);
                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;
                GC.EndNoGCRegion();

                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.");
        }
Example #3
0
 private JoinableTaskContext InitializeJTCAndSC()
 {
     SynchronizationContext.SetSynchronizationContext(SingleThreadedTestSynchronizationContext.New());
     return(new JoinableTaskContext());
 }