未验证 提交 fd997c77 编写于 作者: M msftbot[bot] 提交者: GitHub

Merge pull request #41955 from CyrusNajmabadi/dumpCommandLine

Persist command line strings to temporary storage to prevent excess in-memory use.
...@@ -10,7 +10,7 @@ internal partial class AbstractLegacyProject : ICompilerOptionsHostObject ...@@ -10,7 +10,7 @@ internal partial class AbstractLegacyProject : ICompilerOptionsHostObject
{ {
int ICompilerOptionsHostObject.SetCompilerOptions(string compilerOptions, out bool supported) int ICompilerOptionsHostObject.SetCompilerOptions(string compilerOptions, out bool supported)
{ {
VisualStudioProjectOptionsProcessor.CommandLine = compilerOptions; VisualStudioProjectOptionsProcessor.SetCommandLine(compilerOptions);
supported = true; supported = true;
return VSConstants.S_OK; return VSConstants.S_OK;
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
#nullable enable
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO; using System.IO;
...@@ -16,57 +18,91 @@ internal class VisualStudioProjectOptionsProcessor : IDisposable ...@@ -16,57 +18,91 @@ internal class VisualStudioProjectOptionsProcessor : IDisposable
private readonly VisualStudioProject _project; private readonly VisualStudioProject _project;
private readonly HostWorkspaceServices _workspaceServices; private readonly HostWorkspaceServices _workspaceServices;
private readonly ICommandLineParserService _commandLineParserService; private readonly ICommandLineParserService _commandLineParserService;
private readonly ITemporaryStorageService _temporaryStorageService;
/// <summary> /// <summary>
/// Gate to guard all mutable fields in this class. /// Gate to guard all mutable fields in this class.
/// The lock hierarchy means you are allowed to call out of this class and into <see cref="_project"/> while holding the lock. /// The lock hierarchy means you are allowed to call out of this class and into <see cref="_project"/> while holding the lock.
/// </summary> /// </summary>
private readonly object _gate = new object(); private readonly object _gate = new object();
private string _commandLine = "";
/// <summary>
/// A hashed checksum of the last command line we were set to. We use this
/// as a low cost (in terms of memory) way to determine if the command line
/// actually changes and we need to make any downstream updates.
/// </summary>
private Checksum? _commandLineChecksum;
/// <summary>
/// To save space in the managed heap, we dump the entire command-line string to our
/// temp-storage-service. This is helpful as compiler command-lines can grow extremely large
/// (especially in cases with many references).
/// </summary>
/// <remarks>Note: this will be null in the case that the command line is an empty string.</remarks>
private ITemporaryStreamStorage? _commandLineStorage;
private CommandLineArguments _commandLineArgumentsForCommandLine; private CommandLineArguments _commandLineArgumentsForCommandLine;
private string _explicitRuleSetFilePath; private string? _explicitRuleSetFilePath;
private IReferenceCountedDisposable<ICacheEntry<string, IRuleSetFile>> _ruleSetFile = null; private IReferenceCountedDisposable<ICacheEntry<string, IRuleSetFile>>? _ruleSetFile = null;
public VisualStudioProjectOptionsProcessor(VisualStudioProject project, HostWorkspaceServices workspaceServices) public VisualStudioProjectOptionsProcessor(
VisualStudioProject project,
HostWorkspaceServices workspaceServices)
{ {
_project = project ?? throw new ArgumentNullException(nameof(project)); _project = project ?? throw new ArgumentNullException(nameof(project));
_workspaceServices = workspaceServices; _workspaceServices = workspaceServices;
_commandLineParserService = workspaceServices.GetLanguageServices(project.Language).GetRequiredService<ICommandLineParserService>(); _commandLineParserService = workspaceServices.GetLanguageServices(project.Language).GetRequiredService<ICommandLineParserService>();
_temporaryStorageService = workspaceServices.GetRequiredService<ITemporaryStorageService>();
// Set up _commandLineArgumentsForCommandLine to a default. No lock taken since we're in
// the constructor so nothing can race.
// Set up _commandLineArgumentsForCommandLine to a default. No lock taken since we're in the constructor so nothing can race. // Silence NRT warning. This will be initialized by the call below to ReparseCommandLineIfChanged_NoLock.
ReparseCommandLine_NoLock(); _commandLineArgumentsForCommandLine = null!;
ReparseCommandLineIfChanged_NoLock(commandLine: "");
} }
public string CommandLine /// <returns><see langword="true"/> if the command line was updated.</returns>
private bool ReparseCommandLineIfChanged_NoLock(string commandLine)
{ {
get var checksum = Checksum.Create(commandLine);
if (_commandLineChecksum == checksum)
return false;
_commandLineChecksum = checksum;
// Dispose the existing stored command-line and then persist the new one so we can
// recover it later. Only bother persisting things if we have a non-empty string.
_commandLineStorage?.Dispose();
_commandLineStorage = null;
if (commandLine.Length > 0)
{ {
return _commandLine; _commandLineStorage = _temporaryStorageService.CreateTemporaryStreamStorage();
_commandLineStorage.WriteString(commandLine);
} }
set ReparseCommandLine_NoLock(commandLine);
{ return true;
if (value == null)
{
throw new ArgumentNullException(nameof(value));
} }
public void SetCommandLine(string commandLine)
{
if (commandLine == null)
throw new ArgumentNullException(nameof(commandLine));
lock (_gate) lock (_gate)
{ {
if (_commandLine == value) // If we actually got a new command line, then update the project options, otherwise
// we don't need to do anything.
if (ReparseCommandLineIfChanged_NoLock(commandLine))
{ {
return;
}
_commandLine = value;
ReparseCommandLine_NoLock();
UpdateProjectOptions_NoLock(); UpdateProjectOptions_NoLock();
} }
} }
} }
public string ExplicitRuleSetFilePath public string? ExplicitRuleSetFilePath
{ {
get => _explicitRuleSetFilePath; get => _explicitRuleSetFilePath;
...@@ -89,7 +125,7 @@ public string ExplicitRuleSetFilePath ...@@ -89,7 +125,7 @@ public string ExplicitRuleSetFilePath
/// <summary> /// <summary>
/// Returns the active path to the rule set file that is being used by this project, or null if there isn't a rule set file. /// Returns the active path to the rule set file that is being used by this project, or null if there isn't a rule set file.
/// </summary> /// </summary>
public string EffectiveRuleSetFilePath public string? EffectiveRuleSetFilePath
{ {
get get
{ {
...@@ -112,9 +148,9 @@ private void DisposeOfRuleSetFile_NoLock() ...@@ -112,9 +148,9 @@ private void DisposeOfRuleSetFile_NoLock()
} }
} }
private void ReparseCommandLine_NoLock() private void ReparseCommandLine_NoLock(string commandLine)
{ {
var arguments = CommandLineParser.SplitCommandLineIntoArguments(_commandLine, removeHashComments: false); var arguments = CommandLineParser.SplitCommandLineIntoArguments(commandLine, removeHashComments: false);
_commandLineArgumentsForCommandLine = _commandLineParserService.Parse(arguments, Path.GetDirectoryName(_project.FilePath), isInteractive: false, sdkDirectory: null); _commandLineArgumentsForCommandLine = _commandLineParserService.Parse(arguments, Path.GetDirectoryName(_project.FilePath), isInteractive: false, sdkDirectory: null);
} }
...@@ -193,8 +229,10 @@ private void RuleSetFile_UpdatedOnDisk(object sender, EventArgs e) ...@@ -193,8 +229,10 @@ private void RuleSetFile_UpdatedOnDisk(object sender, EventArgs e)
// effective values was potentially done by the act of parsing the command line. Even though the command line didn't change textually, // effective values was potentially done by the act of parsing the command line. Even though the command line didn't change textually,
// the effective result did. Then we call UpdateProjectOptions_NoLock to reapply any values; that will also re-acquire the new ruleset // the effective result did. Then we call UpdateProjectOptions_NoLock to reapply any values; that will also re-acquire the new ruleset
// includes in the IDE so we can be watching for changes again. // includes in the IDE so we can be watching for changes again.
var commandLine = _commandLineStorage?.ReadString() ?? "";
DisposeOfRuleSetFile_NoLock(); DisposeOfRuleSetFile_NoLock();
ReparseCommandLine_NoLock(); ReparseCommandLine_NoLock(commandLine);
UpdateProjectOptions_NoLock(); UpdateProjectOptions_NoLock();
} }
} }
...@@ -203,10 +241,8 @@ private void RuleSetFile_UpdatedOnDisk(object sender, EventArgs e) ...@@ -203,10 +241,8 @@ private void RuleSetFile_UpdatedOnDisk(object sender, EventArgs e)
/// Overridden by derived classes to provide a hook to modify a <see cref="CompilationOptions"/> with any host-provided values that didn't come from /// Overridden by derived classes to provide a hook to modify a <see cref="CompilationOptions"/> with any host-provided values that didn't come from
/// the command line string. /// the command line string.
/// </summary> /// </summary>
protected virtual CompilationOptions ComputeCompilationOptionsWithHostValues(CompilationOptions compilationOptions, IRuleSetFile ruleSetFileOpt) protected virtual CompilationOptions ComputeCompilationOptionsWithHostValues(CompilationOptions compilationOptions, IRuleSetFile? ruleSetFileOpt)
{ => compilationOptions;
return compilationOptions;
}
/// <summary> /// <summary>
/// Override by derived classes to provide a hook to modify a <see cref="ParseOptions"/> with any host-provided values that didn't come from /// Override by derived classes to provide a hook to modify a <see cref="ParseOptions"/> with any host-provided values that didn't come from
...@@ -234,6 +270,7 @@ public void Dispose() ...@@ -234,6 +270,7 @@ public void Dispose()
lock (_gate) lock (_gate)
{ {
DisposeOfRuleSetFile_NoLock(); DisposeOfRuleSetFile_NoLock();
_commandLineStorage?.Dispose();
} }
} }
} }
......
...@@ -139,12 +139,7 @@ public CPSProject(VisualStudioProject visualStudioProject, VisualStudioWorkspace ...@@ -139,12 +139,7 @@ public CPSProject(VisualStudioProject visualStudioProject, VisualStudioWorkspace
public ProjectId Id => _visualStudioProject.Id; public ProjectId Id => _visualStudioProject.Id;
public void SetOptions(string commandLineForOptions) public void SetOptions(string commandLineForOptions)
{ => _visualStudioProjectOptionsProcessor?.SetCommandLine(commandLineForOptions);
if (_visualStudioProjectOptionsProcessor != null)
{
_visualStudioProjectOptionsProcessor.CommandLine = commandLineForOptions;
}
}
public string? DefaultNamespace public string? DefaultNamespace
{ {
......
...@@ -364,11 +364,6 @@ private async Task WriteStreamMaybeAsync(Stream stream, bool useAsync, Cancellat ...@@ -364,11 +364,6 @@ private async Task WriteStreamMaybeAsync(Stream stream, bool useAsync, Cancellat
throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once);
} }
if (stream.Length == 0)
{
throw new ArgumentOutOfRangeException();
}
using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, cancellationToken)) using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, cancellationToken))
{ {
var size = stream.Length; var size = stream.Length;
......
// 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.IO;
namespace Microsoft.CodeAnalysis.Host
{
internal static class ITemporaryStreamStorageExtensions
{
public static void WriteString(this ITemporaryStreamStorage storage, string value)
{
using var stream = SerializableBytes.CreateWritableStream();
using var writer = new StreamWriter(stream);
writer.Write(value);
writer.Flush();
stream.Position = 0;
storage.WriteStream(stream);
}
public static string ReadString(this ITemporaryStreamStorage storage)
{
using var stream = storage.ReadStream();
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
}
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities; using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit; using Xunit;
namespace Microsoft.CodeAnalysis.UnitTests namespace Microsoft.CodeAnalysis.UnitTests
...@@ -113,12 +112,8 @@ public void TestTemporaryStreamStorageExceptions() ...@@ -113,12 +112,8 @@ public void TestTemporaryStreamStorageExceptions()
Assert.Throws<InvalidOperationException>(() => storage.ReadStream()); Assert.Throws<InvalidOperationException>(() => storage.ReadStream());
Assert.Throws<AggregateException>(() => storage.ReadStreamAsync().Result); Assert.Throws<AggregateException>(() => storage.ReadStreamAsync().Result);
// 0 length streams are not allowed
var stream = new MemoryStream();
Assert.Throws<ArgumentOutOfRangeException>(() => storage.WriteStream(stream));
Assert.Throws<AggregateException>(() => storage.WriteStreamAsync(stream).Wait());
// write a normal stream // write a normal stream
var stream = new MemoryStream();
stream.Write(new byte[] { 42 }, 0, 1); stream.Write(new byte[] { 42 }, 0, 1);
stream.Position = 0; stream.Position = 0;
storage.WriteStreamAsync(stream).Wait(); storage.WriteStreamAsync(stream).Wait();
...@@ -128,6 +123,25 @@ public void TestTemporaryStreamStorageExceptions() ...@@ -128,6 +123,25 @@ public void TestTemporaryStreamStorageExceptions()
Assert.Throws<AggregateException>(() => storage.WriteStreamAsync(null).Wait()); Assert.Throws<AggregateException>(() => storage.WriteStreamAsync(null).Wait());
} }
[Fact]
public void TestZeroLengthStreams()
{
var textFactory = new TextFactoryService();
var service = new TemporaryStorageServiceFactory.TemporaryStorageService(textFactory);
var storage = service.CreateTemporaryStreamStorage(CancellationToken.None);
// 0 length streams are allowed
using (var stream1 = new MemoryStream())
{
storage.WriteStream(stream1);
}
using (var stream2 = storage.ReadStream())
{
Assert.Equal(0, stream2.Length);
}
}
[Fact, Trait(Traits.Feature, Traits.Features.Workspace)] [Fact, Trait(Traits.Feature, Traits.Features.Workspace)]
public void TestTemporaryStorageMemoryMappedFileManagement() public void TestTemporaryStorageMemoryMappedFileManagement()
{ {
......
...@@ -22,7 +22,6 @@ internal static class SerializableBytes ...@@ -22,7 +22,6 @@ internal static class SerializableBytes
internal static PooledStream CreateReadableStream(byte[] bytes) internal static PooledStream CreateReadableStream(byte[] bytes)
=> CreateReadableStream(bytes, bytes.Length); => CreateReadableStream(bytes, bytes.Length);
internal static PooledStream CreateReadableStream(byte[] bytes, int length) internal static PooledStream CreateReadableStream(byte[] bytes, int length)
{ {
var stream = CreateWritableStream(); var stream = CreateWritableStream();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册