コード例 #1
0
    protected void ExecuteOnDispatcher(Func <Task> action)
    {
        if (!SingleThreadedTestSynchronizationContext.IsSingleThreadedSyncContext(SynchronizationContext.Current))
        {
            SynchronizationContext.SetSynchronizationContext(SingleThreadedTestSynchronizationContext.New());
        }

        SingleThreadedTestSynchronizationContext.IFrame?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 is object)
        {
            ExceptionDispatchInfo.Capture(failure).Throw();
        }
    }
コード例 #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))
                    {
                        SingleThreadedTestSynchronizationContext.IFrame?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.");
    }