File: OptionPages\ForceLowMemoryMode.cs
Web Access
Project: src\src\VisualStudio\VisualStudioDiagnosticsToolWindow\Roslyn.VisualStudio.DiagnosticsWindow.csproj (Roslyn.VisualStudio.DiagnosticsWindow)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Options;
 
namespace Roslyn.VisualStudio.DiagnosticsWindow.OptionsPages
{
    internal sealed class ForceLowMemoryMode
    {
        public static readonly Option2<bool> Enabled = new("ForceLowMemoryMode_Enabled", defaultValue: false);
        public static readonly Option2<int> SizeInMegabytes = new("ForceLowMemoryMode_Enabled", defaultValue: 500);
 
        private readonly IGlobalOptionService _globalOptions;
        private MemoryHogger? _hogger;
 
        public ForceLowMemoryMode(IGlobalOptionService globalOptions)
        {
            _globalOptions = globalOptions;
 
            globalOptions.AddOptionChangedHandler(this, Options_OptionChanged);
 
            RefreshFromSettings();
        }
 
        private void Options_OptionChanged(object sender, object target, OptionChangedEventArgs e)
        {
            if (e.HasOption(static option => option.Equals(Enabled) || option.Equals(SizeInMegabytes)))
            {
                RefreshFromSettings();
            }
        }
 
        private void RefreshFromSettings()
        {
            var enabled = _globalOptions.GetOption(Enabled);
 
            if (_hogger != null)
            {
                _hogger.Cancel();
                _hogger = null;
            }
 
            if (enabled)
            {
                _hogger = new MemoryHogger();
                _ = _hogger.PopulateAndMonitorAsync(_globalOptions.GetOption(SizeInMegabytes));
            }
        }
 
        private class MemoryHogger
        {
            private const int BlockSize = 1024 * 1024; // megabyte blocks
            private const int MonitorDelay = 10000; // 10 seconds
 
            private readonly List<byte[]> _blocks = [];
            private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
 
            public MemoryHogger()
            {
            }
 
            public int Count
            {
                get { return _blocks.Count; }
            }
 
            public void Cancel()
            {
                _cancellationTokenSource.Cancel();
            }
 
            public Task PopulateAndMonitorAsync(int size)
            {
                // run on background thread
                return Task.Factory.StartNew(() => this.PopulateAndMonitorWorkerAsync(size), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
            }
 
            private async Task PopulateAndMonitorWorkerAsync(int size)
            {
                try
                {
                    try
                    {
                        for (var n = 0; n < size; n++)
                        {
                            _cancellationTokenSource.Token.ThrowIfCancellationRequested();
 
                            var block = new byte[BlockSize];
 
                            // initialize block bits (so the memory actually gets allocated.. silly runtime!)
                            for (var i = 0; i < BlockSize; i++)
                            {
                                block[i] = 0xFF;
                            }
 
                            _blocks.Add(block);
 
                            // don't hog the thread
                            await Task.Yield();
                        }
                    }
                    catch (OutOfMemoryException)
                    {
                    }
 
                    // monitor memory to keep it paged in
                    while (true)
                    {
                        _cancellationTokenSource.Token.ThrowIfCancellationRequested();
 
                        try
                        {
                            // access all block bytes
                            for (var b = 0; b < _blocks.Count; b++)
                            {
                                _cancellationTokenSource.Token.ThrowIfCancellationRequested();
 
                                var block = _blocks[b];
 
                                byte tmp;
                                for (var i = 0; i < block.Length; i++)
                                {
                                    tmp = block[i];
                                }
 
                                // don't hog the thread
                                await Task.Yield();
                            }
                        }
                        catch (OutOfMemoryException)
                        {
                        }
 
                        await Task.Delay(MonitorDelay, _cancellationTokenSource.Token).ConfigureAwait(false);
                    }
                }
                catch (OperationCanceledException)
                {
                    _blocks.Clear();
 
                    // force garbage collection
                    for (var i = 0; i < 5; i++)
                    {
                        GC.Collect(GC.MaxGeneration);
                        GC.WaitForPendingFinalizers();
                    }
                }
            }
        }
    }
}