Skip to content

Commit

Permalink
added max file limit when reading text file from disk.
Browse files Browse the repository at this point in the history
this is added so that we can reduce chance of OOM when user added massive size text file to solution.

current default max is 100MB. but user can override it through hidden reg key if they want to.

if such file is added, FileTextLoader will throw InvalidDataException and Workspace will raise Workspace Failed event.

in VS host case, we will show the failed event in output windows.

made it to use test option service since option is mutable workspace state which affects all other tests.
  • Loading branch information
heejaechang committed Jan 31, 2017
1 parent dee2f95 commit e83cd71
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 1 deletion.
23 changes: 23 additions & 0 deletions src/Compilers/Core/Portable/FileSystem/FileUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,29 @@ internal static DateTime GetFileTimeStamp(string fullPath)
{
return File.GetLastWriteTimeUtc(fullPath);
}
catch (IOException)
{
throw;
}
catch (Exception e)
{
throw new IOException(e.Message, e);
}
}

/// <exception cref="IOException"/>
internal static long GetFileLength(string fullPath)
{
Debug.Assert(PathUtilities.IsAbsolute(fullPath));
try
{
var info = new FileInfo(fullPath);
return info.Length;
}
catch (IOException)
{
throw;
}
catch (Exception e)
{
throw new IOException(e.Message, e);
Expand Down
46 changes: 46 additions & 0 deletions src/Workspaces/Core/Desktop/Workspace/FileTextLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,28 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
internal static class FileTextLoaderOptions
{
/// <summary>
/// Hidden registry key to control maximum size of a text file we will read into memory.
/// we have this option to reduce a chance of OOM when user adds massive size files to the solution.
/// Default threshold is 100MB which came from some internal data on big files and some discussion.
///
/// User can override default value by setting DWORD value on FileLengthThreshold in
/// "[VS HIVE]\Roslyn\Internal\Performance\Text"
/// </summary>
[ExportOption]
internal static readonly Option<long> FileLengthThreshold = new Option<long>(nameof(FileTextLoaderOptions), nameof(FileLengthThreshold), defaultValue: 100 * 1024 * 1024,
storageLocations: new LocalUserProfileStorageLocation(@"Roslyn\Internal\Performance\Text\FileLengthThreshold"));
}

[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public class FileTextLoader : TextLoader
{
Expand Down Expand Up @@ -69,6 +86,8 @@ protected virtual SourceText CreateText(Stream stream, Workspace workspace)
/// <exception cref="IOException"></exception>
public override async Task<TextAndVersion> LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
{
ValidateFileLength(workspace, _path);

DateTime prevLastWriteTime = FileUtilities.GetFileTimeStamp(_path);

TextAndVersion textAndVersion;
Expand Down Expand Up @@ -168,6 +187,8 @@ public override async Task<TextAndVersion> LoadTextAndVersionAsync(Workspace wor

internal override TextAndVersion LoadTextAndVersionSynchronously(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
{
ValidateFileLength(workspace, _path);

DateTime prevLastWriteTime = FileUtilities.GetFileTimeStamp(_path);

TextAndVersion textAndVersion;
Expand Down Expand Up @@ -198,5 +219,30 @@ private string GetDebuggerDisplay()
{
return nameof(Path) + " = " + Path;
}

private static void ValidateFileLength(Workspace workspace, string path)
{
// Validate file length is under our threshold.
// Otherwise, rather than reading the content into the memory, we will throw
// InvalidDataException to caller of FileTextLoader.LoadText to deal with
// the situation.
//
// check this (http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Workspace/Solution/TextDocumentState.cs,132)
// to see how workspace deal with exception from FileTextLoader. other consumer can handle the exception differently
var fileLength = FileUtilities.GetFileLength(path);
var threshold = workspace.Options.GetOption(FileTextLoaderOptions.FileLengthThreshold);
if (fileLength > threshold)
{
// log max file length which will log to VS telemetry in VS host
Logger.Log(FunctionId.FileTextLoader_FileLengthThresholdExceeded, KeyValueLogMessage.Create(m =>
{
m["FileLength"] = fileLength;
m["Ext"] = PathUtilities.GetExtension(path);
}));

var message = string.Format(WorkspacesResources.File_0_size_of_1_exceeds_maximum_allowed_size_of_2, path, fileLength, threshold);
throw new InvalidDataException(message);
}
}
}
}
1 change: 1 addition & 0 deletions src/Workspaces/Core/Portable/Log/FunctionId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,5 +361,6 @@ internal enum FunctionId
JsonRpcSession_RequestAssetAsync,
SolutionSynchronizationService_GetRemotableData,
AssetService_SynchronizeProjectAssetsAsync,
FileTextLoader_FileLengthThresholdExceeded,
}
}
9 changes: 9 additions & 0 deletions src/Workspaces/Core/Portable/WorkspacesResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Workspaces/Core/Portable/WorkspacesResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,7 @@
<data name="The_first_word_0_must_begin_with_a_lower_case_character" xml:space="preserve">
<value>The first word, '{0}', must begin with a lower case character</value>
</data>
<data name="File_0_size_of_1_exceeds_maximum_allowed_size_of_2" xml:space="preserve">
<value>File '{0}' size of {1} exceeds maximum allowed size of {2}</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Options.Providers;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.UnitTests
{
[ExportWorkspaceServiceFactory(typeof(IOptionService), ServiceLayer.Host), Shared]
internal class TestOptionsServiceFactory : IWorkspaceServiceFactory
{
private readonly ImmutableArray<Lazy<IOptionProvider>> _providers;

[ImportingConstructor]
public TestOptionsServiceFactory(
[ImportMany] IEnumerable<Lazy<IOptionProvider>> optionProviders)
{
_providers = optionProviders.ToImmutableArray();
}

public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
{
// give out new option service per workspace
return new OptionServiceFactory.OptionService(
new GlobalOptionService(_providers, SpecializedCollections.EmptyEnumerable<Lazy<IOptionPersister>>()),
workspaceServices);
}
}
}
3 changes: 2 additions & 1 deletion src/Workspaces/CoreTest/ServicesTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
<Compile Include="Execution\SnapshotSerializationTestBase.cs" />
<Compile Include="Execution\SnapshotSerializationTests.cs" />
<Compile Include="ExtensionOrdererTests.cs" />
<Compile Include="Host\WorkspaceServices\TestOptionsServiceFactory.cs" />
<Compile Include="Host\WorkspaceServices\TestProjectCacheService.cs" />
<Compile Include="Host\WorkspaceServices\TestTemporaryStorageService.cs" />
<Compile Include="LinkedFileDiffMerging\LinkedFileDiffMergingTests.TextMerging.cs" />
Expand Down Expand Up @@ -158,4 +159,4 @@
</None>
</ItemGroup>
<Import Project="..\..\..\build\Targets\Imports.targets" />
</Project>
</Project>
48 changes: 48 additions & 0 deletions src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.VisualBasic;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using CS = Microsoft.CodeAnalysis.CSharp;

Expand Down Expand Up @@ -1417,6 +1418,53 @@ public void TestProjectCompletenessWithMultipleProjects()
Assert.True(transitivelyDependsOnNormalProjects.HasSuccessfullyLoadedAsync().Result);
}

[Fact]
public async Task TestMassiveFileSize()
{
// set max file length to 1 bytes
var maxLength = 1;
var workspace = new AdhocWorkspace(TestHost.Services, ServiceLayer.Host);
workspace.Options = workspace.Options.WithChangedOption(FileTextLoaderOptions.FileLengthThreshold, maxLength);

using (var root = new TempRoot())
{
var file = root.CreateFile(prefix: "massiveFile", extension: ".cs").WriteAllText("hello");

var loader = new FileTextLoader(file.Path, Encoding.UTF8);
var textLength = FileUtilities.GetFileLength(file.Path);

var expected = string.Format(WorkspacesResources.File_0_size_of_1_exceeds_maximum_allowed_size_of_2, file.Path, textLength, maxLength);
var exceptionThrown = false;

try
{
// test async one
var unused = await loader.LoadTextAndVersionAsync(workspace, DocumentId.CreateNewId(ProjectId.CreateNewId()), CancellationToken.None);
}
catch (InvalidDataException ex)
{
exceptionThrown = true;
Assert.Equal(expected, ex.Message);
}

Assert.True(exceptionThrown);

exceptionThrown = false;
try
{
// test sync one
var unused = loader.LoadTextAndVersionSynchronously(workspace, DocumentId.CreateNewId(ProjectId.CreateNewId()), CancellationToken.None);
}
catch (InvalidDataException ex)
{
exceptionThrown = true;
Assert.Equal(expected, ex.Message);
}

Assert.True(exceptionThrown);
}
}

private static void GetMultipleProjects(
out Project csBrokenProject,
out Project vbNormalProject,
Expand Down

0 comments on commit e83cd71

Please sign in to comment.