Skip to content

Commit 5b1873e

Browse files
committed
Use FileOptions.Asynchronous when doing async IO
1 parent 2738b3a commit 5b1873e

File tree

9 files changed

+161
-50
lines changed

9 files changed

+161
-50
lines changed

src/ImageSharp/IO/IFileSystem.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.IO;
@@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO;
99
internal interface IFileSystem
1010
{
1111
/// <summary>
12-
/// Returns a readable stream as defined by the path.
12+
/// Opens a file as defined by the path and returns it as a readable stream.
1313
/// </summary>
1414
/// <param name="path">Path to the file to open.</param>
15-
/// <returns>A stream representing the file to open.</returns>
15+
/// <returns>A stream representing the opened file.</returns>
1616
Stream OpenRead(string path);
1717

1818
/// <summary>
19-
/// Creates or opens a file and returns it as a writable stream as defined by the path.
19+
/// Opens a file as defined by the path and returns it as a readable stream
20+
/// that can be used for asynchronous reading.
2021
/// </summary>
2122
/// <param name="path">Path to the file to open.</param>
22-
/// <returns>A stream representing the file to open.</returns>
23+
/// <returns>A stream representing the opened file.</returns>
24+
Stream OpenReadAsynchronous(string path);
25+
26+
/// <summary>
27+
/// Creates or opens a file as defined by the path and returns it as a writable stream.
28+
/// </summary>
29+
/// <param name="path">Path to the file to open.</param>
30+
/// <returns>A stream representing the opened file.</returns>
2331
Stream Create(string path);
32+
33+
/// <summary>
34+
/// Creates or opens a file as defined by the path and returns it as a writable stream
35+
/// that can be used for asynchronous reading and writing.
36+
/// </summary>
37+
/// <param name="path">Path to the file to open.</param>
38+
/// <returns>A stream representing the opened file.</returns>
39+
Stream CreateAsynchronous(string path);
2440
}

src/ImageSharp/IO/LocalFileSystem.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.IO;
@@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem
1111
/// <inheritdoc/>
1212
public Stream OpenRead(string path) => File.OpenRead(path);
1313

14+
/// <inheritdoc/>
15+
public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions
16+
{
17+
Mode = FileMode.Open,
18+
Access = FileAccess.Read,
19+
Share = FileShare.Read,
20+
Options = FileOptions.Asynchronous,
21+
});
22+
1423
/// <inheritdoc/>
1524
public Stream Create(string path) => File.Create(path);
25+
26+
/// <inheritdoc/>
27+
public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions
28+
{
29+
Mode = FileMode.Create,
30+
Access = FileAccess.ReadWrite,
31+
Share = FileShare.None,
32+
Options = FileOptions.Asynchronous,
33+
});
1634
}

src/ImageSharp/Image.FromFile.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static async Task<IImageFormat> DetectFormatAsync(
7272
{
7373
Guard.NotNull(options, nameof(options));
7474

75-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
75+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
7676
return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false);
7777
}
7878

@@ -144,7 +144,7 @@ public static async Task<ImageInfo> IdentifyAsync(
144144
CancellationToken cancellationToken = default)
145145
{
146146
Guard.NotNull(options, nameof(options));
147-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
147+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
148148
return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false);
149149
}
150150

@@ -214,7 +214,7 @@ public static async Task<Image> LoadAsync(
214214
string path,
215215
CancellationToken cancellationToken = default)
216216
{
217-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
217+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
218218
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
219219
}
220220

@@ -291,7 +291,7 @@ public static async Task<Image<TPixel>> LoadAsync<TPixel>(
291291
Guard.NotNull(options, nameof(options));
292292
Guard.NotNull(path, nameof(path));
293293

294-
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
294+
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
295295
return await LoadAsync<TPixel>(options, stream, cancellationToken).ConfigureAwait(false);
296296
}
297297
}

src/ImageSharp/ImageExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static async Task SaveAsync(
7070
Guard.NotNull(path, nameof(path));
7171
Guard.NotNull(encoder, nameof(encoder));
7272

73-
using Stream fs = source.GetConfiguration().FileSystem.Create(path);
73+
await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path);
7474
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
7575
}
7676

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
using SixLabors.ImageSharp.IO;
@@ -11,36 +11,97 @@ public class LocalFileSystemTests
1111
public void OpenRead()
1212
{
1313
string path = Path.GetTempFileName();
14-
string testData = Guid.NewGuid().ToString();
15-
File.WriteAllText(path, testData);
14+
try
15+
{
16+
string testData = Guid.NewGuid().ToString();
17+
File.WriteAllText(path, testData);
1618

17-
var fs = new LocalFileSystem();
19+
LocalFileSystem fs = new();
1820

19-
using (var r = new StreamReader(fs.OpenRead(path)))
20-
{
21-
string data = r.ReadToEnd();
21+
using (Stream stream = fs.OpenRead(path))
22+
using (StreamReader reader = new(stream))
23+
{
24+
string data = reader.ReadToEnd();
2225

23-
Assert.Equal(testData, data);
26+
Assert.Equal(testData, data);
27+
}
28+
}
29+
finally
30+
{
31+
File.Delete(path);
2432
}
33+
}
2534

26-
File.Delete(path);
35+
[Fact]
36+
public async Task OpenReadAsynchronous()
37+
{
38+
string path = Path.GetTempFileName();
39+
try
40+
{
41+
string testData = Guid.NewGuid().ToString();
42+
File.WriteAllText(path, testData);
43+
44+
LocalFileSystem fs = new();
45+
46+
await using (Stream stream = fs.OpenReadAsynchronous(path))
47+
using (StreamReader reader = new(stream))
48+
{
49+
string data = await reader.ReadToEndAsync();
50+
51+
Assert.Equal(testData, data);
52+
}
53+
}
54+
finally
55+
{
56+
File.Delete(path);
57+
}
2758
}
2859

2960
[Fact]
3061
public void Create()
3162
{
3263
string path = Path.GetTempFileName();
33-
string testData = Guid.NewGuid().ToString();
34-
var fs = new LocalFileSystem();
64+
try
65+
{
66+
string testData = Guid.NewGuid().ToString();
67+
LocalFileSystem fs = new();
3568

36-
using (var r = new StreamWriter(fs.Create(path)))
69+
using (Stream stream = fs.Create(path))
70+
using (StreamWriter writer = new(stream))
71+
{
72+
writer.Write(testData);
73+
}
74+
75+
string data = File.ReadAllText(path);
76+
Assert.Equal(testData, data);
77+
}
78+
finally
3779
{
38-
r.Write(testData);
80+
File.Delete(path);
3981
}
82+
}
4083

41-
string data = File.ReadAllText(path);
42-
Assert.Equal(testData, data);
84+
[Fact]
85+
public async Task CreateAsynchronous()
86+
{
87+
string path = Path.GetTempFileName();
88+
try
89+
{
90+
string testData = Guid.NewGuid().ToString();
91+
LocalFileSystem fs = new();
4392

44-
File.Delete(path);
93+
await using (Stream stream = fs.CreateAsynchronous(path))
94+
using (StreamWriter writer = new(stream))
95+
{
96+
await writer.WriteAsync(testData);
97+
}
98+
99+
string data = File.ReadAllText(path);
100+
Assert.Equal(testData, data);
101+
}
102+
finally
103+
{
104+
File.Delete(path);
105+
}
45106
}
46107
}

tests/ImageSharp.Tests/Image/ImageSaveTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public ImageSaveTests()
4444
[Fact]
4545
public void SavePath()
4646
{
47-
var stream = new MemoryStream();
47+
using MemoryStream stream = new();
4848
this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream);
4949
this.image.Save("path.png");
5050

@@ -54,7 +54,7 @@ public void SavePath()
5454
[Fact]
5555
public void SavePathWithEncoder()
5656
{
57-
var stream = new MemoryStream();
57+
using MemoryStream stream = new();
5858
this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream);
5959

6060
this.image.Save("path.jpg", this.encoderNotInFormat.Object);
@@ -73,7 +73,7 @@ public void ToBase64String()
7373
[Fact]
7474
public void SaveStreamWithMime()
7575
{
76-
var stream = new MemoryStream();
76+
using MemoryStream stream = new();
7777
this.image.Save(stream, this.localImageFormat.Object);
7878

7979
this.encoder.Verify(x => x.Encode(this.image, stream));
@@ -82,7 +82,7 @@ public void SaveStreamWithMime()
8282
[Fact]
8383
public void SaveStreamWithEncoder()
8484
{
85-
var stream = new MemoryStream();
85+
using MemoryStream stream = new();
8686

8787
this.image.Save(stream, this.encoderNotInFormat.Object);
8888

tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ protected ImageLoadTestBase()
122122
Stream StreamFactory() => this.DataStream;
123123

124124
this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory);
125+
this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory);
125126
this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory);
126127
this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object;
127128
this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem;
@@ -132,6 +133,11 @@ public void Dispose()
132133
// Clean up the global object;
133134
this.localStreamReturnImageRgba32?.Dispose();
134135
this.localStreamReturnImageAgnostic?.Dispose();
136+
137+
if (this.dataStreamLazy.IsValueCreated)
138+
{
139+
this.dataStreamLazy.Value.Dispose();
140+
}
135141
}
136142

137143
protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker);
Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
#nullable enable
5+
46
namespace SixLabors.ImageSharp.Tests;
57

68
/// <summary>
79
/// A test image file.
810
/// </summary>
911
public class TestFileSystem : ImageSharp.IO.IFileSystem
1012
{
11-
private readonly Dictionary<string, Func<Stream>> fileSystem = new Dictionary<string, Func<Stream>>(StringComparer.OrdinalIgnoreCase);
13+
private readonly Dictionary<string, Func<Stream>> fileSystem = new(StringComparer.OrdinalIgnoreCase);
1214

1315
public void AddFile(string path, Func<Stream> data)
1416
{
@@ -18,35 +20,39 @@ public void AddFile(string path, Func<Stream> data)
1820
}
1921
}
2022

21-
public Stream Create(string path)
23+
public Stream Create(string path) => this.GetStream(path) ?? File.Create(path);
24+
25+
public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
2226
{
23-
// if we have injected a fake file use it instead
24-
lock (this.fileSystem)
25-
{
26-
if (this.fileSystem.ContainsKey(path))
27-
{
28-
Stream stream = this.fileSystem[path]();
29-
stream.Position = 0;
30-
return stream;
31-
}
32-
}
27+
Mode = FileMode.Create,
28+
Access = FileAccess.ReadWrite,
29+
Share = FileShare.None,
30+
Options = FileOptions.Asynchronous,
31+
});
3332

34-
return File.Create(path);
35-
}
33+
public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path);
34+
35+
public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
36+
{
37+
Mode = FileMode.Open,
38+
Access = FileAccess.Read,
39+
Share = FileShare.Read,
40+
Options = FileOptions.Asynchronous,
41+
});
3642

37-
public Stream OpenRead(string path)
43+
private Stream? GetStream(string path)
3844
{
3945
// if we have injected a fake file use it instead
4046
lock (this.fileSystem)
4147
{
42-
if (this.fileSystem.ContainsKey(path))
48+
if (this.fileSystem.TryGetValue(path, out Func<Stream>? streamFactory))
4349
{
44-
Stream stream = this.fileSystem[path]();
50+
Stream stream = streamFactory();
4551
stream.Position = 0;
4652
return stream;
4753
}
4854
}
4955

50-
return File.OpenRead(path);
56+
return null;
5157
}
5258
}

tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem
1313

1414
Stream IFileSystem.Create(string path) => this.stream;
1515

16+
Stream IFileSystem.CreateAsynchronous(string path) => this.stream;
17+
1618
Stream IFileSystem.OpenRead(string path) => this.stream;
19+
20+
Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream;
1721
}

0 commit comments

Comments
 (0)