|
// 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;
}
}
|