File: Implementation\VirtualMemoryNotificationListener.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Runtime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.VisualStudio.LanguageServices.Implementation;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.Shell.Interop;
using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider;
 
namespace Microsoft.VisualStudio.LanguageServices;
 
/// <summary>
/// Listens to broadcast notifications from the Visual Studio Shell indicating that the application is running
/// low on available virtual memory.
/// </summary>
internal sealed class VirtualMemoryNotificationListener : IVsBroadcastMessageEvents
{
    // memory threshold to turn off full solution analysis - 200MB
    private const long MemoryThreshold = 200 * 1024 * 1024;
 
    // low vm more info page link
    private const string LowVMMoreInfoLink = "https://go.microsoft.com/fwlink/?LinkID=799402&clcid=0x409";
    private readonly IGlobalOptionService _globalOptions;
    private readonly VisualStudioWorkspace _workspace;
    private readonly WorkspaceCacheService? _workspaceCacheService;
 
    private bool _alreadyLogged;
    private bool _infoBarShown;
 
    private VirtualMemoryNotificationListener(
        IVsShell shell,
        IGlobalOptionService globalOptions,
        VisualStudioWorkspace workspace)
    {
        _globalOptions = globalOptions;
        _workspace = workspace;
        _workspaceCacheService = workspace.Services.GetService<IWorkspaceCacheService>() as WorkspaceCacheService;
 
        if (GCSettings.IsServerGC)
        {
            // Server GC has been explicitly enabled, which tends to run with higher memory pressure than the
            // default workstation GC. Allow this case without triggering frequent feature shutdown.
            return;
        }
 
        _workspace.WorkspaceChanged += OnWorkspaceChanged;
 
        // Note: We never unhook this event sink. It lives for the lifetime of the host.
        ErrorHandler.ThrowOnFailure(shell.AdviseBroadcastMessages(this, out var cookie));
    }
 
    public static async Task<VirtualMemoryNotificationListener> CreateAsync(
        VisualStudioWorkspace workspace,
        IThreadingContext threadingContext,
        IAsyncServiceProvider serviceProvider,
        IGlobalOptionService globalOptions,
        CancellationToken cancellationToken)
    {
        await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        var shell = (IVsShell?)await serviceProvider.GetServiceAsync(typeof(SVsShell)).ConfigureAwait(true);
        Assumes.Present(shell);
 
        return new VirtualMemoryNotificationListener(shell, globalOptions, workspace);
    }
 
    /// <summary>
    /// Called by the Visual Studio Shell to notify components of a broadcast message.
    /// </summary>
    /// <param name="msg">The message identifier.</param>
    /// <param name="wParam">First parameter associated with the message.</param>
    /// <param name="lParam">Second parameter associated with the message.</param>
    /// <returns>S_OK always.</returns>
    public int OnBroadcastMessage(uint msg, IntPtr wParam, IntPtr lParam)
    {
        switch (msg)
        {
            case VSConstants.VSM_VIRTUALMEMORYLOW:
            case VSConstants.VSM_VIRTUALMEMORYCRITICAL:
            case VSConstants.VSM_MEMORYHIGH:
            case VSConstants.VSM_MEMORYEXCESSIVE:
                {
                    if (!_alreadyLogged)
                    {
                        // record that we had hit critical memory barrier
                        Logger.Log(FunctionId.VirtualMemory_MemoryLow, KeyValueLogMessage.Create(m =>
                        {
                            // which message we are logging and memory left in bytes when this is called.
                            m["MSG"] = msg;
                            m["MemoryLeft"] = (long)wParam;
                        }));
 
                        _alreadyLogged = true;
                    }
 
                    _workspaceCacheService?.FlushCaches();
 
                    if (ShouldDisableBackgroundAnalysis((long)wParam))
                    {
                        DisableBackgroundAnalysis();
                        ShowInfoBarIfRequired();
                    }
 
                    // turn off low latency GC mode.
                    // once we hit this, not hitting "Out of memory" exception is more important than typing being smooth all the time.
                    // once it is turned off, user will hit time to time keystroke which responsive time is more than 50ms. in our own perf lab,
                    // about 1-2% was over 50ms with this off when we first introduced this GC mode.
                    GCManager.TurnOffLowLatencyMode();
 
                    break;
                }
        }
 
        return VSConstants.S_OK;
    }
 
    private bool ShouldDisableBackgroundAnalysis(long availableMemory)
    {
        // conditions
        // 1. Available memory is less than the threshold and 
        // 2. Background analysis is not already minimal and
        // 3. Background analysis memory monitor is on (user can set it off using registry to prevent turning off background analysis)
 
        return availableMemory < MemoryThreshold &&
            !SolutionCrawlerOptionsStorage.LowMemoryForcedMinimalBackgroundAnalysis &&
            _globalOptions.GetOption(VirtualMemoryNotificationListenerOptionsStorage.BackgroundAnalysisMemoryMonitor);
    }
 
    private static void DisableBackgroundAnalysis()
    {
        // Force low VM minimal background analysis for the current VS session.
        SolutionCrawlerOptionsStorage.LowMemoryForcedMinimalBackgroundAnalysis = true;
    }
 
    private void RenableBackgroundAnalysis()
    {
        // Revert forced low VM minimal background analysis for the current VS session.
        SolutionCrawlerOptionsStorage.LowMemoryForcedMinimalBackgroundAnalysis = false;
    }
 
    private void ShowInfoBarIfRequired()
    {
        if (_infoBarShown)
        {
            return;
        }
 
        // Show info bar.
        _workspace.Services.GetRequiredService<IErrorReportingService>()
            .ShowGlobalErrorInfo(
                message: ServicesVSResources.Visual_Studio_has_suspended_some_advanced_features_to_improve_performance,
                TelemetryFeatureName.VirtualMemoryNotification,
                exception: null,
                new InfoBarUI(ServicesVSResources.Re_enable, InfoBarUI.UIKind.Button, RenableBackgroundAnalysis),
                new InfoBarUI(ServicesVSResources.Learn_more, InfoBarUI.UIKind.HyperLink,
                    () => VisualStudioNavigateToLinkService.StartBrowser(new Uri(LowVMMoreInfoLink)), closeAfterAction: false));
 
        _infoBarShown = true;
    }
 
    private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
    {
        if (e.Kind != WorkspaceChangeKind.SolutionAdded)
        {
            return;
        }
 
        // For newly opened solution, reset the info bar state.
        _infoBarShown = false;
    }
}