Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting buffer size of FileStream outside constructor #46489

Closed
EatonZ opened this issue Dec 31, 2020 · 10 comments
Closed

Allow setting buffer size of FileStream outside constructor #46489

EatonZ opened this issue Dec 31, 2020 · 10 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.IO
Milestone

Comments

@EatonZ
Copy link
Contributor

EatonZ commented Dec 31, 2020

Background and Motivation

When initializing a new FileStream, you have the option of specifying a buffer size only when initializing it (bufferSize). This is fine for the majority of cases, but I have a unique use-case that has required me to add a hacky workaround, and I would like to propose a better solution.

My use-case is a class inheriting from FileStream called DiskStream. Its constructor calls the base constructor with an opened SafeFileHandle:

public DiskStream(string PhysicalDrive, Native.DesiredAccesses Access = Native.DesiredAccesses.GENERIC_WRITE | Native.DesiredAccesses.GENERIC_READ, FileShare Share = FileShare.Read, Native.FileFlags Flags = Native.FileFlags.FILE_FLAG_NO_BUFFERING) :
base(DoCreateFile(PhysicalDrive, Access, Share, Flags), DetermineFileAccess(Access)) //DoCreateFile returns a SafeFileHandle, and DetermineFileAccess a FileAccess.
{
    ...

bufferSize could be specified as the third param when calling the base, but the problem is, at that point, I do not know what it is. It is only when my constructor runs, that my code can figure it out using the base FileStream's now-populated SafeFileHandle:

public DiskStream(string PhysicalDrive, Native.DesiredAccesses Access = Native.DesiredAccesses.GENERIC_WRITE | Native.DesiredAccesses.GENERIC_READ, FileShare Share = FileShare.Read, Native.FileFlags Flags = Native.FileFlags.FILE_FLAG_NO_BUFFERING) :
base(DoCreateFile(PhysicalDrive, Access, Share, Flags), DetermineFileAccess(Access))
{
    <PhysicalSectorSize is obtained utilizing SafeFileHandle>
    //Set the FileStream buffer size. Have to set it this way because there's currently no other way. This is important when the physical sector size is 512. The default buffer size is 4096, meaning that FileStream will read 8 sectors when the physical sector size is 512, instead of 1, which is what is intended. This will cause issues when operating at the end of disks, and may cause alignment-related IO errors in general.
    GetType().BaseType.GetField("_bufferLength", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(this, (int)PhysicalSectorSize);
    ...

Without an alternative method to set the buffer size outside of the constructor, my only apparent option is reflection, which is prone to break at any time if the non-public _bufferLength field is removed or renamed.

Proposed API

In FileStream:

public void SetBufferSize(int bufferSize) => _bufferLength = bufferSize;

Usage Examples

The hacky code I posted above could be cleaned up to not use reflection:

public DiskStream(string PhysicalDrive, Native.DesiredAccesses Access = Native.DesiredAccesses.GENERIC_WRITE | Native.DesiredAccesses.GENERIC_READ, FileShare Share = FileShare.Read, Native.FileFlags Flags = Native.FileFlags.FILE_FLAG_NO_BUFFERING) :
base(DoCreateFile(PhysicalDrive, Access, Share, Flags), DetermineFileAccess(Access))
{
    <PhysicalSectorSize is obtained utilizing SafeFileHandle>
    SetBufferSize((int)PhysicalSectorSize);
    ...

Much cleaner, and no risk of breakage!

Alternative Designs

Replace FileStream's private int _bufferLength; with a public property.

Risks

The proposed API may be controversial. Changing the buffer size after a FileStream has started to be used may cause problems, but from what I gather, after reviewing FileStream's source code, setting _buffer to null after changing its size may make SetBufferSize safe to use at any point during a FileStream's life.

@EatonZ EatonZ added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Dec 31, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.IO untriaged New issue has not been triaged by the area owner labels Dec 31, 2020
@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Dec 31, 2020

How about a delegating constructor in your class:

public DiskStream(string PhysicalDrive, Native.DesiredAccesses Access = Native.DesiredAccesses.GENERIC_WRITE | Native.DesiredAccesses.GENERIC_READ, FileShare Share = FileShare.Read, Native.FileFlags Flags = Native.FileFlags.FILE_FLAG_NO_BUFFERING) :
this(DoCreateFile(PhysicalDrive, Access, Share, Flags), DetermineFileAccess(Access))
{
}

private DiskStream(SafeFileHandle handle, FileAccess access) : base(handle, access, GetPhysicalSectorSize(handle))
{
}

private static int GetPhysicalSectorSize(SafeFileHandle handle)
{
    try 
    {
        <PhysicalSectorSize is obtained utilizing SafeFileHandle>
    }
    catch
    {
        // Closing the handle here is somewhat ugly
        // but the constructor that calls this method
        // is not able to do that itself.
        handle.Dispose();
        throw;
    }
}

That would avoid reflection, at least.

Related: dotnet/csharplang#377, dotnet/csharplang#2335.

@EatonZ
Copy link
Contributor Author

EatonZ commented Dec 31, 2020

Hi @KalleOlaviNiemitalo, clever idea, but that won't quite do it for me because I also use that sector size value to set a few other non-static fields in the DiskStream instance. It wouldn't be possible to set those in your example GetPhysicalSectorSize, or in second constructor (the sector size would have to be obtained again).

@KalleOlaviNiemitalo
Copy link

KalleOlaviNiemitalo commented Dec 31, 2020

I think you could stash the sector size by passing it from the static method to the private constructor in a struct or tuple:

public DiskStream(
    string PhysicalDrive,
    Native.DesiredAccesses Access = Native.DesiredAccesses.GENERIC_WRITE | Native.DesiredAccesses.GENERIC_READ,
    FileShare Share = FileShare.Read,
    Native.FileFlags Flags = Native.FileFlags.FILE_FLAG_NO_BUFFERING)
: this(PreInit(PhysicalDrive, Access, Share, Flags))
{
}

private DiskStream(
    (SafeFileHandle Handle, FileAccess Access, int SectorSize) args)
: base(args.Handle, args.Access, args.SectorSize)
{
    this.sectorSize = args.SectorSize;
}

private static (SafeFileHandle Handle, FileAccess Access, int SectorSize) PreInit(
    string PhysicalDrive,
    Native.DesiredAccesses Access,
    FileShare Share,
    Native.FileFlags Flags)
{
    // TODO: exception handling
    SafeFileHandle handle = DoCreateFile(PhysicalDrive, Access, Share, Flags);
    FileAccess fileAccess = DetermineFileAccess(Access);
    int sectorSize = GetSectorSize(handle);
    return (handle, fileAccess, sectorSize);
}

Alternatively, use a variable declaration in an out argument as shown in dotnet/csharplang#2335 (comment).

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 2, 2021

@KalleOlaviNiemitalo Hm, seems like an option, but it still seems like allowing setting buffer size would be the cleaner and easier option. Looking forward to hearing if this is something the .NET team would consider, as it could have other uses outside of my use-case.

@En3Tho
Copy link
Contributor

En3Tho commented Jan 2, 2021

Can you just make a static method Create that will return your DiskStream? I don't think constructor should necessarily do all these complex operations, determining buffersizes and so on. Just make a method, calculate all that outside and pass simple args into constructor. Also, your example does not show how changing bufferSize dynamically does help people at all. Like "detect disk slowdown and change bufferSize accordingly to gain some advantage" etc.

@carlossanlop
Copy link
Member

@stephentoub do you know if there could be unintended or dangerous consequences if bufferSize is changed after the FileStream creation?

@carlossanlop carlossanlop removed the untriaged New issue has not been triaged by the area owner label Jan 8, 2021
@carlossanlop carlossanlop added this to the Future milestone Jan 8, 2021
@stephentoub
Copy link
Member

There are a variety of possible problems if it could be changed at any point. For example, all async usage that might be employing a buffer would need to track whether the buffer was in use, as a change in buffer size really means a change in buffer. It also impacts various other optimizations in the implementation, such as the _buffer being stored as part of the PreAllocatedOverlapped, so that would need to be sorted out (it could be as simple as disposing the old one and creating a new one, but it'd require looking into).

I understand the use case, but it is very niche, and at present I don't think it's worth complicating the implementation to support it. There are also lots of other types in .NET that accept such a buffer size, and it's not clear to me why this particular case is special.

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 8, 2021

@En3Tho Something I will check.

@stephentoub I appreciate you taking the time to review this. If you do not want to change this, do you have any other suggestions for a less-hacky workaround that have no been mentioned here?

@stephentoub
Copy link
Member

stephentoub commented Jan 8, 2021

do you have any other suggestions for a less-hacky workaround that have no been mentioned here?

You could wrap rather than derive from FileStream? e.g.

public sealed class DiskStream : Stream
{
    private readonly FileStream _fileStream;
    ...
}

Or you could have a factory method rather than a public type? e.g.

public static FileStream CreateDiskStream(...)
{
    ... // do lots of stuff
    return new FileStream(...);
}

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 12, 2021

Thank you everyone for your thoughts and suggestions. I decided to go with @KalleOlaviNiemitalo's suggestion - using out args and a PreInit method. This works, and is good enough to alleviate my breakage concern.

I agree with @stephentoub's point that other classes use buffer sizes and implementing it only here and not in others would be an odd choice.

I am closing this as my issue is now solved, and it doesn't appear an API change is desirable at this time.

@EatonZ EatonZ closed this as completed Jan 12, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Feb 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.IO
Projects
None yet
Development

No branches or pull requests

6 participants