Skip to content

Commit 9a27ef7

Browse files
authored
Reduce CPU/Allocations in elfie loading
In addition to persisting the elfie index as a txt file, persist it as a binary file, allowing it to on subsequent loads to be read from the binary file. This showed pretty drastic CPU/allocation improvements during the initial solution load.
1 parent 42f217f commit 9a27ef7

File tree

6 files changed

+132
-53
lines changed

6 files changed

+132
-53
lines changed

src/Features/Core/Portable/SymbolSearch/Windows/IDatabaseFactoryService.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
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-
75
using Microsoft.CodeAnalysis.Elfie.Model;
86

97
namespace Microsoft.CodeAnalysis.SymbolSearch;
108

119
internal interface IDatabaseFactoryService
1210
{
13-
AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes);
11+
AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes, bool isBinary);
1412
}

src/Features/Core/Portable/SymbolSearch/Windows/IIOService.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
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-
5+
using System;
76
using System.IO;
87

98
namespace Microsoft.CodeAnalysis.SymbolSearch;
@@ -17,7 +16,7 @@ internal interface IIOService
1716
void Delete(FileInfo file);
1817
bool Exists(FileSystemInfo info);
1918
byte[] ReadAllBytes(string path);
20-
void Replace(string sourceFileName, string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors);
19+
void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors);
2120
void Move(string sourceFileName, string destinationFileName);
22-
void WriteAndFlushAllBytes(string path, byte[] bytes);
21+
void WriteAndFlushAllBytes(string path, ArraySegment<byte> bytes);
2322
}

src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.DatabaseFactoryService.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
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-
75
using System.IO;
86
using Microsoft.CodeAnalysis.Elfie.Model;
97

@@ -13,12 +11,22 @@ internal sealed partial class SymbolSearchUpdateEngine
1311
{
1412
private sealed class DatabaseFactoryService : IDatabaseFactoryService
1513
{
16-
public AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes)
14+
public AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes, bool isBinary)
1715
{
1816
using var memoryStream = new MemoryStream(bytes);
19-
using var streamReader = new StreamReader(memoryStream);
2017
var database = new AddReferenceDatabase(ArdbVersion.V1);
21-
database.ReadText(streamReader);
18+
19+
if (isBinary)
20+
{
21+
using var binaryReader = new BinaryReader(memoryStream);
22+
database.ReadBinary(binaryReader);
23+
}
24+
else
25+
{
26+
using var streamReader = new StreamReader(memoryStream);
27+
database.ReadText(streamReader);
28+
}
29+
2230
return database;
2331
}
2432
}

src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.IOService.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
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-
5+
using System;
76
using System.IO;
87

98
namespace Microsoft.CodeAnalysis.SymbolSearch;
@@ -20,16 +19,17 @@ private sealed class IOService : IIOService
2019

2120
public byte[] ReadAllBytes(string path) => File.ReadAllBytes(path);
2221

23-
public void Replace(string sourceFileName, string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors)
22+
public void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors)
2423
=> File.Replace(sourceFileName, destinationFileName, destinationBackupFileName, ignoreMetadataErrors);
2524

2625
public void Move(string sourceFileName, string destinationFileName)
2726
=> File.Move(sourceFileName, destinationFileName);
2827

29-
public void WriteAndFlushAllBytes(string path, byte[] bytes)
28+
public void WriteAndFlushAllBytes(string path, ArraySegment<byte> bytes)
3029
{
3130
using var fileStream = new FileStream(path, FileMode.Create);
32-
fileStream.Write(bytes, 0, bytes.Length);
31+
32+
fileStream.Write(bytes.Array!, bytes.Offset, bytes.Count);
3333
fileStream.Flush(flushToDisk: true);
3434
}
3535
}

src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.Update.cs

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
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-
75
using System;
86
using System.Collections.Concurrent;
97
using System.Collections.Generic;
@@ -18,6 +16,7 @@
1816
using Microsoft.CodeAnalysis.AddImport;
1917
using Microsoft.CodeAnalysis.Elfie.Model;
2018
using Microsoft.CodeAnalysis.Shared.Utilities;
19+
using Roslyn.Utilities;
2120
using static System.FormattableString;
2221

2322
namespace 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

Comments
 (0)