Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better BlobBuilder pooling #72383

Merged
merged 12 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/Compilers/Core/Portable/Compilation/Compilation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3351,8 +3351,6 @@ internal static bool SerializePeToStream(
RSAParameters? privateKeyOpt,
CancellationToken cancellationToken)
{
bool emitSecondaryAssembly = getMetadataPeStreamOpt != null;

bool includePrivateMembersOnPrimaryOutput = metadataOnly ? includePrivateMembers : true;
bool deterministicPrimaryOutput = (metadataOnly && !includePrivateMembers) || isDeterministic;
if (!Cci.PeWriter.WritePeToStream(
Expand All @@ -3372,7 +3370,7 @@ internal static bool SerializePeToStream(
}

// produce the secondary output (ref assembly) if needed
if (emitSecondaryAssembly)
if (getMetadataPeStreamOpt is not null)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allowed me to avoid a nullable suppression on getMetadataPeStreamOpt below.

{
Debug.Assert(!metadataOnly);
Debug.Assert(!includePrivateMembers);
Expand Down
10 changes: 5 additions & 5 deletions src/Compilers/Core/Portable/PEWriter/ExtendedPEBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ public ExtendedPEBuilder(
PEHeaderBuilder header,
MetadataRootBuilder metadataRootBuilder,
BlobBuilder ilStream,
BlobBuilder mappedFieldData,
BlobBuilder managedResources,
ResourceSectionBuilder nativeResources,
DebugDirectoryBuilder debugDirectoryBuilder,
BlobBuilder? mappedFieldData,
BlobBuilder? managedResources,
ResourceSectionBuilder? nativeResources,
DebugDirectoryBuilder? debugDirectoryBuilder,
int strongNameSignatureSize,
MethodDefinitionHandle entryPoint,
CorFlags flags,
Func<IEnumerable<Blob>, BlobContentId> deterministicIdProvider,
Func<IEnumerable<Blob>, BlobContentId>? deterministicIdProvider,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes all line up with the declarations on the base type ctor.

bool withMvidSection)
: base(header, metadataRootBuilder, ilStream, mappedFieldData, managedResources, nativeResources,
debugDirectoryBuilder, strongNameSignatureSize, entryPoint, flags, deterministicIdProvider)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Microsoft.Cci;

namespace Microsoft.CodeAnalysis;

internal static class MetadataBuilderExtensions
{
internal static BlobHandle GetOrAddBlobAndFree(this MetadataBuilder metadataBuilder, PooledBlobBuilder builder)
{
var handle = metadataBuilder.GetOrAddBlob(builder);
builder.Free();
return handle;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ private BlobHandle SerializeSequencePoints(
return default(BlobHandle);
}

var writer = new BlobBuilder();
var writer = PooledBlobBuilder.GetInstance();

int previousNonHiddenStartLine = -1;
int previousNonHiddenStartColumn = -1;
Expand Down Expand Up @@ -699,7 +699,7 @@ private BlobHandle SerializeSequencePoints(
previousNonHiddenStartColumn = sequencePoints[i].StartColumn;
}

return _debugMetadataOpt.GetOrAddBlob(writer);
return _debugMetadataOpt.GetOrAddBlobAndFree(writer);
}

private static DebugSourceDocument TryGetSingleDocument(ImmutableArray<SequencePoint> sequencePoints)
Expand Down Expand Up @@ -809,38 +809,38 @@ private void SerializeEncMethodDebugInformation(IMethodBody methodBody, MethodDe

if (!encInfo.LocalSlots.IsDefaultOrEmpty)
{
var writer = new BlobBuilder();
var writer = PooledBlobBuilder.GetInstance();

encInfo.SerializeLocalSlots(writer);

_debugMetadataOpt.AddCustomDebugInformation(
parent: method,
kind: _debugMetadataOpt.GetOrAddGuid(PortableCustomDebugInfoKinds.EncLocalSlotMap),
value: _debugMetadataOpt.GetOrAddBlob(writer));
value: _debugMetadataOpt.GetOrAddBlobAndFree(writer));
}

if (!encInfo.Lambdas.IsDefaultOrEmpty)
{
var writer = new BlobBuilder();
var writer = PooledBlobBuilder.GetInstance();

encInfo.SerializeLambdaMap(writer);

_debugMetadataOpt.AddCustomDebugInformation(
parent: method,
kind: _debugMetadataOpt.GetOrAddGuid(PortableCustomDebugInfoKinds.EncLambdaAndClosureMap),
value: _debugMetadataOpt.GetOrAddBlob(writer));
value: _debugMetadataOpt.GetOrAddBlobAndFree(writer));
}

if (!encInfo.StateMachineStates.IsDefaultOrEmpty)
{
var writer = new BlobBuilder();
var writer = PooledBlobBuilder.GetInstance();

encInfo.SerializeStateMachineStates(writer);

_debugMetadataOpt.AddCustomDebugInformation(
parent: method,
kind: _debugMetadataOpt.GetOrAddGuid(PortableCustomDebugInfoKinds.EncStateMachineStateMap),
value: _debugMetadataOpt.GetOrAddBlob(writer));
value: _debugMetadataOpt.GetOrAddBlobAndFree(writer));
}
}

Expand Down
123 changes: 91 additions & 32 deletions src/Compilers/Core/Portable/PEWriter/PeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
Expand Down Expand Up @@ -35,13 +33,69 @@ public PeWritingException(Exception inner)

internal static class PeWriter
{
internal struct EmitBuilders
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Know there was, rightfully, confusion in previous iterations of the change on whose responsibility it was to free values and whether or not that was handled correctly in the code. Tried to respond to that feedback by moving all of the responsibility for the builder ownership into this type. Hoping this makes everything clearer.

{
internal BlobBuilder IlBlobBuilder;
internal PooledBlobBuilder MappedFieldDataBlobBuilder;
internal PooledBlobBuilder ManagedResourceBlobBuilder;
internal PooledBlobBuilder? PortableExecutableBlobBuilder;
internal PooledBlobBuilder? PortablePdbBlobBuilder;

public EmitBuilders()
{
IlBlobBuilder = new BlobBuilder(32 * 1024);
MappedFieldDataBlobBuilder = PooledBlobBuilder.GetInstance();
ManagedResourceBlobBuilder = PooledBlobBuilder.GetInstance();
PortableExecutableBlobBuilder = null;
PortablePdbBlobBuilder = null;
}

internal void Free()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make this IDisposable and using it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of our pool freeing in the compiler is not via IDisposable which is why I didn't use that here.

{
if (PortableExecutableBlobBuilder is null)
{
MappedFieldDataBlobBuilder.Free();
ManagedResourceBlobBuilder.Free();
}
else
{
var mappedCount = MappedFieldDataBlobBuilder.Count;
var resourceCount = ManagedResourceBlobBuilder.Count;

PortableExecutableBlobBuilder.Free();

// Once PortableExecutableBuilder is created it becomes the owner of the
// MappedFieldDataBuilder and ManagedResourceBuilder instances. Freeing
// it is sufficient to free both of them.
//
// However there is a bug in LinkSuffix / LinkPrefix which causes the
// ownership to not happen when these are length 0. Need to handle that
// specially until this is fixed.
// https://github.com/dotnet/runtime/issues/99266
if (mappedCount == 0)
{
MappedFieldDataBlobBuilder.AssertAlive();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a previous iteration of this change I had a similar method called AssertFreed and @333fred rightfully pointed out it's a race condition. As written now this call is not a race cause the BlobBuilder will be alive. Once the bug mentioned here is fixed this assert also becomes a race condition. However the chance of the race is unlikely enough that this will still serve as an sufficient trip wire to ensure that we don't forget to update this code.

MappedFieldDataBlobBuilder.Free();
}

if (resourceCount == 0)
{
ManagedResourceBlobBuilder.AssertAlive();
ManagedResourceBlobBuilder.Free();
}
}

PortablePdbBlobBuilder?.Free();
}
}

internal static bool WritePeToStream(
EmitContext context,
CommonMessageProvider messageProvider,
Func<Stream> getPeStream,
Func<Stream> getPortablePdbStreamOpt,
PdbWriter nativePdbWriterOpt,
string pdbPathOpt,
Func<Stream?> getPeStream,
Func<Stream?>? getPortablePdbStreamOpt,
PdbWriter? nativePdbWriterOpt,
string? pdbPathOpt,
bool metadataOnly,
bool isDeterministic,
bool emitTestCoverageData,
Expand All @@ -64,16 +118,13 @@ internal static bool WritePeToStream(
// based on the contents of the generated stream.
Debug.Assert(properties.PersistentIdentifier == default(Guid));

var ilBuilder = new BlobBuilder(32 * 1024);
var mappedFieldDataBuilder = new BlobBuilder();
var managedResourceBuilder = new BlobBuilder(1024);

var emitBuilders = new EmitBuilders();
Copy link
Member Author

@jaredpar jaredpar Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlekseyTs, @333fred

Last night I was considering going ahead and fixing the LinkSuffix / LinkPrefix bug and realized my current approach is flawed. Once that bug is fixed and a new version of SRM is used with Roslyn then my code will start throwing cause Count == 0 builders will be double freed. This will very concretely happen in Source Build.

After some thought I decided a more durable fix is to pass null into ExtendedPEBuilder for these two BlobBuilder instead of an empty BlobBuilder. The null is an allowed case and is not impacted by the bug fix. There were two ways to achieve this:

  1. Add a simple check after the BuildAndMetadataAndIL and free + null out the fields if their Count is zero. This is a smaller change but felt a bit off. Like I was fixing the symptom not the cause.
  2. Do not allocate the BlobBuilder unless we're going to put content into it. This change produces bigger code churn but feels more correct.

Ended up going with approach (2) here. If you feel churn is too big can use approach (1) instead.

Blob mvidFixup, mvidStringFixup;
mdWriter.BuildMetadataAndIL(
nativePdbWriterOpt,
ilBuilder,
mappedFieldDataBuilder,
managedResourceBuilder,
emitBuilders.IlBlobBuilder,
emitBuilders.MappedFieldDataBlobBuilder,
emitBuilders.ManagedResourceBlobBuilder,
out mvidFixup,
out mvidStringFixup);

Expand Down Expand Up @@ -113,9 +164,10 @@ internal static bool WritePeToStream(
nativePdbWriterOpt.WriteCompilerVersion(context.Module.CommonCompilation.Language);
}

Stream peStream = getPeStream();
Stream? peStream = getPeStream();
if (peStream == null)
{
emitBuilders.Free();
return false;
}

Expand Down Expand Up @@ -155,7 +207,7 @@ internal static bool WritePeToStream(
// We need to calculate the PDB checksum, so we may as well use the calculated hash for PDB ID regardless of whether deterministic build is requested.
var portablePdbContentHash = default(ImmutableArray<byte>);

BlobBuilder portablePdbToEmbed = null;
PooledBlobBuilder? portablePdbToEmbed = null;
if (mdWriter.EmitPortableDebugMetadata)
{
mdWriter.AddRemainingDebugDocuments(mdWriter.Module.DebugDocumentsBuilder.DebugDocuments);
Expand All @@ -167,25 +219,25 @@ internal static bool WritePeToStream(
new Func<IEnumerable<Blob>, BlobContentId>(content => BlobContentId.FromHash(portablePdbContentHash = CryptographicHashProvider.ComputeHash(context.Module.PdbChecksumAlgorithm, content))) :
null;

var portablePdbBlob = new BlobBuilder();
emitBuilders.PortablePdbBlobBuilder = PooledBlobBuilder.GetInstance(zero: true);
var portablePdbBuilder = mdWriter.GetPortablePdbBuilder(metadataRootBuilder.Sizes.RowCounts, debugEntryPointHandle, portablePdbIdProvider);
pdbContentId = portablePdbBuilder.Serialize(portablePdbBlob);
pdbContentId = portablePdbBuilder.Serialize(emitBuilders.PortablePdbBlobBuilder);
portablePdbVersion = portablePdbBuilder.FormatVersion;

if (getPortablePdbStreamOpt == null)
{
// embed to debug directory:
portablePdbToEmbed = portablePdbBlob;
portablePdbToEmbed = emitBuilders.PortablePdbBlobBuilder;
}
else
{
// write to Portable PDB stream:
Stream portablePdbStream = getPortablePdbStreamOpt();
Stream? portablePdbStream = getPortablePdbStreamOpt();
if (portablePdbStream != null)
{
try
{
portablePdbBlob.WriteContentTo(portablePdbStream);
emitBuilders.PortablePdbBlobBuilder.WriteContentTo(portablePdbStream);
}
catch (Exception e) when (!(e is OperationCanceledException))
{
Expand All @@ -195,7 +247,7 @@ internal static bool WritePeToStream(
}
}

DebugDirectoryBuilder debugDirectoryBuilder;
DebugDirectoryBuilder? debugDirectoryBuilder;
if (pdbPathOpt != null || isDeterministic || portablePdbToEmbed != null)
{
debugDirectoryBuilder = new DebugDirectoryBuilder();
Expand All @@ -209,7 +261,7 @@ internal static bool WritePeToStream(
// Emit PDB Checksum entry for Portable and Embedded PDBs. The checksum is not as useful when the PDB is embedded,
// however it allows the client to efficiently validate a standalone Portable PDB that
// has been extracted from Embedded PDB and placed next to the PE file.
debugDirectoryBuilder.AddPdbChecksumEntry(context.Module.PdbChecksumAlgorithm.Name, portablePdbContentHash);
debugDirectoryBuilder.AddPdbChecksumEntry(context.Module.PdbChecksumAlgorithm.Name!, portablePdbContentHash);
}
}

Expand All @@ -234,9 +286,9 @@ internal static bool WritePeToStream(
var peBuilder = new ExtendedPEBuilder(
peHeaderBuilder,
metadataRootBuilder,
ilBuilder,
mappedFieldDataBuilder,
managedResourceBuilder,
emitBuilders.IlBlobBuilder,
emitBuilders.MappedFieldDataBlobBuilder,
emitBuilders.ManagedResourceBlobBuilder,
CreateNativeResourceSectionSerializer(context.Module),
debugDirectoryBuilder,
CalculateStrongNameSignatureSize(context.Module, privateKeyOpt),
Expand All @@ -245,29 +297,36 @@ internal static bool WritePeToStream(
peIdProvider,
metadataOnly && !context.IncludePrivateMembers);

var peBlob = new BlobBuilder();
var peContentId = peBuilder.Serialize(peBlob, out Blob mvidSectionFixup);
// This needs to force the backing builder to zero due to the issue writing COFF
// headers. Can remove once this issue is fixed and we've moved to SRM with the
// fix
// https://github.com/dotnet/runtime/issues/99244
emitBuilders.PortableExecutableBlobBuilder = PooledBlobBuilder.GetInstance(zero: true);
var peContentId = peBuilder.Serialize(emitBuilders.PortableExecutableBlobBuilder, out Blob mvidSectionFixup);

PatchModuleVersionIds(mvidFixup, mvidSectionFixup, mvidStringFixup, peContentId.Guid);

if (privateKeyOpt != null && corFlags.HasFlag(CorFlags.StrongNameSigned))
{
strongNameProvider.SignBuilder(peBuilder, peBlob, privateKeyOpt.Value);
Debug.Assert(strongNameProvider != null);
strongNameProvider.SignBuilder(peBuilder, emitBuilders.PortableExecutableBlobBuilder, privateKeyOpt.Value);
}

try
{
peBlob.WriteContentTo(peStream);
emitBuilders.PortableExecutableBlobBuilder.WriteContentTo(peStream);
}
catch (Exception e) when (!(e is OperationCanceledException))
{
throw new PeWritingException(e);
}

emitBuilders.Free();
return true;
}

private static MethodInfo s_calculateChecksumMethod;
private static MethodInfo? s_calculateChecksumMethod;

// internal for testing
internal static uint CalculateChecksum(BlobBuilder peBlob, Blob checksumBlob)
{
Expand All @@ -282,7 +341,7 @@ internal static uint CalculateChecksum(BlobBuilder peBlob, Blob checksumBlob)
{
peBlob,
checksumBlob,
});
})!;
}

private static void PatchModuleVersionIds(Blob guidFixup, Blob guidSectionFixup, Blob stringFixup, Guid mvid)
Expand Down Expand Up @@ -318,7 +377,7 @@ private static string PadPdbPath(string path)
return path + new string('\0', Math.Max(0, minLength - Encoding.UTF8.GetByteCount(path) - 1));
}

private static ResourceSectionBuilder CreateNativeResourceSectionSerializer(CommonPEModuleBuilder module)
private static ResourceSectionBuilder? CreateNativeResourceSectionSerializer(CommonPEModuleBuilder module)
{
// Win32 resources are supplied to the compiler in one of two forms, .RES (the output of the resource compiler),
// or .OBJ (the output of running cvtres.exe on a .RES file). A .RES file is parsed and processed into
Expand Down
Loading