Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.md file in the project root for more information.

namespace Microsoft.VisualStudio.ProjectSystem.VS
{
internal class FeatureFlags
{
internal const string EnableNuGetRestoreCycleDetection = "ManagedProjectSystem.EnableNuGetRestoreCycleDetection";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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.md file in the project root for more information.

namespace Microsoft.VisualStudio.ProjectSystem.VS.PackageRestore
{
// This algorithm is to detect the pattern A -> B -> A -> B -> A in the most recent N values.
// To keep track of the most N recent values we are using a queue of fixed size, where:
// The most recent value is inserted at the front in O(1) time, and the oldest value is removed at the
// back in O(1) time.
// The counter will keep track of how many times values have appeared in the queue consecutively.
// i.e.
// A -> B -> C -> A -> B -> D -> X -> Y -> X -> Y -> X -> Y -> X
// |---cycle detected----|
internal sealed class NuGetRestoreCycleDetector
{
// The fixed size of the queue
private readonly int _size = 5;

private readonly object _lock;
private readonly Queue<byte[]> _values;
Copy link
Copy Markdown

@jacdavis jacdavis Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queue

Why are we using a queue here and not some data structure with O(1) lookup? I expected this to be a hash table of hashes. A queue doesn't make a lot of sense in my mind. Am I missing some info? #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the hash _lookupTable to improve the search to O(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not convinced a queue is the correct data structure here. Can you please call me to discuss as requested?

private readonly Dictionary<byte[], int> _lookupTable;
private int _counter;

/// <summary>
/// Fixed size of the numbers of values to store.
/// </summary>
/// <remarks>
/// This represents how deep to search for hash cycle.
/// </remarks>
public int Size { get; private set; }

public NuGetRestoreCycleDetector()
{
_lock = new object();
_values = new Queue<byte[]>();
_lookupTable = new Dictionary<byte[], int>();
Size = _size;
}

/// <summary>
/// Validate if hash1 cycle exist.
/// If no cycle exist, then the hashValue is stored.
/// </summary>
/// <param name="hashValue">This is hashValue that is used to verify if it exists in previous restores</param>
/// <returns>True if it contains hash1 cycle, otherwise false</returns>
public bool ComputeCycleDetection(byte[] hashValue)
{
lock (_lock)
{
if (QueueContainsValue(hashValue))
{
_counter++;

// Verify that a hashValue has repeated in almost all cases
if (_counter > Size)
{
Clear();
return true;
}
}
else
{
_counter = 0;
}

Add(hashValue);
}

return false;

bool QueueContainsValue(byte[] hashValue)
{
if (_lookupTable.TryGetValue(hashValue, out int hashCounter) && hashCounter > 0)
{
return true;
}
return false;
}
}

private void Add(byte[] newHashValue)
{
if (_values.Count >= Size)
{
var oldestHashValue = _values.Dequeue();
_lookupTable[oldestHashValue]--;
}

_values.Enqueue(newHashValue);
if (_lookupTable.ContainsKey(newHashValue))
{
_lookupTable[newHashValue]++;
}
else
{
_lookupTable.Add(newHashValue, 1);
}
}

public void Clear()
{
_counter = 0;
_values.Clear();
_lookupTable.Clear();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

using System.Threading.Tasks.Dataflow;
using Microsoft.Internal.Performance;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.IO;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.ProjectSystem.VS.UI.InfoBarService;
using Microsoft.VisualStudio.Telemetry;
Comment thread
ocallesp marked this conversation as resolved.
Outdated
using NuGet.SolutionRestoreManager;
using Microsoft.Internal.VisualStudio.Shell.Interop;

namespace Microsoft.VisualStudio.ProjectSystem.VS.PackageRestore
{
Expand Down Expand Up @@ -49,6 +53,10 @@ internal partial class PackageRestoreDataSource : ChainedProjectValueDataSourceB
// |_________________________________________| |_________________________________________|
//

private readonly NuGetRestoreCycleDetector _cycleDetector = new();
private readonly IVsUIService<SVsFeatureFlags, IVsFeatureFlags> _featureFlagsService;
private readonly ITelemetryService _telemetryService;
private readonly IInfoBarService _infoBarService;
private readonly UnconfiguredProject _project;
private readonly IPackageRestoreUnconfiguredInputDataSource _dataSource;
private readonly IProjectAsynchronousTasksService _projectAsynchronousTasksService;
Expand All @@ -61,6 +69,9 @@ internal partial class PackageRestoreDataSource : ChainedProjectValueDataSourceB

[ImportingConstructor]
public PackageRestoreDataSource(
IVsUIService<SVsFeatureFlags, IVsFeatureFlags> featureFlagsService,
ITelemetryService telemetryService,
IInfoBarService infoBarService,
UnconfiguredProject project,
IPackageRestoreUnconfiguredInputDataSource dataSource,
[Import(ExportContractNames.Scopes.UnconfiguredProject)] IProjectAsynchronousTasksService projectAsynchronousTasksService,
Expand All @@ -71,6 +82,9 @@ public PackageRestoreDataSource(
PackageRestoreSharedJoinableTaskCollection sharedJoinableTaskCollection)
: base(project, sharedJoinableTaskCollection, synchronousDisposal: true, registerDataSource: false)
{
_featureFlagsService = featureFlagsService;
_telemetryService = telemetryService;
_infoBarService = infoBarService;
_project = project;
_dataSource = dataSource;
_projectAsynchronousTasksService = projectAsynchronousTasksService;
Expand Down Expand Up @@ -129,12 +143,18 @@ private async Task<bool> RestoreCoreAsync(PackageRestoreUnconfiguredInput value)

if (_latestHash != null && hash.AsSpan().SequenceEqual(_latestHash))
{
_cycleDetector.Clear();
Comment thread
ocallesp marked this conversation as resolved.
Outdated
SaveNominatedConfiguredVersions(value.ConfiguredInputs);
return true;
}

_latestHash = hash;

if (await CycleDetectedAsync(hash, _projectAsynchronousTasksService.UnloadCancellationToken))
{
return false;
}

_restoreStarted = true;
JoinableTask<bool> joinableTask = JoinableFactory.RunAsync(() =>
{
Expand Down Expand Up @@ -163,6 +183,42 @@ private async Task<bool> RestoreCoreAsync(PackageRestoreUnconfiguredInput value)
return success;
}

private async Task<bool> CycleDetectedAsync(byte[] hash, CancellationToken cancellationToken)
{
bool enabled = await IsNuGetRestoreCycleDetectionEnabledAsync(cancellationToken);

if (!enabled)
{
return false;
}

bool cycleDetected = _cycleDetector.ComputeCycleDetection(hash);

if (!cycleDetected)
{
return false;
}

_telemetryService.PostEvent(TelemetryEventName.NuGetRestoreCycleDetected);

await _infoBarService.ShowInfoBarAsync(
VSResources.InfoBarMessageNuGetCycleDetected,
KnownMonikers.StatusError,
cancellationToken
);

return true;
}

private async Task<bool> IsNuGetRestoreCycleDetectionEnabledAsync(CancellationToken cancellationToken)
{
await _project.Services.ThreadingPolicy.SwitchToUIThread(cancellationToken);

IVsFeatureFlags featureFlagsService = _featureFlagsService.Value;

return featureFlagsService.IsFeatureEnabled(FeatureFlags.EnableNuGetRestoreCycleDetection, defaultValue: false);
}

private async Task<bool> NominateForRestoreAsync(ProjectRestoreInfo restoreInfo, CancellationToken cancellationToken)
{
RestoreLogger.BeginNominateRestore(_logger, _project.FullPath, restoreInfo);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.md file in the project root for more information.

using Microsoft.VisualStudio.Imaging.Interop;

namespace Microsoft.VisualStudio.ProjectSystem.VS.UI.InfoBarService;

/// <summary>
/// Displays messages in the information bar attached to Visual Studio's main window.
/// </summary>
[ProjectSystemContract(ProjectSystemContractScope.ProjectService, ProjectSystemContractProvider.System)]
internal interface IInfoBarService
{
/// <summary>
/// Shows an information bar with the specified message, image and UI, replacing an existing one
/// with the same message if there is one.
/// </summary>
Task ShowInfoBarAsync(string message, ImageMoniker image, CancellationToken cancellationToken, params InfoBarUI[] items);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.md file in the project root for more information.

namespace Microsoft.VisualStudio.ProjectSystem.VS.UI.InfoBarService;

/// <summary>
/// Represents information bar user interface elements.
/// </summary>
internal class InfoBarUI
{
/// <summary>
/// Initializes a new instance of the <see cref="InfoBarUI"/> class.
/// </summary>
/// <param name="title">The title of the UI _element.</param>
/// <param name="kind">The kind of the UI _element, either <see cref="InfoBarUIKind.Button"/> or <see cref="InfoBarUIKind.Hyperlink"/></param>
/// <param name="action">The <see cref="Action"/> to run when the UI _element is clicked</param>
/// <param name="closeAfterAction"><see langword="true"/> if the information bar should close after <paramref name="action"/> is run.</param>
public InfoBarUI(string title, InfoBarUIKind kind, Action action, bool closeAfterAction = true)
{
Requires.NotNullOrEmpty(title, nameof(title));
Requires.NotNull(action, nameof(action));

Title = title;
Kind = kind;
Action = action;
CloseAfterAction = closeAfterAction;
}

/// <summary>
/// Gets the title of the UI _element.
/// </summary>
public string Title { get; }

/// <summary>
/// Gets the kind of the UI _element, either <see cref="InfoBarUIKind.Button"/> or <see cref="InfoBarUIKind.Hyperlink"/>.
/// </summary>
public InfoBarUIKind Kind { get; }

/// <summary>
/// Gets the <see cref="Action"/> to run when the UI _element is clicked.
/// </summary>
public Action Action { get; }

/// <summary>
/// Gets whether information bar should close after <see cref="Action"/> is run.
/// </summary>
public bool CloseAfterAction { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.md file in the project root for more information.

namespace Microsoft.VisualStudio.ProjectSystem.VS.UI.InfoBarService;

/// <summary>
/// Represents the kind of a <see cref="InfoBarUI"/>.
/// </summary>
internal enum InfoBarUIKind
{
/// <summary>
/// The <see cref="InfoBarUI"/> is a button.
/// </summary>
Button,

/// <summary>
/// The <see cref="InfoBarUI"/> is a hyperlink.
/// </summary>
Hyperlink,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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.md file in the project root for more information.

using Microsoft.VisualStudio.Shell.Interop;

namespace Microsoft.VisualStudio.ProjectSystem.VS.UI.InfoBarService;

internal partial class VsInfoBarService
{
private class InfoBarEntry : IVsInfoBarUIEvents
{
private readonly IVsInfoBarUIElement _element;
private readonly InfoBarUI[] _items;
private readonly Action<InfoBarEntry> _onClose;
private readonly uint _cookie;

public InfoBarEntry(string message, IVsInfoBarUIElement element, InfoBarUI[] items, Action<InfoBarEntry> onClose)
{
Assumes.NotNull(message);
Assumes.NotNull(element);
Assumes.NotNull(items);
Assumes.NotNull(onClose);

Message = message;
Comment thread
ocallesp marked this conversation as resolved.
Outdated
_element = element;
_items = items;
_onClose = onClose;
element.Advise(this, out _cookie);
}

public string Message { get; }

public void Close()
{
_element.Close();
}

public void OnActionItemClicked(IVsInfoBarUIElement element, IVsInfoBarActionItem actionItem)
{
Requires.NotNull(element, nameof(element));
Requires.NotNull(actionItem, nameof(actionItem));

InfoBarUI item = _items.First(i => i.Title == actionItem.Text);
item.Action();

if (item.CloseAfterAction)
{
Close();
}
}

public void OnClosed(IVsInfoBarUIElement element)
{
Requires.NotNull(element, nameof(element));

_element.Unadvise(_cookie);
_onClose(this);
}
}
}

Loading