22// The .NET Foundation licenses this file to you under the MIT license.
33// See the LICENSE file in the project root for more information.
44
5- #nullable disable
6-
75using System ;
86using System . Collections . Concurrent ;
97using System . Collections . Generic ;
1816using Microsoft . CodeAnalysis . AddImport ;
1917using Microsoft . CodeAnalysis . Elfie . Model ;
2018using Microsoft . CodeAnalysis . Shared . Utilities ;
19+ using Roslyn . Utilities ;
2120using static System . FormattableString ;
2221
2322namespace Microsoft . CodeAnalysis . SymbolSearch ;
@@ -267,13 +266,14 @@ private async Task<TimeSpan> DownloadFullDatabaseAsync(FileInfo databaseFileInfo
267266 return ( succeeded : false , failureDelay ) ;
268267 }
269268
270- var bytes = contentBytes ;
269+ var bytes = contentBytes ! ;
270+ AddReferenceDatabase database ;
271271
272272 // Make a database out of that and set it to our in memory database that we'll be
273273 // searching.
274274 try
275275 {
276- CreateAndSetInMemoryDatabase ( bytes ) ;
276+ database = CreateAndSetInMemoryDatabase ( bytes , isBinary : false ) ;
277277 }
278278 catch ( Exception e ) when ( _service . _reportAndSwallowExceptionUnlessCanceled ( e , cancellationToken ) )
279279 {
@@ -288,18 +288,55 @@ private async Task<TimeSpan> DownloadFullDatabaseAsync(FileInfo databaseFileInfo
288288
289289 // Write the file out to disk so we'll have it the next time we launch VS. Do this
290290 // after we set the in-memory instance so we at least have something to search while
291- // we're waiting to write.
292- await WriteDatabaseFileAsync ( databaseFileInfo , bytes , cancellationToken ) . ConfigureAwait ( false ) ;
291+ // we're waiting to write. It's ok if either of these writes don't succeed. If the txt
292+ // file fails to persist, a subsequent VS session will redownload the index and again try
293+ // to persist. If the binary file fails to persist, a subsequent VS session will just load
294+ // the index from the txt file and again try to persist the binary file.
295+ await WriteDatabaseTextFileAsync ( databaseFileInfo , bytes , cancellationToken ) . ConfigureAwait ( false ) ;
296+ await WriteDatabaseBinaryFileAsync ( database , databaseFileInfo , cancellationToken ) . ConfigureAwait ( false ) ;
293297
294298 var delay = _service . _delayService . UpdateSucceededDelay ;
295299 LogInfo ( $ "Processing full database element completed. Update again in { delay } ") ;
296300 return ( succeeded : true , delay ) ;
297301 }
298302
299- private async Task WriteDatabaseFileAsync ( FileInfo databaseFileInfo , byte [ ] bytes , CancellationToken cancellationToken )
303+ private async Task WriteDatabaseTextFileAsync ( FileInfo databaseFileInfo , byte [ ] bytes , CancellationToken cancellationToken )
300304 {
301305 LogInfo ( "Writing database file" ) ;
302306
307+ await WriteDatabaseFileAsync ( databaseFileInfo , new ArraySegment < byte > ( bytes ) , cancellationToken ) . ConfigureAwait ( false ) ;
308+
309+ LogInfo ( "Writing database file completed" ) ;
310+ }
311+
312+ private async Task WriteDatabaseBinaryFileAsync ( AddReferenceDatabase database , FileInfo databaseFileInfo , CancellationToken cancellationToken )
313+ {
314+ using var memoryStream = new MemoryStream ( ) ;
315+ using var writer = new BinaryWriter ( memoryStream ) ;
316+
317+ LogInfo ( "Writing database binary file" ) ;
318+
319+ database . WriteBinary ( writer ) ;
320+ writer . Flush ( ) ;
321+
322+ // Obtain the underlying array from the memory stream. If for some reason this isn't available,
323+ // fall back to reading the stream into a new byte array.
324+ if ( ! memoryStream . TryGetBuffer ( out var arraySegmentBuffer ) )
325+ {
326+ memoryStream . Position = 0 ;
327+
328+ // Read the buffer directly from the memory stream.
329+ arraySegmentBuffer = new ArraySegment < byte > ( memoryStream . ReadAllBytes ( ) ) ;
330+ }
331+
332+ var databaseBinaryFileInfo = GetBinaryFileInfo ( databaseFileInfo ) ;
333+ await WriteDatabaseFileAsync ( databaseBinaryFileInfo , arraySegmentBuffer , cancellationToken ) . ConfigureAwait ( false ) ;
334+
335+ LogInfo ( "Writing database binary file completed" ) ;
336+ }
337+
338+ private async Task WriteDatabaseFileAsync ( FileInfo databaseFileInfo , ArraySegment < byte > bytes , CancellationToken cancellationToken )
339+ {
303340 await RepeatIOAsync (
304341 cancellationToken =>
305342 {
@@ -343,17 +380,19 @@ await RepeatIOAsync(
343380 IOUtilities . PerformIO ( ( ) => _service . _ioService . Delete ( new FileInfo ( tempFilePath ) ) ) ;
344381 }
345382 } , cancellationToken ) . ConfigureAwait ( false ) ;
346-
347- LogInfo ( "Writing database file completed" ) ;
348383 }
349384
385+ private static FileInfo GetBinaryFileInfo ( FileInfo databaseFileInfo )
386+ => new FileInfo ( Path . ChangeExtension ( databaseFileInfo . FullName , ".bin" ) ) ;
387+
350388 private async Task < TimeSpan > PatchLocalDatabaseAsync ( FileInfo databaseFileInfo , CancellationToken cancellationToken )
351389 {
352390 LogInfo ( "Patching local database" ) ;
353391
354392 LogInfo ( "Reading in local database" ) ;
355- // (intentionally not wrapped in IOUtilities. If this throws we want to restart).
356- var databaseBytes = _service . _ioService . ReadAllBytes ( databaseFileInfo . FullName ) ;
393+
394+ var ( databaseBytes , isBinary ) = GetDatabaseBytes ( databaseFileInfo ) ;
395+
357396 LogInfo ( $ "Reading in local database completed. databaseBytes.Length={ databaseBytes . Length } ") ;
358397
359398 // Make a database instance out of those bytes and set is as the current in memory database
@@ -363,7 +402,7 @@ private async Task<TimeSpan> PatchLocalDatabaseAsync(FileInfo databaseFileInfo,
363402 AddReferenceDatabase database ;
364403 try
365404 {
366- database = CreateAndSetInMemoryDatabase ( databaseBytes ) ;
405+ database = CreateAndSetInMemoryDatabase ( databaseBytes , isBinary ) ;
367406 }
368407 catch ( Exception e ) when ( _service . _reportAndSwallowExceptionUnlessCanceled ( e , cancellationToken ) )
369408 {
@@ -381,12 +420,34 @@ private async Task<TimeSpan> PatchLocalDatabaseAsync(FileInfo databaseFileInfo,
381420 LogInfo ( "Downloading and processing patch file: " + serverPath ) ;
382421
383422 var element = await DownloadFileAsync ( serverPath , cancellationToken ) . ConfigureAwait ( false ) ;
384- var delayUntilUpdate = await ProcessPatchXElementAsync ( databaseFileInfo , element , databaseBytes , cancellationToken ) . ConfigureAwait ( false ) ;
423+ var delayUntilUpdate = await ProcessPatchXElementAsync (
424+ databaseFileInfo ,
425+ element ,
426+ // We pass a delegate to get the database bytes so that we can avoid reading the bytes when we don't need them due to no patch to apply.
427+ getDatabaseBytes : ( ) => isBinary ? _service . _ioService . ReadAllBytes ( databaseFileInfo . FullName ) : databaseBytes ,
428+ cancellationToken ) . ConfigureAwait ( false ) ;
385429
386430 LogInfo ( "Downloading and processing patch file completed" ) ;
387431 LogInfo ( "Patching local database completed" ) ;
388432
389433 return delayUntilUpdate ;
434+
435+ ( byte [ ] dataBytes , bool isBinary ) GetDatabaseBytes ( FileInfo databaseFileInfo )
436+ {
437+ var databaseBinaryFileInfo = GetBinaryFileInfo ( databaseFileInfo ) ;
438+
439+ try
440+ {
441+ // First attempt to read from the binary file. If that fails, fall back to the text file.
442+ return ( _service . _ioService . ReadAllBytes ( databaseBinaryFileInfo . FullName ) , isBinary : true ) ;
443+ }
444+ catch ( Exception e ) when ( IOUtilities . IsNormalIOException ( e ) )
445+ {
446+ }
447+
448+ // (intentionally not wrapped in IOUtilities. If this throws we want to restart).
449+ return ( _service . _ioService . ReadAllBytes ( databaseFileInfo . FullName ) , isBinary : false ) ;
450+ }
390451 }
391452
392453 /// <summary>
@@ -395,20 +456,20 @@ private async Task<TimeSpan> PatchLocalDatabaseAsync(FileInfo databaseFileInfo,
395456 /// indicates that our data is corrupt), the exception will bubble up and must be appropriately
396457 /// dealt with by the caller.
397458 /// </summary>
398- private AddReferenceDatabase CreateAndSetInMemoryDatabase ( byte [ ] bytes )
459+ private AddReferenceDatabase CreateAndSetInMemoryDatabase ( byte [ ] bytes , bool isBinary )
399460 {
400- var database = CreateDatabaseFromBytes ( bytes ) ;
461+ var database = CreateDatabaseFromBytes ( bytes , isBinary ) ;
401462 _service . _sourceToDatabase [ _source ] = new AddReferenceDatabaseWrapper ( database ) ;
402463 return database ;
403464 }
404465
405466 private async Task < TimeSpan > ProcessPatchXElementAsync (
406- FileInfo databaseFileInfo , XElement patchElement , byte [ ] databaseBytes , CancellationToken cancellationToken )
467+ FileInfo databaseFileInfo , XElement patchElement , Func < byte [ ] > getDatabaseBytes , CancellationToken cancellationToken )
407468 {
408469 try
409470 {
410471 LogInfo ( "Processing patch element" ) ;
411- var delayUntilUpdate = await TryProcessPatchXElementAsync ( databaseFileInfo , patchElement , databaseBytes , cancellationToken ) . ConfigureAwait ( false ) ;
472+ var delayUntilUpdate = await TryProcessPatchXElementAsync ( databaseFileInfo , patchElement , getDatabaseBytes , cancellationToken ) . ConfigureAwait ( false ) ;
412473 if ( delayUntilUpdate != null )
413474 {
414475 LogInfo ( $ "Processing patch element completed. Update again in { delayUntilUpdate . Value } ") ;
@@ -427,13 +488,19 @@ private async Task<TimeSpan> ProcessPatchXElementAsync(
427488 }
428489
429490 private async Task < TimeSpan ? > TryProcessPatchXElementAsync (
430- FileInfo databaseFileInfo , XElement patchElement , byte [ ] databaseBytes , CancellationToken cancellationToken )
491+ FileInfo databaseFileInfo , XElement patchElement , Func < byte [ ] > getDatabaseBytes , CancellationToken cancellationToken )
431492 {
432493 ParsePatchElement ( patchElement , out var upToDate , out var tooOld , out var patchBytes ) ;
494+ AddReferenceDatabase database ;
433495
434496 if ( upToDate )
435497 {
436498 LogInfo ( "Local version is up to date" ) ;
499+
500+ var databaseBinaryFileInfo = GetBinaryFileInfo ( databaseFileInfo ) ;
501+ if ( ! _service . _ioService . Exists ( databaseBinaryFileInfo ) )
502+ await WriteDatabaseBinaryFileAsync ( _service . _sourceToDatabase [ _source ] . Database , databaseFileInfo , cancellationToken ) . ConfigureAwait ( false ) ;
503+
437504 return _service . _delayService . UpdateSucceededDelay ;
438505 }
439506
@@ -443,22 +510,29 @@ private async Task<TimeSpan> ProcessPatchXElementAsync(
443510 return null ;
444511 }
445512
446- LogInfo ( $ "Got patch. databaseBytes.Length={ databaseBytes . Length } patchBytes.Length={ patchBytes . Length } .") ;
513+ var databaseBytes = getDatabaseBytes ( ) ;
514+ LogInfo ( $ "Got patch. databaseBytes.Length={ databaseBytes . Length } patchBytes.Length={ patchBytes ! . Length } .") ;
447515
448516 // We have patch data. Apply it to our current database bytes to produce the new
449517 // database.
450518 LogInfo ( "Applying patch" ) ;
451519 var finalBytes = _service . _patchService . ApplyPatch ( databaseBytes , patchBytes ) ;
452520 LogInfo ( $ "Applying patch completed. finalBytes.Length={ finalBytes . Length } ") ;
453521
454- CreateAndSetInMemoryDatabase ( finalBytes ) ;
522+ // finalBytes is generated from the current database and the patch, not from the binary file.
523+ database = CreateAndSetInMemoryDatabase ( finalBytes , isBinary : false ) ;
455524
456- await WriteDatabaseFileAsync ( databaseFileInfo , finalBytes , cancellationToken ) . ConfigureAwait ( false ) ;
525+ // Attempt to persist the txt and binary forms of the index. It's ok if either of these writes
526+ // don't succeed. If the txt file fails to persist, a subsequent VS session will redownload the
527+ // index and again try to persist. If the binary file fails to persist, a subsequent VS session
528+ // will just load the index from the txt file and again try to persist the binary file.
529+ await WriteDatabaseTextFileAsync ( databaseFileInfo , finalBytes , cancellationToken ) . ConfigureAwait ( false ) ;
530+ await WriteDatabaseBinaryFileAsync ( database , databaseFileInfo , cancellationToken ) . ConfigureAwait ( false ) ;
457531
458532 return _service . _delayService . UpdateSucceededDelay ;
459533 }
460534
461- private static void ParsePatchElement ( XElement patchElement , out bool upToDate , out bool tooOld , out byte [ ] patchBytes )
535+ private static void ParsePatchElement ( XElement patchElement , out bool upToDate , out bool tooOld , out byte [ ] ? patchBytes )
462536 {
463537 patchBytes = null ;
464538
@@ -486,10 +560,10 @@ private static void ParsePatchElement(XElement patchElement, out bool upToDate,
486560 }
487561 }
488562
489- private AddReferenceDatabase CreateDatabaseFromBytes ( byte [ ] bytes )
563+ private AddReferenceDatabase CreateDatabaseFromBytes ( byte [ ] bytes , bool isBinary )
490564 {
491565 LogInfo ( "Creating database from bytes" ) ;
492- var result = _service . _databaseFactoryService . CreateDatabaseFromBytes ( bytes ) ;
566+ var result = _service . _databaseFactoryService . CreateDatabaseFromBytes ( bytes , isBinary ) ;
493567 LogInfo ( "Creating database from bytes completed" ) ;
494568 return result ;
495569 }
@@ -534,7 +608,7 @@ private async Task<XElement> DownloadFileAsync(string serverPath, CancellationTo
534608 }
535609
536610 /// <summary>Returns 'null' if download is not available and caller should keep polling.</summary>
537- private async Task < ( XElement element , TimeSpan delay ) > TryDownloadFileAsync ( IFileDownloader fileDownloader , CancellationToken cancellationToken )
611+ private async Task < ( XElement ? element , TimeSpan delay ) > TryDownloadFileAsync ( IFileDownloader fileDownloader , CancellationToken cancellationToken )
538612 {
539613 LogInfo ( "Read file from client" ) ;
540614
@@ -609,7 +683,7 @@ private async Task RepeatIOAsync(Action<CancellationToken> action, CancellationT
609683 }
610684 }
611685
612- private async Task < ( bool succeeded , byte [ ] contentBytes ) > TryParseDatabaseElementAsync ( XElement element , CancellationToken cancellationToken )
686+ private async Task < ( bool succeeded , byte [ ] ? contentBytes ) > TryParseDatabaseElementAsync ( XElement element , CancellationToken cancellationToken )
613687 {
614688 LogInfo ( "Parsing database element" ) ;
615689 var contentsAttribute = element . Attribute ( ContentAttributeName ) ;
0 commit comments