22using System . Collections . Generic ;
33using System . IO ;
44using System . Text ;
5+ using System . Threading . Tasks ;
56
67namespace 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