Skip to content

Commit f34d1a8

Browse files
Async cache I/O, new tests, and shortcut update
--- - πŸš€ Switched cache read/write to async file I/O for better performance - πŸ§ͺ Added unit tests for FileUtilities and InputValidator - 🧹 Refactored code for clarity and removed unused usings - πŸ“Š Improved status bar progress reporting during optimization - πŸ› οΈ Added OptionsProvider for Tools > Options integration - ⌨️ Changed lossy optimize shortcut to Ctrl+Shift+Alt+L - πŸ“ Updated project file and auto-generated metadata - πŸ—ƒοΈ Updated binary files for package and cache tests
1 parent 213b2cc commit f34d1a8

12 files changed

Lines changed: 885 additions & 124 deletions

β€Žsrc/Commands/OptimizeLosslessCommand.csβ€Ž

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5-
using System.Threading;
6-
using MadsKristensen.ImageOptimizer.Common;
75

86
namespace MadsKristensen.ImageOptimizer
97
{
@@ -89,7 +87,7 @@ private static void AddSupportedFilesFromDirectory(string directoryPath, Concurr
8987
{
9088
// Sequential enumeration is more efficient for I/O-bound operations
9189
// as PLINQ adds overhead without benefit for file system access
92-
var supportedFiles = Directory.EnumerateFiles(directoryPath, Constants.AllFilesPattern, SearchOption.AllDirectories)
90+
IEnumerable<string> supportedFiles = Directory.EnumerateFiles(directoryPath, Constants.AllFilesPattern, SearchOption.AllDirectories)
9391
.Where(Compressor.IsFileSupported);
9492

9593
foreach (var file in supportedFiles)

β€Žsrc/Commands/WorkspaceOptimizeCommand.csβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.ComponentModel.Composition;
44
using System.IO;
55
using System.Linq;
6-
using MadsKristensen.ImageOptimizer.Common;
76
using Microsoft.VisualStudio;
87
using Microsoft.VisualStudio.OLE.Interop;
98
using Microsoft.VisualStudio.Workspace.VSIntegration.UI;
@@ -38,7 +37,7 @@ public class WorkspaceOptimizeCommand : IWorkspaceCommandHandler
3837
{
3938
/// <inheritdoc/>
4039
public bool IgnoreOnMultiselect => false;
41-
40+
4241
/// <inheritdoc/>
4342
public int Priority => 100;
4443

@@ -79,7 +78,7 @@ private static IEnumerable<string> GetImageFiles(List<WorkspaceVisualNodeBase> s
7978
try
8079
{
8180
// Sequential enumeration is more efficient for I/O-bound operations
82-
var images = Directory.EnumerateFiles(folder.FullPath, Constants.AllFilesPattern, SearchOption.AllDirectories)
81+
IEnumerable<string> images = Directory.EnumerateFiles(folder.FullPath, Constants.AllFilesPattern, SearchOption.AllDirectories)
8382
.Where(Compressor.IsFileSupported);
8483

8584
foreach (var image in images)

β€Žsrc/Compression/Cache.csβ€Ž

Lines changed: 174 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Text;
5+
using System.Threading.Tasks;
56

67
namespace MadsKristensen.ImageOptimizer
78
{
@@ -94,7 +95,7 @@ internal bool ContainsFile(string filePath)
9495
}
9596

9697
/// <summary>
97-
/// Saves the cache to disk asynchronously.
98+
/// Saves the cache to disk asynchronously using async file I/O.
9899
/// </summary>
99100
public async Task SaveToDiskAsync()
100101
{
@@ -103,16 +104,40 @@ public async Task SaveToDiskAsync()
103104
return;
104105
}
105106

106-
await Task.Run(() =>
107+
// Use async file I/O for better performance
108+
await SaveToDiskInternalAsync();
109+
}
110+
111+
private void SaveToDiskInternal()
112+
{
113+
try
107114
{
108-
lock (_saveLock)
115+
if (!_cacheFile.Directory.Exists)
116+
{
117+
_cacheFile.Directory.Create();
118+
}
119+
120+
// Use StringBuilder for better performance with large caches
121+
var sb = new StringBuilder(_cache.Count * 50); // Estimate average line length
122+
foreach (KeyValuePair<string, long> kvp in _cache)
109123
{
110-
SaveToDiskInternal();
124+
_ = sb.AppendLine($"{kvp.Key}|{kvp.Value}");
111125
}
112-
});
126+
127+
// Write all content at once using async I/O
128+
File.WriteAllText(_cacheFile.FullName, sb.ToString());
129+
}
130+
catch (Exception ex)
131+
{
132+
ex.LogAsync().FireAndForget();
133+
}
113134
}
114135

115-
private void SaveToDiskInternal()
136+
137+
/// <summary>
138+
/// Saves the cache to disk using async file I/O.
139+
/// </summary>
140+
private async Task SaveToDiskInternalAsync()
116141
{
117142
try
118143
{
@@ -122,103 +147,153 @@ private void SaveToDiskInternal()
122147
}
123148

124149
// Use StringBuilder for better performance with large caches
125-
var sb = new StringBuilder(_cache.Count * 50); // Estimate average line length
150+
var sb = new StringBuilder(_cache.Count * 50);
126151
foreach (KeyValuePair<string, long> kvp in _cache)
127152
{
128153
_ = sb.AppendLine($"{kvp.Key}|{kvp.Value}");
129154
}
130155

131-
// Write all content at once instead of line by line
132-
File.WriteAllText(_cacheFile.FullName, sb.ToString());
133-
}
134-
catch (Exception ex)
135-
{
136-
ex.LogAsync().FireAndForget();
137-
}
138-
}
139-
140-
private ConcurrentDictionary<string, long> ReadCacheFromDisk()
141-
{
142-
var dic = new ConcurrentDictionary<string, long>();
143-
144-
if (_cacheFile?.FullName == null || !_cacheFile.Exists)
145-
{
146-
return dic;
147-
}
148-
149-
try
150-
{
151-
var content = File.ReadAllText(_cacheFile.FullName);
152-
if (string.IsNullOrEmpty(content))
153-
{
154-
return dic;
155-
}
156-
157-
var lines = content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
158-
159-
foreach (var line in lines)
160-
{
161-
var separatorIndex = line.LastIndexOf('|');
162-
if (separatorIndex <= 0 || separatorIndex == line.Length - 1)
163-
{
164-
continue;
165-
}
166-
167-
var filePath = line.Substring(0, separatorIndex);
168-
var lengthStr = line.Substring(separatorIndex + 1);
169-
170-
if (long.TryParse(lengthStr, out var length))
171-
{
172-
dic[filePath] = length;
173-
}
174-
}
175-
}
176-
catch (Exception ex)
177-
{
178-
// If cache is corrupted, start fresh and log the error
179-
ex.LogAsync().FireAndForget();
180-
return new ConcurrentDictionary<string, long>();
181-
}
182-
183-
return dic;
184-
}
185-
186-
/// <summary>
187-
/// Determines the cache file path based on the solution/folder structure.
188-
/// </summary>
189-
internal FileInfo LoadCacheFileName(string fileName)
190-
{
191-
if (string.IsNullOrEmpty(fileName))
192-
{
193-
return null;
194-
}
195-
196-
try
197-
{
198-
var file = new FileInfo(fileName);
199-
DirectoryInfo directory = file.Directory;
200-
201-
while (directory != null)
202-
{
203-
var vsDirPath = Path.Combine(directory.FullName, Constants.VsDirectoryName);
204-
205-
if (Directory.Exists(vsDirPath))
206-
{
207-
var cacheFileName = _type is CompressionType.Lossy
208-
? Constants.LossyCacheFileName
209-
: Constants.LosslessCacheFileName;
210-
return new FileInfo(Path.Combine(vsDirPath, Vsix.Name, cacheFileName));
211-
}
212-
213-
directory = directory.Parent;
214-
}
215-
}
216-
catch (Exception ex)
217-
{
218-
ex.LogAsync().FireAndForget();
219-
}
220-
221-
return null;
222-
}
156+
// Use async StreamWriter for .NET Framework compatibility
157+
using (var writer = new StreamWriter(_cacheFile.FullName, false, Encoding.UTF8))
158+
{
159+
await writer.WriteAsync(sb.ToString()).ConfigureAwait(false);
160+
}
161+
}
162+
catch (Exception ex)
163+
{
164+
ex.LogAsync().FireAndForget();
165+
}
166+
}
167+
168+
private ConcurrentDictionary<string, long> ReadCacheFromDisk()
169+
{
170+
var dic = new ConcurrentDictionary<string, long>();
171+
172+
if (_cacheFile?.FullName == null || !_cacheFile.Exists)
173+
{
174+
return dic;
175+
}
176+
177+
try
178+
{
179+
var content = File.ReadAllText(_cacheFile.FullName);
180+
if (string.IsNullOrEmpty(content))
181+
{
182+
return dic;
183+
}
184+
185+
ParseCacheContent(content, dic);
186+
}
187+
catch (Exception ex)
188+
{
189+
// If cache is corrupted, start fresh and log the error
190+
ex.LogAsync().FireAndForget();
191+
return new ConcurrentDictionary<string, long>();
192+
}
193+
194+
return dic;
195+
}
196+
197+
/// <summary>
198+
/// Reads the cache from disk using async file I/O.
199+
/// </summary>
200+
internal async Task<ConcurrentDictionary<string, long>> ReadCacheFromDiskAsync()
201+
{
202+
var dic = new ConcurrentDictionary<string, long>();
203+
204+
if (_cacheFile?.FullName == null || !_cacheFile.Exists)
205+
{
206+
return dic;
207+
}
208+
209+
try
210+
{
211+
// Use async StreamReader for .NET Framework compatibility
212+
string content;
213+
using (var reader = new StreamReader(_cacheFile.FullName, Encoding.UTF8))
214+
{
215+
content = await reader.ReadToEndAsync().ConfigureAwait(false);
216+
}
217+
218+
if (string.IsNullOrEmpty(content))
219+
{
220+
return dic;
221+
}
222+
223+
ParseCacheContent(content, dic);
224+
}
225+
catch (Exception ex)
226+
{
227+
ex.LogAsync().FireAndForget();
228+
return new ConcurrentDictionary<string, long>();
229+
}
230+
231+
232+
233+
return dic;
234+
}
235+
236+
/// <summary>
237+
/// Parses cache content into a dictionary.
238+
/// </summary>
239+
private static void ParseCacheContent(string content, ConcurrentDictionary<string, long> dic)
240+
{
241+
var lines = content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
242+
243+
foreach (var line in lines)
244+
{
245+
var separatorIndex = line.LastIndexOf('|');
246+
if (separatorIndex <= 0 || separatorIndex == line.Length - 1)
247+
{
248+
continue;
249+
}
250+
251+
var filePath = line.Substring(0, separatorIndex);
252+
var lengthStr = line.Substring(separatorIndex + 1);
253+
254+
if (long.TryParse(lengthStr, out var length))
255+
{
256+
dic[filePath] = length;
257+
}
258+
}
259+
}
260+
261+
/// <summary>
262+
/// Determines the cache file path based on the solution/folder structure.
263+
/// </summary>
264+
internal FileInfo LoadCacheFileName(string fileName)
265+
{
266+
if (string.IsNullOrEmpty(fileName))
267+
{
268+
return null;
269+
}
270+
271+
try
272+
{
273+
var file = new FileInfo(fileName);
274+
DirectoryInfo directory = file.Directory;
275+
276+
while (directory != null)
277+
{
278+
var vsDirPath = Path.Combine(directory.FullName, Constants.VsDirectoryName);
279+
280+
if (Directory.Exists(vsDirPath))
281+
{
282+
var cacheFileName = _type is CompressionType.Lossy
283+
? Constants.LossyCacheFileName
284+
: Constants.LosslessCacheFileName;
285+
return new FileInfo(Path.Combine(vsDirPath, Vsix.Name, cacheFileName));
223286
}
287+
288+
directory = directory.Parent;
224289
}
290+
}
291+
catch (Exception ex)
292+
{
293+
ex.LogAsync().FireAndForget();
294+
}
295+
296+
return null;
297+
}
298+
}
299+
}

0 commit comments

Comments
Β (0)