diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.cs index 181b69b3b4b4f3..6db40e928ffed9 100644 --- a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.cs +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.cs @@ -87,6 +87,8 @@ protected override ModuleData CreateValueFromKey(string key) private readonly SharedGenericsMode _genericsMode; + internal SharedGenericsMode SharedGenericsMode => _genericsMode; + public IReadOnlyDictionary InputFilePaths { get; diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs index 54ff003ae967de..e0e406e6e9464a 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/CoffObjectWriter.cs @@ -62,8 +62,18 @@ protected sealed record SectionDefinition(CoffSectionHeader Header, Stream Strea private static readonly ObjectNodeSection DebugTypesSection = new ObjectNodeSection(".debug$T", SectionType.ReadOnly); private static readonly ObjectNodeSection DebugSymbolSection = new ObjectNodeSection(".debug$S", SectionType.ReadOnly); - public CoffObjectWriter(NodeFactory factory, ObjectWritingOptions options, OutputInfoBuilder outputInfoBuilder = null) - : base(factory, options, outputInfoBuilder) + public CoffObjectWriter( + NodeFactory factory, + ObjectWritingOptions options, + OutputInfoBuilder outputInfoBuilder = null, + Action recordIncrementalNode = null, + Action> completeIncrementalLayout = null) + : base( + factory, + options, + outputInfoBuilder, + recordIncrementalNode, + completeIncrementalLayout) { _machine = factory.Target.Architecture switch { @@ -481,6 +491,32 @@ private protected override void EmitObjectFile(Stream outputFileStream) stringTable.Write(outputFileStream); } + private protected override bool TryGetObjectFileRange( + int sectionIndex, + long sectionOffset, + int size, + out long fileOffset) + { + fileOffset = 0; + if ((uint)sectionIndex >= (uint)_sections.Count || + sectionOffset < 0 || + size < 0) + { + return false; + } + + CoffSectionHeader header = _sections[sectionIndex].Header; + if (header.PointerToRawData == 0 || + sectionOffset > header.SizeOfRawData || + size > header.SizeOfRawData - sectionOffset) + { + return false; + } + + fileOffset = checked(header.PointerToRawData + sectionOffset); + return true; + } + protected struct CoffHeader { public Machine Machine { get; set; } diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs index 21b1802a49536b..0e9a2db9d34f0a 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs @@ -32,6 +32,8 @@ private protected sealed record ChecksumsToCalculate(int SectionIndex, long Offs private protected readonly NodeFactory _nodeFactory; private protected readonly ObjectWritingOptions _options; private protected readonly OutputInfoBuilder _outputInfoBuilder; + private readonly Action _recordIncrementalNode; + private readonly Action> _completeIncrementalLayout; private readonly bool _isSingleFileCompilation; protected readonly Utf8StringBuilder _utf8StringBuilder = new(); @@ -48,11 +50,18 @@ private protected sealed record ChecksumsToCalculate(int SectionIndex, long Offs // Symbol table private readonly Dictionary _definedSymbols = new(); - private protected ObjectWriter(NodeFactory factory, ObjectWritingOptions options, OutputInfoBuilder outputInfoBuilder = null) + private protected ObjectWriter( + NodeFactory factory, + ObjectWritingOptions options, + OutputInfoBuilder outputInfoBuilder = null, + Action recordIncrementalNode = null, + Action> completeIncrementalLayout = null) { _nodeFactory = factory; _options = options; _outputInfoBuilder = outputInfoBuilder; + _recordIncrementalNode = recordIncrementalNode; + _completeIncrementalLayout = completeIncrementalLayout; _isSingleFileCompilation = _nodeFactory.CompilationModuleGroup.IsSingleFileCompilation; // Padding byte for code sections (NOP for x86/x64) @@ -81,6 +90,16 @@ private protected ObjectWriter(NodeFactory factory, ObjectWritingOptions options /// private protected virtual ObjectNodeSection GetEmitSection(ObjectNodeSection section) => section; + private protected virtual bool TryGetObjectFileRange( + int sectionIndex, + long sectionOffset, + int size, + out long fileOffset) + { + fileOffset = 0; + return false; + } + private protected SectionWriter GetOrCreateSection(ObjectNodeSection section) => GetOrCreateSection(section, default, default); @@ -399,7 +418,8 @@ public virtual void EmitObject(Stream outputFileStream, IReadOnlyCollection + TryGetObjectFileRange(sectionIndex, sectionOffset, size, out long fileOffset) ? + fileOffset : + null); } private protected virtual void RecordMethodDeclaration(INodeWithTypeSignature node) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj index 69e9d87637f92d..bd5e01ba8be751 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/ILCompiler.Compiler.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/IncrementalCompilationTests.cs b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/IncrementalCompilationTests.cs new file mode 100644 index 00000000000000..36251801951b3c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler.Tests/IncrementalCompilationTests.cs @@ -0,0 +1,1162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +using ILCompiler.DependencyAnalysis; +using ILCompiler.ObjectWriter; + +using Internal.IL; +using Internal.Text; +using Internal.TypeSystem; + +using Xunit; + +namespace ILCompiler.Compiler.Tests +{ + public sealed class IncrementalCompilationTests + { + [Fact] + public void ConstantOnlyLeafChangeIsAccepted() + { + byte[] baseline = + [ + (byte)ILOpcode.ldarg_0, + (byte)ILOpcode.ldc_i4, 1, 0, 0, 0, + (byte)ILOpcode.add, + (byte)ILOpcode.ret, + ]; + byte[] updated = (byte[])baseline.Clone(); + updated[2] = 2; + + Assert.True(IncrementalBodyUpdate.IsDependencyNeutralConstantChange(baseline, updated)); + } + + [Fact] + public void BodyGateRejectsUnsafeOrUnchangedEdits() + { + Assert.False(IncrementalBodyUpdate.IsDependencyNeutralConstantChange( + [(byte)ILOpcode.ldarg_s, 0, (byte)ILOpcode.ret], + [(byte)ILOpcode.ldarg_s, 1, (byte)ILOpcode.ret])); + Assert.False(IncrementalBodyUpdate.IsDependencyNeutralConstantChange( + [(byte)ILOpcode.call, 1, 0, 0, 0, (byte)ILOpcode.ret], + [(byte)ILOpcode.call, 2, 0, 0, 0, (byte)ILOpcode.ret])); + Assert.False(IncrementalBodyUpdate.IsDependencyNeutralConstantChange( + [(byte)ILOpcode.ldc_i4, 1, 0, 0, 0, (byte)ILOpcode.ret], + [(byte)ILOpcode.ldc_i4, 1, 0, 0, 0, (byte)ILOpcode.ret])); + Assert.True(IncrementalBodyUpdate.IsDependencyNeutralConstantChange( + [(byte)ILOpcode.ldc_i4_s, 1, (byte)ILOpcode.ret], + [(byte)ILOpcode.ldc_i4_s, 2, (byte)ILOpcode.ret])); + } + + [Theory] + [InlineData(RelocType.IMAGE_REL_BASED_ABSOLUTE, true, 4)] + [InlineData(RelocType.IMAGE_REL_BASED_ADDR32NB, true, 4)] + [InlineData(RelocType.IMAGE_REL_BASED_HIGHLOW, true, 4)] + [InlineData(RelocType.IMAGE_REL_BASED_DIR64, true, 8)] + [InlineData(RelocType.IMAGE_REL_BASED_REL32, true, 4)] + [InlineData(RelocType.IMAGE_REL_BASED_RELPTR32, true, 4)] + [InlineData(RelocType.IMAGE_REL_SECREL, true, 4)] + [InlineData(RelocType.IMAGE_REL_SECTION, false, 0)] + [InlineData(RelocType.IMAGE_REL_BASED_ARM64_BRANCH26, false, 0)] + public void WindowsX64RelocationWidthsAreExplicit( + RelocType relocType, + bool expected, + int expectedWidth) + { + Assert.Equal( + expected, + IncrementalObjectBaseline.TryGetWindowsX64RelocationWidth( + relocType, + out int width)); + Assert.Equal(expectedWidth, width); + } + + [Fact] + public void BaselineRejectsNonRelocationByteMismatch() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + byte[] recorded = [1, 2, 3, 4]; + byte[] actual = [1, 2, 9, 4]; + File.WriteAllBytes(objectPath, actual); + + var node = new TestObjectNode(recorded); + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + long emissionLength = actual.Length; + byte[] emissionHash = SHA256.HashData(actual); + + Assert.False(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + emissionLength, + emissionHash, + SHA256.HashData([1]), + SHA256.HashData([2]), + out IncrementalObjectBaseline baseline, + out string reason)); + Assert.Null(baseline); + Assert.Contains("non-relocation byte", reason); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void PatchCopiesVerifiedHandleAndPublishesWithoutOverwrite() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + string outputPath = Path.Combine(directory, "updated.obj"); + string revertedPath = Path.Combine(directory, "reverted.obj"); + byte[] original = [1, 2, 3, 4]; + File.WriteAllBytes(objectPath, original); + + var node = new TestObjectNode(original); + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + byte[] assemblyHash = SHA256.HashData([1]); + byte[] configurationHash = SHA256.HashData([2]); + long emissionLength = original.Length; + byte[] emissionHash = SHA256.HashData(original); + + Assert.True(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + emissionLength, + emissionHash, + assemblyHash, + configurationHash, + out IncrementalObjectBaseline baseline, + out string reason), + reason); + + using (baseline) + { + node.Data = [1, 9, 3, 4]; + Assert.True(baseline.TryWritePatchedObject( + outputPath, + [node], + factory: null, + assemblyHash, + configurationHash, + out long patchedByteCount, + out reason), + reason); + Assert.Equal(1, patchedByteCount); + Assert.Equal(node.Data, File.ReadAllBytes(outputPath)); + + node.Data = (byte[])original.Clone(); + Assert.True(baseline.TryWritePatchedObject( + revertedPath, + [node], + factory: null, + assemblyHash, + configurationHash, + out patchedByteCount, + out reason), + reason); + Assert.Equal(0, patchedByteCount); + Assert.Equal(original, File.ReadAllBytes(revertedPath)); + + node.Data = [1, 8, 3, 4]; + Assert.False(baseline.TryWritePatchedObject( + outputPath, + [node], + factory: null, + assemblyHash, + configurationHash, + out _, + out reason)); + Assert.Contains("already exists", reason); + Assert.Equal([1, 9, 3, 4], File.ReadAllBytes(outputPath)); + } + + Assert.Empty(Directory.GetFiles(directory, "*.tmp")); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void BaselineAndPatchRejectHashBindingMismatches() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + string outputPath = Path.Combine(directory, "updated.obj"); + byte[] original = [1, 2, 3, 4]; + File.WriteAllBytes(objectPath, original); + + var node = new TestObjectNode(original); + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + byte[] assemblyHash = SHA256.HashData([1]); + byte[] configurationHash = SHA256.HashData([2]); + + Assert.False(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + original.Length, + SHA256.HashData([9]), + assemblyHash, + configurationHash, + out _, + out string reason)); + Assert.Contains("hash", reason); + + Assert.True(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + original.Length, + SHA256.HashData(original), + assemblyHash, + configurationHash, + out IncrementalObjectBaseline baseline, + out reason), + reason); + + using (baseline) + { + Assert.False(baseline.TryWritePatchedObject( + outputPath, + [node], + factory: null, + SHA256.HashData([3]), + configurationHash, + out _, + out reason)); + Assert.Contains("assembly", reason); + + Assert.False(baseline.TryWritePatchedObject( + outputPath, + [node], + factory: null, + assemblyHash, + SHA256.HashData([4]), + out _, + out reason)); + Assert.Contains("configuration", reason); + } + + Assert.False(File.Exists(outputPath)); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void PatchRejectsRelocationAddendChange() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + string outputPath = Path.Combine(directory, "updated.obj"); + byte[] original = [1, 2, 3, 4, 5]; + File.WriteAllBytes(objectPath, original); + + var node = new TestObjectNode(original); + node.Relocations = + [ + new Relocation(RelocType.IMAGE_REL_BASED_REL32, 0, node), + ]; + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + byte[] assemblyHash = SHA256.HashData([1]); + byte[] configurationHash = SHA256.HashData([2]); + long emissionLength = original.Length; + byte[] emissionHash = SHA256.HashData(original); + + Assert.True(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + emissionLength, + emissionHash, + assemblyHash, + configurationHash, + out IncrementalObjectBaseline baseline, + out string reason), + reason); + + using (baseline) + { + node.Data = [9, 2, 3, 4, 5]; + Assert.False(baseline.TryWritePatchedObject( + outputPath, + [node], + factory: null, + assemblyHash, + configurationHash, + out _, + out reason)); + Assert.Contains("addend", reason); + } + + Assert.False(File.Exists(outputPath)); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Theory] + [InlineData("alignment", "alignment")] + [InlineData("size", "size or alignment")] + [InlineData("symbol", "defined symbols")] + [InlineData("symbol-offset", "defined symbols")] + [InlineData("relocation-count", "relocation count")] + [InlineData("relocation-type", "changed a relocation")] + [InlineData("relocation-offset", "changed a relocation")] + [InlineData("relocation-target", "changed a relocation")] + [InlineData("relocation-target-offset", "changed a relocation")] + public void PatchRejectsObjectShapeChanges(string change, string expectedReason) + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + string outputPath = Path.Combine(directory, "updated.obj"); + byte[] original = [1, 2, 3, 4, 5, 6, 7, 8]; + File.WriteAllBytes(objectPath, original); + + var node = new TestObjectNode(original) { Alignment = 4 }; + var otherNode = new TestObjectNode(original); + node.Relocations = + [ + new Relocation(RelocType.IMAGE_REL_BASED_REL32, 0, otherNode), + ]; + node.DefinedSymbols = [node]; + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + byte[] assemblyHash = SHA256.HashData([1]); + byte[] configurationHash = SHA256.HashData([2]); + long emissionLength = original.Length; + byte[] emissionHash = SHA256.HashData(original); + + Assert.True(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + emissionLength, + emissionHash, + assemblyHash, + configurationHash, + out IncrementalObjectBaseline baseline, + out string reason), + reason); + + using (baseline) + { + switch (change) + { + case "alignment": + node.Alignment = 8; + break; + case "size": + node.Data = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + break; + case "symbol": + node.DefinedSymbols = [otherNode]; + break; + case "symbol-offset": + node.OffsetValue = 1; + break; + case "relocation-count": + node.Relocations = []; + break; + case "relocation-type": + node.Relocations = + [ + new Relocation(RelocType.IMAGE_REL_BASED_HIGHLOW, 0, node), + ]; + break; + case "relocation-offset": + node.Relocations = + [ + new Relocation(RelocType.IMAGE_REL_BASED_REL32, 4, node), + ]; + break; + case "relocation-target": + node.Relocations = + [ + new Relocation(RelocType.IMAGE_REL_BASED_REL32, 0, node), + ]; + break; + case "relocation-target-offset": + otherNode.OffsetValue = 1; + break; + } + + Assert.False(baseline.TryWritePatchedObject( + outputPath, + [node], + factory: null, + assemblyHash, + configurationHash, + out _, + out reason)); + Assert.Contains(expectedReason, reason); + } + + Assert.False(File.Exists(outputPath)); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void BaselineRejectsOverlappingRelocations() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + byte[] original = [1, 2, 3, 4, 5, 6]; + File.WriteAllBytes(objectPath, original); + + var node = new TestObjectNode(original); + node.Relocations = + [ + new Relocation(RelocType.IMAGE_REL_BASED_REL32, 0, node), + new Relocation(RelocType.IMAGE_REL_BASED_REL32, 2, node), + ]; + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + long emissionLength = original.Length; + byte[] emissionHash = SHA256.HashData(original); + + Assert.False(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + emissionLength, + emissionHash, + SHA256.HashData([1]), + SHA256.HashData([2]), + out _, + out string reason)); + Assert.Contains("overlap", reason); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void ComdatAndDuplicateRecordsAreRejectedBeforeMutation() + { + byte[] data = [1, 2, 3, 4]; + var node = new TestObjectNode(data); + var layout = new IncrementalObjectLayout([node]); + layout.RecordNode(node, 0, 0, node.GetData(null), isComdat: false); + layout.RecordNode(node, 0, 0, node.GetData(null), isComdat: false); + Assert.Contains("more than once", layout.FailureReason); + + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + File.WriteAllBytes(objectPath, data); + layout = CreateLayout(node, isComdat: true); + long emissionLength = data.Length; + byte[] emissionHash = SHA256.HashData(data); + + Assert.False(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + emissionLength, + emissionHash, + SHA256.HashData([1]), + SHA256.HashData([2]), + out _, + out string reason)); + Assert.Contains("COMDAT", reason); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void BaselineAllowsPageRoundedLoadedImageAndRejectsPrefixMismatch() + { + string assemblyPath = typeof(IncrementalCompilationTests).Assembly.Location; + byte[] image = File.ReadAllBytes(assemblyPath); + using var reader = new PEReader(new MemoryStream(image, writable: false)); + MetadataReader metadata = reader.GetMetadataReader(); + Guid mvid = metadata.GetGuid(metadata.GetModuleDefinition().Mvid); + + int roundedLength = checked((image.Length + 4095) & ~4095); + if (roundedLength == image.Length) + roundedLength += 4096; + byte[] longerLoadedImage = new byte[roundedLength]; + image.CopyTo(longerLoadedImage, 0); + Assert.True(IncrementalAssemblyBaseline.TryCreate( + longerLoadedImage, + mvid, + metadata.MethodDefinitions.Count, + assemblyPath, + out _, + out string reason), + reason); + + longerLoadedImage[0] ^= 1; + Assert.False(IncrementalAssemblyBaseline.TryCreate( + longerLoadedImage, + mvid, + metadata.MethodDefinitions.Count, + assemblyPath, + out _, + out reason)); + Assert.Contains("does-not-match", reason); + } + + [Fact] + public void IncrementalFailureContractMatchesCompilerException() + { + var exception = new IncrementalCompilationException("rejected"); + + Assert.Equal(85, IncrementalFailureContract.CleanFallbackExitCode); + Assert.Equal(IncrementalFailureContract.FailureHResult, exception.HResult); + Assert.True(IncrementalFailureContract.IsCleanFallbackRequested( + exception, + isEnvironmentRequested: true)); + Assert.False(IncrementalFailureContract.IsCleanFallbackRequested( + exception, + isEnvironmentRequested: false)); + Assert.False(IncrementalFailureContract.IsCleanFallbackRequested( + new InvalidOperationException(), + isEnvironmentRequested: true)); + } + + [Fact] + public void PeGateMasksOnlyPermittedBytesAndMethodBodies() + { + string directory = CreateTestDirectory(); + try + { + byte[] baselineImage = File.ReadAllBytes( + typeof(IncrementalCompilationTests).Assembly.Location); + string baselinePath = Path.Combine(directory, "baseline.dll"); + string updatedPath = Path.Combine(directory, "updated.dll"); + File.WriteAllBytes(baselinePath, baselineImage); + + IncrementalAssemblyBaseline baseline = CreateAssemblyBaseline( + baselineImage, + baselinePath); + byte[] updatedImage = (byte[])baselineImage.Clone(); + using (var reader = new PEReader(new MemoryStream(updatedImage, writable: false))) + { + int timestampOffset = + checked(reader.PEHeaders.CoffHeaderStartOffset + sizeof(uint)); + updatedImage[timestampOffset] ^= 0x5A; + } + int ilOffset = GetMethodILOffset(updatedImage, nameof(PeFixture)); + Assert.Equal(0x44, updatedImage[ilOffset + 2]); + updatedImage[ilOffset + 2] = 0x45; + File.WriteAllBytes(updatedPath, updatedImage); + + Assert.True(IncrementalBodyUpdate.TryCreate( + baseProvider: null, + baseline, + updatedPath, + allowUnchangedTarget: false, + out IncrementalBodyUpdate update, + out string reason), + reason); + Assert.Equal(1, update.ChangedMethodCount); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Theory] + [InlineData("mvid", "module-version-id-changed")] + [InlineData("non-body", "non-method-assembly-content-changed")] + [InlineData("body-size", "method-body-size-changed")] + [InlineData("body-shape", "method-body-shape-changed")] + public void PeGateRejectsIdentityAndShapeChanges(string change, string expectedReason) + { + string directory = CreateTestDirectory(); + try + { + byte[] baselineImage = File.ReadAllBytes( + typeof(IncrementalCompilationTests).Assembly.Location); + string baselinePath = Path.Combine(directory, "baseline.dll"); + string updatedPath = Path.Combine(directory, "updated.dll"); + File.WriteAllBytes(baselinePath, baselineImage); + IncrementalAssemblyBaseline baseline = CreateAssemblyBaseline( + baselineImage, + baselinePath); + byte[] updatedImage = (byte[])baselineImage.Clone(); + + switch (change) + { + case "mvid": + using (var reader = new PEReader( + new MemoryStream(updatedImage, writable: false))) + { + MetadataReader metadata = reader.GetMetadataReader(); + Guid mvid = metadata.GetGuid(metadata.GetModuleDefinition().Mvid); + int offset = FindSequence(updatedImage, mvid.ToByteArray()); + Assert.True(offset >= 0); + updatedImage[offset] ^= 1; + } + break; + case "non-body": + updatedImage[2] ^= 1; + break; + case "body-size": + int bodyOffset = GetMethodBodyOffset(updatedImage, nameof(PeFixture)); + if ((updatedImage[bodyOffset] & 3) == 2) + { + updatedImage[bodyOffset] = checked((byte)(updatedImage[bodyOffset] + 4)); + } + else + { + int size = BitConverter.ToInt32(updatedImage, bodyOffset + 4); + BitConverter.GetBytes(size + 1).CopyTo(updatedImage, bodyOffset + 4); + } + break; + case "body-shape": + int fatBodyOffset = GetMethodBodyOffset( + updatedImage, + nameof(PeFatFixture)); + Assert.Equal(3, updatedImage[fatBodyOffset] & 3); + updatedImage[fatBodyOffset + 2] ^= 1; + break; + } + + File.WriteAllBytes(updatedPath, updatedImage); + Assert.False(IncrementalBodyUpdate.TryCreate( + baseProvider: null, + baseline, + updatedPath, + allowUnchangedTarget: true, + out _, + out string reason)); + Assert.Contains(expectedReason, reason); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void NonEcmaMethodIlIsNotOverlayable() + { + Assert.False(IncrementalBodyUpdate.IsOverlayableMethodIL(null)); + Assert.False(IncrementalBodyUpdate.IsOverlayableMethodIL(new TestMethodIL())); + } + + [Fact] + public void SequentialDirtyUnionIncludesEditDifferentMethodAndRevert() + { + Assert.Equal( + [1], + Sorted(IncrementalBodyUpdate.GetAffectedMethodTokens([1], previousTokens: null))); + Assert.Equal( + [1, 2], + Sorted(IncrementalBodyUpdate.GetAffectedMethodTokens([2], [1]))); + Assert.Equal( + [2], + Sorted(IncrementalBodyUpdate.GetAffectedMethodTokens([], [2]))); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + public void UnsafeCommandLineOutputsAreRejected(int enabledOption) + { + var values = new bool[9]; + values[enabledOption] = true; + Assert.False(RyuJitCompilationBuilder.TryValidateIncrementalCommandLineConfiguration( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + out _, + out string reason)); + Assert.Contains("unsupported", reason); + } + + [Theory] + [InlineData("gc", "gc-info")] + [InlineData("frame", "frame")] + [InlineData("eh", "exception-handling")] + [InlineData("debug", "debug-info")] + public void CodeStateChangesAreRejected(string change, string expectedReason) + { + IncrementalCodeState baseline = CreateCodeState(); + IncrementalCodeState current = change switch + { + "gc" => CreateCodeState(gcInfo: [2]), + "frame" => CreateCodeState( + frameInfos: [new FrameInfo(0, 0, 1, [1])]), + "eh" => CreateCodeState(hasEhInfo: true), + "debug" => CreateCodeState( + debugLocations: [new DebugLocInfo(1, 1)]), + _ => throw new ArgumentOutOfRangeException(nameof(change)), + }; + + Assert.False(IncrementalCodeStateValidator.Matches( + baseline, + current, + out string reason)); + Assert.Contains(expectedReason, reason); + } + + [Theory] + [InlineData("order", "static-dependency")] + [InlineData("reason", "static-dependency")] + [InlineData("marked", "unmarked-static-dependency")] + [InlineData("conditional-reason", "conditional-dependency")] + [InlineData("conditional-marked", "unmarked-conditional-dependency")] + public void DependencyStateChangesAreRejected(string change, string expectedReason) + { + var first = new object(); + var second = new object(); + IncrementalDependencyEntry[] baselineStatic = + [ + new(first, "first", marked: true), + new(second, "second", marked: true), + ]; + IncrementalConditionalDependencyEntry[] baselineConditional = + [ + new(first, second, "conditional", nodeMarked: true, otherReasonNodeMarked: true), + ]; + IncrementalDependencyEntry[] currentStatic = (IncrementalDependencyEntry[])baselineStatic.Clone(); + IncrementalConditionalDependencyEntry[] currentConditional = + (IncrementalConditionalDependencyEntry[])baselineConditional.Clone(); + + switch (change) + { + case "order": + currentStatic = + [ + new(second, "second", marked: true), + new(first, "first", marked: true), + ]; + break; + case "reason": + currentStatic[0] = new(first, "different", marked: true); + break; + case "marked": + currentStatic[0] = new(first, "first", marked: false); + break; + case "conditional-reason": + currentConditional[0] = + new(first, second, "different", nodeMarked: true, otherReasonNodeMarked: true); + break; + case "conditional-marked": + currentConditional[0] = + new(first, second, "conditional", nodeMarked: true, otherReasonNodeMarked: false); + break; + } + + Assert.False(IncrementalDependencyValidator.Matches( + baselineStatic, + baselineConditional, + currentStatic, + currentConditional, + out string reason)); + Assert.Contains(expectedReason, reason); + } + + [Fact] + public void LayoutRejectsDuplicateUnresolvedAndOverlappingLocations() + { + var first = new TestObjectNode([1, 2, 3, 4]); + var second = new TestObjectNode([5, 6, 7, 8]); + var duplicate = new IncrementalObjectLayout([first, first]); + Assert.Contains("more than once", duplicate.FailureReason); + + var unresolved = new IncrementalObjectLayout([first]); + unresolved.RecordNode(first, 0, 0, first.GetData(null), isComdat: false); + unresolved.Complete((int _, long _, int _) => null); + Assert.Contains("could not be resolved", unresolved.FailureReason); + + var overlap = new IncrementalObjectLayout([first, second]); + overlap.RecordNode(first, 0, 0, first.GetData(null), isComdat: false); + overlap.RecordNode(second, 0, 2, second.GetData(null), isComdat: false); + overlap.Complete( + (int _, long sectionOffset, int _) => sectionOffset); + Assert.Contains("overlap", overlap.FailureReason); + } + + [Fact] + public void BaselineRejectsOutOfBoundsLocation() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + byte[] data = [1, 2, 3, 4]; + File.WriteAllBytes(objectPath, data); + var node = new TestObjectNode(data); + var layout = new IncrementalObjectLayout([node]); + layout.RecordNode(node, 0, 0, node.GetData(null), isComdat: false); + layout.Complete((int _, long _, int _) => 1); + + Assert.False(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + data.Length, + SHA256.HashData(data), + SHA256.HashData([1]), + SHA256.HashData([2]), + out _, + out string reason)); + Assert.Contains("outside", reason); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void NullRelocationsAndSymbolsAreTreatedAsEmpty() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + byte[] data = [1, 2, 3, 4]; + File.WriteAllBytes(objectPath, data); + var node = new TestObjectNode(data) + { + Relocations = null, + DefinedSymbols = null, + }; + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + + Assert.True(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + data.Length, + SHA256.HashData(data), + SHA256.HashData([1]), + SHA256.HashData([2]), + out IncrementalObjectBaseline baseline, + out string reason), + reason); + baseline.Dispose(); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [Fact] + public void StagedBatchRollsBackPublishedOutputsAfterLaterFailure() + { + string directory = CreateTestDirectory(); + try + { + string objectPath = Path.Combine(directory, "baseline.obj"); + byte[] data = [1, 2, 3, 4]; + File.WriteAllBytes(objectPath, data); + var node = new TestObjectNode(data); + IncrementalObjectLayout layout = CreateLayout(node, isComdat: false); + byte[] assemblyHash = SHA256.HashData([1]); + byte[] configurationHash = SHA256.HashData([2]); + Assert.True(IncrementalObjectBaseline.TryOpenLocked( + objectPath, + layout, + data.Length, + SHA256.HashData(data), + assemblyHash, + configurationHash, + out IncrementalObjectBaseline baseline, + out string reason), + reason); + + using (baseline) + { + string firstOutput = Path.Combine(directory, "first.obj"); + string secondOutput = Path.Combine(directory, "second.obj"); + Assert.True(baseline.TryStagePatchedObject( + firstOutput, + [node], + factory: null, + assemblyHash, + configurationHash, + out IncrementalStagedObject first, + out _, + out reason), + reason); + Assert.True(baseline.TryStagePatchedObject( + secondOutput, + [node], + factory: null, + assemblyHash, + configurationHash, + out IncrementalStagedObject second, + out _, + out reason), + reason); + + File.WriteAllBytes(secondOutput, [9]); + Assert.True(first.TryPublish(out reason), reason); + Assert.False(second.TryPublish(out reason)); + Assert.True(first.TryCleanup(out string firstCleanup), firstCleanup); + Assert.True(second.TryCleanup(out string secondCleanup), secondCleanup); + Assert.False(File.Exists(firstOutput)); + Assert.Equal([9], File.ReadAllBytes(secondOutput)); + Assert.Empty(Directory.GetFiles(directory, "*.tmp")); + } + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + [PlatformSpecific(TestPlatforms.Windows)] + [Fact] + public void CleanupFailureIsExplicit() + { + string directory = CreateTestDirectory(); + try + { + string temporaryPath = Path.Combine(directory, "staged.tmp"); + File.WriteAllBytes(temporaryPath, [1]); + var staged = new IncrementalStagedObject( + temporaryPath, + Path.Combine(directory, "output.obj")); + + using (new FileStream( + staged.TemporaryPath, + FileMode.Open, + FileAccess.Read, + FileShare.None)) + { + Assert.False(staged.TryCleanup(out string reason)); + Assert.Contains("could not be deleted", reason); + } + + Assert.True(staged.TryCleanup(out string cleanupReason), cleanupReason); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + private static IncrementalAssemblyBaseline CreateAssemblyBaseline( + byte[] image, + string path) + { + using var reader = new PEReader(new MemoryStream(image, writable: false)); + MetadataReader metadata = reader.GetMetadataReader(); + Assert.True(IncrementalAssemblyBaseline.TryCreate( + image, + metadata.GetGuid(metadata.GetModuleDefinition().Mvid), + metadata.MethodDefinitions.Count, + path, + out IncrementalAssemblyBaseline baseline, + out string reason), + reason); + return baseline; + } + + private static int GetMethodBodyOffset(byte[] image, string methodName) + { + using var reader = new PEReader(new MemoryStream(image, writable: false)); + MetadataReader metadata = reader.GetMetadataReader(); + foreach (MethodDefinitionHandle handle in metadata.MethodDefinitions) + { + MethodDefinition method = metadata.GetMethodDefinition(handle); + if (metadata.GetString(method.Name) == methodName) + return RvaToFileOffset(reader.PEHeaders, method.RelativeVirtualAddress); + } + + throw new InvalidOperationException($"Method '{methodName}' was not found."); + } + + private static int GetMethodILOffset(byte[] image, string methodName) + { + int bodyOffset = GetMethodBodyOffset(image, methodName); + if ((image[bodyOffset] & 3) == 2) + return checked(bodyOffset + 1); + + int headerSize = (BitConverter.ToUInt16(image, bodyOffset) >> 12) * 4; + return checked(bodyOffset + headerSize); + } + + private static int RvaToFileOffset(PEHeaders headers, int rva) + { + foreach (SectionHeader section in headers.SectionHeaders) + { + int sectionSize = Math.Max(section.VirtualSize, section.SizeOfRawData); + if (rva >= section.VirtualAddress && + rva - section.VirtualAddress < sectionSize) + { + return checked(section.PointerToRawData + rva - section.VirtualAddress); + } + } + + throw new InvalidOperationException($"RVA 0x{rva:X8} was not mapped."); + } + + private static int FindSequence(byte[] image, byte[] sequence) + { + for (int i = 0; i <= image.Length - sequence.Length; i++) + { + if (image.AsSpan(i, sequence.Length).SequenceEqual(sequence)) + return i; + } + + return -1; + } + + private static int[] Sorted(HashSet values) + { + int[] result = new int[values.Count]; + values.CopyTo(result); + Array.Sort(result); + return result; + } + + private static IncrementalCodeState CreateCodeState( + FrameInfo[] frameInfos = null, + byte[] gcInfo = null, + bool hasEhInfo = false, + DebugLocInfo[] debugLocations = null) + { + return new IncrementalCodeState( + frameInfos ?? Array.Empty(), + gcInfo ?? [1], + hasEhInfo, + debugLocations ?? Array.Empty(), + Array.Empty(), + Array.Empty(), + debugInfo: null, + Array.Empty()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int PeFixture(int value) => value + 0x11223344; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int PeFatFixture(int value) + { + try + { + int result = value + 1; + return result; + } + catch (Exception) + { + return -1; + } + } + + private static IncrementalObjectLayout CreateLayout( + TestObjectNode node, + bool isComdat) + { + var layout = new IncrementalObjectLayout([node]); + layout.RecordNode( + node, + sectionIndex: 0, + sectionOffset: 0, + node.GetData(factory: null), + isComdat); + layout.Complete( + (int sectionIndex, long sectionOffset, int _) => + sectionIndex == 0 ? sectionOffset : null); + return layout; + } + + private static string CreateTestDirectory() + { + string directory = Path.Combine( + AppContext.BaseDirectory, + $"IncrementalCompilationTests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + return directory; + } + + private sealed class TestObjectNode : ObjectNode, ISymbolDefinitionNode + { + internal TestObjectNode(byte[] data) + { + Data = (byte[])data.Clone(); + } + + internal byte[] Data { get; set; } + internal Relocation[] Relocations { get; set; } = Array.Empty(); + internal int Alignment { get; set; } = 1; + internal ISymbolDefinitionNode[] DefinedSymbols { get; set; } = + Array.Empty(); + + public override int ClassCode => -203822639; + public override bool IsShareable => false; + public override bool StaticDependenciesAreComputed => true; + internal int OffsetValue { get; set; } + public int Offset => OffsetValue; + public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) => + sb.Append(nameof(TestObjectNode)); + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) => + new ObjectData( + (byte[])Data.Clone(), + Relocations is null ? null : (Relocation[])Relocations.Clone(), + Alignment, + DefinedSymbols is null ? + null : + (ISymbolDefinitionNode[])DefinedSymbols.Clone()); + public override ObjectNodeSection GetSection(NodeFactory factory) => + ObjectNodeSection.DataSection; + protected override string GetName(NodeFactory factory) => nameof(TestObjectNode); + } + + private sealed class TestMethodIL : MethodIL + { + public override MethodDesc OwningMethod => null; + public override int MaxStack => 0; + public override bool IsInitLocals => false; + public override byte[] GetILBytes() => [(byte)ILOpcode.ret]; + public override LocalVariableDefinition[] GetLocals() => + Array.Empty(); + public override ILExceptionRegion[] GetExceptionRegions() => + Array.Empty(); + public override object GetObject( + int token, + NotFoundBehavior notFoundBehavior = NotFoundBehavior.Throw) => null; + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilationBuilder.Aot.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilationBuilder.Aot.cs index 03818be44eb2a3..14e48fd263aa52 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilationBuilder.Aot.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilationBuilder.Aot.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Text; + +using Internal.IL; namespace ILCompiler { @@ -9,6 +12,32 @@ public partial class CompilationBuilder { private PreinitializationManager _preinitializationManager; + internal bool TryGetIncrementalBaseConfiguration( + out string description, + out string reason) + { + description = null; + if (_dependencyTrackingLevel != DependencyTrackingLevel.None) + { + reason = "dependency tracking must be disabled"; + return false; + } + if (_preinitializationManager is not null && !_preinitializationManager.IsDisabled) + { + reason = "preinitialization must be disabled"; + return false; + } + + var builder = new StringBuilder(); + builder.Append("sharedgenerics=").Append((int)_context.SharedGenericsMode); + builder.Append(";delegatefeatures=").Append((int)_context.DelegateFeatures); + builder.Append(";genericcycledepth=").Append(_context.GenericCycleDepthCutoff); + builder.Append(";genericcyclebreadth=").Append(_context.GenericCycleBreadthCutoff); + description = builder.ToString(); + reason = null; + return true; + } + // These need to provide reasonable defaults so that the user can optionally skip // calling the Use/Configure methods and still get something reasonable back. protected MetadataManager _metadataManager; diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.Aot.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.Aot.cs index 6ae737aa39c41a..f92d764cf4bbda 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.Aot.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.Aot.cs @@ -32,6 +32,10 @@ public SharedGenericsConfiguration GenericsConfig get; } + internal DelegateFeature DelegateFeatures { get; } + internal int GenericCycleDepthCutoff { get; } + internal int GenericCycleBreadthCutoff { get; } + private readonly MetadataFieldLayoutAlgorithm _metadataFieldLayoutAlgorithm = new CompilerMetadataFieldLayoutAlgorithm(); private readonly RuntimeDeterminedFieldLayoutAlgorithm _runtimeDeterminedFieldLayoutAlgorithm = new RuntimeDeterminedFieldLayoutAlgorithm(); private readonly VectorOfTFieldLayoutAlgorithm _vectorOfTFieldLayoutAlgorithm; @@ -53,6 +57,9 @@ public CompilerTypeSystemContext(TargetDetails details, SharedGenericsMode gener : base(details) { _genericsMode = genericsMode; + DelegateFeatures = delegateFeatures; + GenericCycleDepthCutoff = genericCycleDepthCutoff; + GenericCycleBreadthCutoff = genericCycleBreadthCutoff; _virtualMethodAlgorithm = new AsyncAwareVirtualMethodResolutionAlgorithm(this); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectWriter/ObjectWriter.Aot.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectWriter/ObjectWriter.Aot.cs index 342f15702a17a6..356036dcdade4b 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectWriter/ObjectWriter.Aot.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectWriter/ObjectWriter.Aot.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Security.Cryptography; using System.Linq; using ILCompiler.DependencyAnalysis; using ILCompiler.DependencyAnalysisFramework; @@ -20,17 +21,66 @@ namespace ILCompiler.ObjectWriter public abstract partial class ObjectWriter { public static void EmitObject(string objectFilePath, IReadOnlyCollection nodes, NodeFactory factory, ObjectWritingOptions options, IObjectDumper dumper, Logger logger) + { + EmitObjectForIncrementalCompilation( + objectFilePath, + nodes, + factory, + options, + dumper, + logger, + recordIncrementalNode: null, + completeIncrementalLayout: null, + out _, + out _); + } + + internal static void EmitObjectForIncrementalCompilation( + string objectFilePath, + IReadOnlyCollection nodes, + NodeFactory factory, + ObjectWritingOptions options, + IObjectDumper dumper, + Logger logger, + Action recordIncrementalNode, + Action> completeIncrementalLayout, + out long objectLength, + out byte[] objectHash) { var stopwatch = Stopwatch.StartNew(); ObjectWriter objectWriter = factory.Target.IsApplePlatform ? new MachObjectWriter(factory, options) : - factory.Target.OperatingSystem == TargetOS.Windows ? new CoffObjectWriter(factory, options) : + factory.Target.OperatingSystem == TargetOS.Windows ? new CoffObjectWriter( + factory, + options, + recordIncrementalNode: recordIncrementalNode, + completeIncrementalLayout: completeIncrementalLayout) : factory.Target.Architecture == TargetArchitecture.Wasm32 ? new WasmRelocatableObjectWriter(factory, options) : new ElfObjectWriter(factory, options); - using Stream outputFileStream = new FileStream(objectFilePath, FileMode.Create); + bool incrementalEmission = + recordIncrementalNode is not null || + completeIncrementalLayout is not null; + if ((recordIncrementalNode is null) != (completeIncrementalLayout is null)) + throw new ArgumentException("Incremental object-emission callbacks must be provided together."); + + using FileStream outputFileStream = incrementalEmission ? + new FileStream(objectFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None) : + new FileStream(objectFilePath, FileMode.Create); objectWriter.EmitObject(outputFileStream, nodes, dumper, logger); + if (incrementalEmission) + { + outputFileStream.Flush(flushToDisk: true); + outputFileStream.Position = 0; + objectLength = outputFileStream.Length; + objectHash = SHA256.HashData(outputFileStream); + } + else + { + objectLength = 0; + objectHash = null; + } stopwatch.Stop(); if (logger.IsVerbose) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/PreinitializationManager.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/PreinitializationManager.cs index f885f77bc60894..9931d8aa37fcca 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/PreinitializationManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/PreinitializationManager.cs @@ -18,6 +18,7 @@ public class PreinitializationManager private readonly bool _supportsLazyCctors; public ReadOnlyFieldPolicy ReadOnlyFieldPolicy => _preinitHashTable._readOnlyPolicy; + internal bool IsDisabled => _preinitHashTable._policy is TypePreinit.DisabledPreinitializationPolicy; public PreinitializationManager(TypeSystemContext context, CompilationModuleGroup compilationGroup, ILProvider ilprovider, TypePreinit.TypePreinitializationPolicy policy, ReadOnlyFieldPolicy readOnlyPolicy, FlowAnnotations flowAnnotations) { diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs index f8418a8d67befc..dbcb21fc38ce19 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs @@ -43,6 +43,20 @@ public void SetCode(ObjectData data) _methodCode = data; } + internal void ResetForIncrementalCompilation() + { + _methodCode = null; + _frameInfos = null; + _gcInfo = null; + _ehInfo = null; + _debugLocInfos = null; + _debugVarInfos = null; + _debugEHClauseInfos = null; + _nonRelocationDependencies = null; + _debugInfo = null; + _localTypes = null; + } + public MethodDesc Method => _method; protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler); @@ -151,6 +165,8 @@ public void InitializeEHInfo(ObjectData ehInfo) public DebugLocInfo[] DebugLocInfos => _debugLocInfos; public DebugVarInfo[] DebugVarInfos => _debugVarInfos; public DebugEHClauseInfo[] DebugEHClauseInfos => _debugEHClauseInfos; + internal MethodDebugInformation DebugInfoForIncrementalCompilation => _debugInfo; + internal TypeDesc[] LocalTypesForIncrementalCompilation => _localTypes; public bool IsStateMachineMoveNextMethod => _debugInfo.IsStateMachineMoveNextMethod; diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs index 8992e89f2d54e4..834e3311fd1037 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs @@ -22,6 +22,9 @@ public RyuJitNodeFactory(CompilerTypeSystemContext context, CompilationModuleGro protected override bool CanFold(MethodDesc method) => _methodBodyDeduplicator?.CanFold(method) ?? false; + internal bool CanFoldMethodBodyForIncrementalCompilation(MethodDesc method) => + CanFold(method); + protected override IMethodNode CreateMethodEntrypointNode(MethodDesc method) { if (method.IsInternalCall) diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalBodyUpdate.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalBodyUpdate.cs new file mode 100644 index 00000000000000..2c0b23b4f4d9d0 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalBodyUpdate.cs @@ -0,0 +1,624 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Security.Cryptography; + +using Internal.IL; +using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; + +namespace ILCompiler +{ + internal sealed class IncrementalAssemblyBaseline + { + private IncrementalAssemblyBaseline(byte[] image, Guid mvid) + { + Image = image; + Mvid = mvid; + ImageHash = SHA256.HashData(image); + } + + internal byte[] Image { get; } + internal byte[] ImageHash { get; } + internal Guid Mvid { get; } + + internal static bool TryCreate( + EcmaModule module, + string path, + out IncrementalAssemblyBaseline baseline, + out string reason) + { + MetadataReader moduleMetadata = module.MetadataReader; + return TryCreate( + module.PEReader.GetEntireImage().GetContent().AsSpan(), + moduleMetadata.GetGuid(moduleMetadata.GetModuleDefinition().Mvid), + moduleMetadata.MethodDefinitions.Count, + path, + out baseline, + out reason); + } + + internal static bool TryCreate( + ReadOnlySpan loadedImage, + Guid loadedMvid, + int loadedMethodCount, + string path, + out IncrementalAssemblyBaseline baseline, + out string reason) + { + baseline = null; + reason = null; + byte[] image; + try + { + image = File.ReadAllBytes(path); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + reason = $"baseline-assembly-read-failed:{ex.Message}"; + return false; + } + + try + { + using var reader = new PEReader(new MemoryStream(image, writable: false)); + if (!reader.HasMetadata) + { + reason = "baseline-assembly-has-no-metadata"; + return false; + } + if (loadedImage.Length < image.Length || + !loadedImage.Slice(0, image.Length).SequenceEqual(image)) + { + reason = "baseline-input-does-not-match-loaded-module"; + return false; + } + + MetadataReader fileMetadata = reader.GetMetadataReader(); + Guid fileMvid = fileMetadata.GetGuid(fileMetadata.GetModuleDefinition().Mvid); + if (fileMvid != loadedMvid || + fileMetadata.MethodDefinitions.Count != loadedMethodCount) + { + reason = "baseline-input-does-not-match-loaded-module"; + return false; + } + + baseline = new IncrementalAssemblyBaseline(image, fileMvid); + return true; + } + catch (BadImageFormatException) + { + reason = "invalid-baseline-assembly"; + return false; + } + } + } + + internal sealed class IncrementalBodyUpdate : ILProvider + { + private readonly ILProvider _baseProvider; + private readonly Guid _baseMvid; + private readonly Dictionary _updatedMethodBodies; + + private IncrementalBodyUpdate( + ILProvider baseProvider, + Guid baseMvid, + Dictionary updatedMethodBodies) + { + _baseProvider = baseProvider; + _baseMvid = baseMvid; + _updatedMethodBodies = updatedMethodBodies; + } + + internal int ChangedMethodCount => _updatedMethodBodies.Count; + + internal IEnumerable ChangedMethodTokens => _updatedMethodBodies.Keys; + + internal static bool TryCreate( + ILProvider baseProvider, + IncrementalAssemblyBaseline baseline, + string updatedAssemblyPath, + bool allowUnchangedTarget, + out IncrementalBodyUpdate update, + out string reason) + { + update = null; + reason = null; + byte[] updatedImage; + + try + { + updatedImage = File.ReadAllBytes(updatedAssemblyPath); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + reason = $"updated-assembly-read-failed:{ex.Message}"; + return false; + } + + byte[] baseImage = baseline.Image; + if (baseImage.Length != updatedImage.Length) + { + reason = "assembly-size-changed"; + return false; + } + + using var baseReader = new PEReader(new MemoryStream(baseImage, writable: false)); + using var updatedReader = new PEReader(new MemoryStream(updatedImage, writable: false)); + if (!baseReader.HasMetadata || !updatedReader.HasMetadata) + { + reason = "assembly-has-no-metadata"; + return false; + } + + MetadataReader baseMetadata = baseReader.GetMetadataReader(); + MetadataReader updatedMetadata = updatedReader.GetMetadataReader(); + if (baseMetadata.MethodDefinitions.Count != updatedMetadata.MethodDefinitions.Count) + { + reason = "method-definition-count-changed"; + return false; + } + + Guid baseMvid = baseMetadata.GetGuid(baseMetadata.GetModuleDefinition().Mvid); + Guid updatedMvid = updatedMetadata.GetGuid(updatedMetadata.GetModuleDefinition().Mvid); + if (baseMvid != baseline.Mvid || baseMvid != updatedMvid) + { + reason = "module-version-id-changed"; + return false; + } + + byte[] sanitizedBaseImage = (byte[])baseImage.Clone(); + byte[] sanitizedUpdatedImage = (byte[])updatedImage.Clone(); + if (!TryMaskNonSemanticDirectories(baseReader, sanitizedBaseImage, out reason) || + !TryMaskNonSemanticDirectories(updatedReader, sanitizedUpdatedImage, out reason)) + { + return false; + } + + var updatedBodies = new Dictionary(); + foreach (MethodDefinitionHandle methodHandle in baseMetadata.MethodDefinitions) + { + MethodDefinition baseMethod = baseMetadata.GetMethodDefinition(methodHandle); + MethodDefinition updatedMethod = updatedMetadata.GetMethodDefinition(methodHandle); + int baseRva = baseMethod.RelativeVirtualAddress; + int updatedRva = updatedMethod.RelativeVirtualAddress; + int token = MetadataTokens.GetToken(methodHandle); + + if ((baseRva == 0) != (updatedRva == 0)) + { + reason = $"method-body-presence-changed:{token:X8}"; + return false; + } + if (baseRva == 0) + continue; + + MethodBodyBlock baseBody; + MethodBodyBlock updatedBody; + try + { + baseBody = baseReader.GetMethodBody(baseRva); + updatedBody = updatedReader.GetMethodBody(updatedRva); + } + catch (BadImageFormatException) + { + reason = $"invalid-method-body:{token:X8}"; + return false; + } + + if (baseBody.Size != updatedBody.Size) + { + reason = $"method-body-size-changed:{token:X8}"; + return false; + } + + if (!TryMaskMethodBody(baseReader.PEHeaders, sanitizedBaseImage, baseRva, baseBody.Size) || + !TryMaskMethodBody(updatedReader.PEHeaders, sanitizedUpdatedImage, updatedRva, updatedBody.Size)) + { + reason = $"method-body-location-invalid:{token:X8}"; + return false; + } + + byte[] baseIl = baseBody.GetILBytes(); + byte[] updatedIl = updatedBody.GetILBytes(); + if (!HasEquivalentMethodBodyShape(baseBody, updatedBody)) + { + reason = $"method-body-shape-changed:{token:X8}"; + return false; + } + if (baseIl.AsSpan().SequenceEqual(updatedIl)) + continue; + + string methodName = baseMetadata.GetString(baseMethod.Name).ToString(); + if (methodName is ".cctor" or ".ctor") + { + reason = $"constructor-body-changed:{token:X8}"; + return false; + } + + TypeDefinition declaringType = baseMetadata.GetTypeDefinition(baseMethod.GetDeclaringType()); + if (baseMethod.GetGenericParameters().Count != 0 || + declaringType.GetGenericParameters().Count != 0) + { + reason = $"generic-method-or-type-changed:{token:X8}"; + return false; + } + + if (!IsDependencyNeutralConstantChange(baseIl, updatedIl)) + { + reason = $"changed-body-is-not-constant-only:{token:X8}"; + return false; + } + + updatedBodies.Add(token, updatedIl); + } + + if (!sanitizedBaseImage.AsSpan().SequenceEqual(sanitizedUpdatedImage)) + { + reason = "non-method-assembly-content-changed"; + return false; + } + if (updatedBodies.Count == 0 && !allowUnchangedTarget) + { + reason = "no-method-body-changes"; + return false; + } + + update = new IncrementalBodyUpdate(baseProvider, baseMvid, updatedBodies); + return true; + } + + internal bool IsChangedMethod(MethodDesc method) + { + return TryGetTargetMethodToken(method, out _, out int token) && + _updatedMethodBodies.ContainsKey(token); + } + + internal static HashSet GetAffectedMethodTokens( + IEnumerable currentTokens, + IEnumerable previousTokens) + { + var result = new HashSet(currentTokens); + if (previousTokens is not null) + result.UnionWith(previousTokens); + return result; + } + + internal bool CanOverlayChangedMethod(MethodDesc method, out string reason) + { + if (!IsChangedMethod(method)) + { + reason = null; + return true; + } + + MethodIL original = _baseProvider.GetMethodIL(method); + if (!IsOverlayableMethodIL(original)) + { + reason = original is null ? + "changed-method-has-no-il" : + "changed-method-il-is-not-overlayable"; + return false; + } + + reason = null; + return true; + } + + internal static bool IsOverlayableMethodIL(MethodIL methodIL) => + methodIL?.GetMethodILDefinition() is EcmaMethodIL; + + public override MethodIL GetMethodIL(MethodDesc method) + { + MethodIL original = _baseProvider.GetMethodIL(method); + if (original is null || + !TryGetTargetMethodToken(method, out EcmaMethod ecmaMethod, out int token) || + !_updatedMethodBodies.TryGetValue(token, out byte[] updatedIl)) + { + return original; + } + + MethodIL originalDefinition = original.GetMethodILDefinition(); + if (originalDefinition is not EcmaMethodIL) + { + throw new InvalidOperationException( + $"Incremental IL cannot overlay method token {token:X8}."); + } + + var updatedDefinition = new UpdatedMethodIL(ecmaMethod, originalDefinition, updatedIl); + return method == ecmaMethod ? + updatedDefinition : + new InstantiatedMethodIL(method, updatedDefinition); + } + + internal bool TryGetTargetMethodToken( + MethodDesc method, + out EcmaMethod ecmaMethod, + out int token) + { + ecmaMethod = method.GetTypicalMethodDefinition() as EcmaMethod; + if (ecmaMethod is null) + { + token = 0; + return false; + } + + MetadataReader metadata = ecmaMethod.Module.MetadataReader; + Guid mvid = metadata.GetGuid(metadata.GetModuleDefinition().Mvid); + if (mvid != _baseMvid) + { + token = 0; + return false; + } + + token = MetadataTokens.GetToken(ecmaMethod.Handle); + return true; + } + + private static bool HasEquivalentMethodBodyShape(MethodBodyBlock left, MethodBodyBlock right) + { + if (left.MaxStack != right.MaxStack || + left.LocalVariablesInitialized != right.LocalVariablesInitialized || + left.LocalSignature != right.LocalSignature || + left.ExceptionRegions.Length != right.ExceptionRegions.Length) + { + return false; + } + + for (int i = 0; i < left.ExceptionRegions.Length; i++) + { + ExceptionRegion leftRegion = left.ExceptionRegions[i]; + ExceptionRegion rightRegion = right.ExceptionRegions[i]; + if (leftRegion.Kind != rightRegion.Kind || + leftRegion.TryOffset != rightRegion.TryOffset || + leftRegion.TryLength != rightRegion.TryLength || + leftRegion.HandlerOffset != rightRegion.HandlerOffset || + leftRegion.HandlerLength != rightRegion.HandlerLength || + leftRegion.CatchType != rightRegion.CatchType || + leftRegion.FilterOffset != rightRegion.FilterOffset) + { + return false; + } + } + + return true; + } + + internal static bool IsDependencyNeutralConstantChange(ReadOnlySpan baseIl, ReadOnlySpan updatedIl) + { + var baseReader = new ILReader(baseIl); + var updatedReader = new ILReader(updatedIl); + bool foundChangedConstant = false; + + while (baseReader.HasNext && updatedReader.HasNext) + { + ILOpcode baseOpcode = baseReader.ReadILOpcode(); + ILOpcode updatedOpcode = updatedReader.ReadILOpcode(); + if (baseOpcode != updatedOpcode || !IsAllowedLeafOpcode(baseOpcode)) + return false; + + int baseOperandStart = baseReader.Offset; + int updatedOperandStart = updatedReader.Offset; + baseReader.Skip(baseOpcode); + updatedReader.Skip(updatedOpcode); + + ReadOnlySpan baseOperand = + baseIl.Slice(baseOperandStart, baseReader.Offset - baseOperandStart); + ReadOnlySpan updatedOperand = + updatedIl.Slice(updatedOperandStart, updatedReader.Offset - updatedOperandStart); + if (!baseOperand.SequenceEqual(updatedOperand)) + { + if (baseOpcode is not ( + ILOpcode.ldc_i4_s or + ILOpcode.ldc_i4 or + ILOpcode.ldc_i8 or + ILOpcode.ldc_r4 or + ILOpcode.ldc_r8)) + { + return false; + } + + foundChangedConstant = true; + } + } + + return foundChangedConstant && + !baseReader.HasNext && + !updatedReader.HasNext; + } + + private static bool IsAllowedLeafOpcode(ILOpcode opcode) + { + return opcode is + ILOpcode.nop or + ILOpcode.ldarg_0 or + ILOpcode.ldarg_1 or + ILOpcode.ldarg_2 or + ILOpcode.ldarg_3 or + ILOpcode.ldarg_s or + ILOpcode.ldarg or + ILOpcode.ldc_i4_m1 or + ILOpcode.ldc_i4_0 or + ILOpcode.ldc_i4_1 or + ILOpcode.ldc_i4_2 or + ILOpcode.ldc_i4_3 or + ILOpcode.ldc_i4_4 or + ILOpcode.ldc_i4_5 or + ILOpcode.ldc_i4_6 or + ILOpcode.ldc_i4_7 or + ILOpcode.ldc_i4_8 or + ILOpcode.ldc_i4_s or + ILOpcode.ldc_i4 or + ILOpcode.ldc_i8 or + ILOpcode.ldc_r4 or + ILOpcode.ldc_r8 or + ILOpcode.add or + ILOpcode.sub or + ILOpcode.mul or + ILOpcode.div or + ILOpcode.div_un or + ILOpcode.rem or + ILOpcode.rem_un or + ILOpcode.and or + ILOpcode.or or + ILOpcode.xor or + ILOpcode.shl or + ILOpcode.shr or + ILOpcode.shr_un or + ILOpcode.neg or + ILOpcode.not or + ILOpcode.conv_i4 or + ILOpcode.conv_i8 or + ILOpcode.conv_u4 or + ILOpcode.conv_u8 or + ILOpcode.ret; + } + + private static bool TryMaskNonSemanticDirectories( + PEReader reader, + byte[] image, + out string reason) + { + PEHeaders headers = reader.PEHeaders; + int timestampOffset = checked(headers.CoffHeaderStartOffset + sizeof(uint)); + if ((uint)timestampOffset > (uint)(image.Length - sizeof(uint))) + { + reason = "coff-header-location-invalid"; + return false; + } + image.AsSpan(timestampOffset, sizeof(uint)).Clear(); + + if (headers.PEHeader is not null) + { + const int ChecksumOffsetInPEHeader = 64; + int checksumOffset = checked(headers.PEHeaderStartOffset + ChecksumOffsetInPEHeader); + if ((uint)checksumOffset > (uint)(image.Length - sizeof(uint))) + { + reason = "pe-checksum-location-invalid"; + return false; + } + image.AsSpan(checksumOffset, sizeof(uint)).Clear(); + } + + if (headers.CorHeader is not null && + !TryMaskDirectory(headers, image, headers.CorHeader.StrongNameSignatureDirectory)) + { + reason = "strong-name-directory-location-invalid"; + return false; + } + if (headers.PEHeader is not null && + !TryMaskDirectory(headers, image, headers.PEHeader.DebugTableDirectory)) + { + reason = "debug-directory-location-invalid"; + return false; + } + + reason = null; + return true; + } + + private static bool TryMaskDirectory( + PEHeaders headers, + byte[] image, + DirectoryEntry directory) + { + if (directory.Size == 0) + return true; + + return TryRvaToFileRange( + headers, + image, + directory.RelativeVirtualAddress, + directory.Size, + out int offset) && + Clear(image, offset, directory.Size); + } + + private static bool TryMaskMethodBody( + PEHeaders headers, + byte[] image, + int relativeVirtualAddress, + int size) + { + return TryRvaToFileRange( + headers, + image, + relativeVirtualAddress, + size, + out int offset) && + Clear(image, offset, size); + } + + private static bool TryRvaToFileRange( + PEHeaders headers, + byte[] image, + int relativeVirtualAddress, + int size, + out int fileOffset) + { + fileOffset = 0; + if (relativeVirtualAddress < 0 || size < 0) + return false; + + foreach (SectionHeader section in headers.SectionHeaders) + { + long offsetInSection = (long)relativeVirtualAddress - section.VirtualAddress; + if (offsetInSection < 0 || + offsetInSection > section.SizeOfRawData || + size > section.SizeOfRawData - offsetInSection) + { + continue; + } + + long candidate = (long)section.PointerToRawData + offsetInSection; + if (candidate < 0 || + candidate > image.Length || + size > image.Length - candidate || + candidate > int.MaxValue) + { + return false; + } + + fileOffset = (int)candidate; + return true; + } + + return false; + } + + private static bool Clear(byte[] image, int offset, int size) + { + image.AsSpan(offset, size).Clear(); + return true; + } + + private sealed class UpdatedMethodIL : MethodIL + { + private readonly EcmaMethod _method; + private readonly MethodIL _original; + private readonly byte[] _updatedIl; + + internal UpdatedMethodIL(EcmaMethod method, MethodIL original, byte[] updatedIl) + { + _method = method; + _original = original; + _updatedIl = updatedIl; + } + + public override MethodDesc OwningMethod => _method; + public override int MaxStack => _original.MaxStack; + public override bool IsInitLocals => _original.IsInitLocals; + public override byte[] GetILBytes() => _updatedIl; + public override LocalVariableDefinition[] GetLocals() => _original.GetLocals(); + public override ILExceptionRegion[] GetExceptionRegions() => _original.GetExceptionRegions(); + public override object GetObject(int token, NotFoundBehavior notFoundBehavior) => + _original.GetObject(token, notFoundBehavior); + public override Internal.IL.MethodDebugInformation GetDebugInfo() => _original.GetDebugInfo(); + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalCodeStateValidator.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalCodeStateValidator.cs new file mode 100644 index 00000000000000..4ab71b97682446 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalCodeStateValidator.cs @@ -0,0 +1,315 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +using ILCompiler.DependencyAnalysis; + +using Internal.IL; +using Internal.JitInterface; +using Internal.TypeSystem; + +namespace ILCompiler +{ + internal readonly struct IncrementalDependencyEntry + { + internal IncrementalDependencyEntry(object node, string reason, bool marked) + { + Node = node; + Reason = reason; + Marked = marked; + } + + internal object Node { get; } + internal string Reason { get; } + internal bool Marked { get; } + } + + internal readonly struct IncrementalConditionalDependencyEntry + { + internal IncrementalConditionalDependencyEntry( + object node, + object otherReasonNode, + string reason, + bool nodeMarked, + bool otherReasonNodeMarked) + { + Node = node; + OtherReasonNode = otherReasonNode; + Reason = reason; + NodeMarked = nodeMarked; + OtherReasonNodeMarked = otherReasonNodeMarked; + } + + internal object Node { get; } + internal object OtherReasonNode { get; } + internal string Reason { get; } + internal bool NodeMarked { get; } + internal bool OtherReasonNodeMarked { get; } + } + + internal static class IncrementalDependencyValidator + { + internal static bool Matches( + IReadOnlyList baselineStatic, + IReadOnlyList baselineConditional, + IReadOnlyList currentStatic, + IReadOnlyList currentConditional, + out string reason) + { + for (int i = 0; i < currentStatic.Count; i++) + { + IncrementalDependencyEntry current = currentStatic[i]; + if (!current.Marked) + { + reason = $"unmarked-static-dependency:{i}"; + return false; + } + if (i >= baselineStatic.Count) + { + reason = $"static-dependency:{i}"; + return false; + } + + IncrementalDependencyEntry baseline = baselineStatic[i]; + if (!ReferenceEquals(baseline.Node, current.Node) || + !string.Equals(baseline.Reason, current.Reason, StringComparison.Ordinal)) + { + reason = $"static-dependency:{i}"; + return false; + } + } + if (baselineStatic.Count != currentStatic.Count) + { + reason = $"static-dependency-count:{baselineStatic.Count}:{currentStatic.Count}"; + return false; + } + + for (int i = 0; i < currentConditional.Count; i++) + { + IncrementalConditionalDependencyEntry current = currentConditional[i]; + if (!current.NodeMarked || !current.OtherReasonNodeMarked) + { + reason = $"unmarked-conditional-dependency:{i}"; + return false; + } + if (i >= baselineConditional.Count) + { + reason = $"conditional-dependency:{i}"; + return false; + } + + IncrementalConditionalDependencyEntry baseline = baselineConditional[i]; + if (!ReferenceEquals(baseline.Node, current.Node) || + !ReferenceEquals(baseline.OtherReasonNode, current.OtherReasonNode) || + !string.Equals(baseline.Reason, current.Reason, StringComparison.Ordinal)) + { + reason = $"conditional-dependency:{i}"; + return false; + } + } + if (baselineConditional.Count != currentConditional.Count) + { + reason = + $"conditional-dependency-count:{baselineConditional.Count}:{currentConditional.Count}"; + return false; + } + + reason = null; + return true; + } + } + + internal readonly struct IncrementalCodeState + { + internal IncrementalCodeState(MethodCodeNode node) + : this( + node.FrameInfos, + node.GCInfo, + node.EHInfo is not null, + node.DebugLocInfos, + node.DebugVarInfos, + node.DebugEHClauseInfos, + node.DebugInfoForIncrementalCompilation, + node.LocalTypesForIncrementalCompilation) + { + } + + internal IncrementalCodeState( + FrameInfo[] frameInfos, + byte[] gcInfo, + bool hasEhInfo, + DebugLocInfo[] debugLocInfos, + DebugVarInfo[] debugVarInfos, + DebugEHClauseInfo[] debugEhClauseInfos, + MethodDebugInformation debugInfo, + TypeDesc[] localTypes) + { + FrameInfos = frameInfos; + GcInfo = gcInfo; + HasEhInfo = hasEhInfo; + DebugLocInfos = debugLocInfos; + DebugVarInfos = debugVarInfos; + DebugEhClauseInfos = debugEhClauseInfos; + DebugInfo = debugInfo; + LocalTypes = localTypes; + } + + internal FrameInfo[] FrameInfos { get; } + internal byte[] GcInfo { get; } + internal bool HasEhInfo { get; } + internal DebugLocInfo[] DebugLocInfos { get; } + internal DebugVarInfo[] DebugVarInfos { get; } + internal DebugEHClauseInfo[] DebugEhClauseInfos { get; } + internal MethodDebugInformation DebugInfo { get; } + internal TypeDesc[] LocalTypes { get; } + } + + internal static class IncrementalCodeStateValidator + { + internal static bool Matches( + in IncrementalCodeState baseline, + MethodCodeNode current, + out string reason) => + Matches(baseline, new IncrementalCodeState(current), out reason); + + internal static bool Matches( + in IncrementalCodeState baseline, + in IncrementalCodeState current, + out string reason) + { + if (baseline.HasEhInfo || current.HasEhInfo) + { + reason = "exception-handling-info-present"; + return false; + } + if (!((ReadOnlySpan)baseline.GcInfo).SequenceEqual(current.GcInfo)) + { + reason = "gc-info"; + return false; + } + + FrameInfo[] currentFrames = current.FrameInfos; + int oldFrameCount = baseline.FrameInfos?.Length ?? 0; + int newFrameCount = currentFrames?.Length ?? 0; + if (oldFrameCount != newFrameCount) + { + reason = "frame-count"; + return false; + } + for (int i = 0; i < oldFrameCount; i++) + { + if (!baseline.FrameInfos[i].Equals(currentFrames[i])) + { + reason = $"frame:{i}"; + return false; + } + } + + if (!DebugLocationsMatch(baseline.DebugLocInfos, current.DebugLocInfos) || + !DebugVariablesMatch(baseline.DebugVarInfos, current.DebugVarInfos) || + !DebugEhClausesMatch(baseline.DebugEhClauseInfos, current.DebugEhClauseInfos) || + !ReferenceEquals(baseline.DebugInfo, current.DebugInfo) || + !TypesMatch(baseline.LocalTypes, current.LocalTypes)) + { + reason = "debug-info"; + return false; + } + + reason = null; + return true; + } + + private static bool DebugLocationsMatch(DebugLocInfo[] left, DebugLocInfo[] right) + { + int count = left?.Length ?? 0; + if (count != (right?.Length ?? 0)) + return false; + + for (int i = 0; i < count; i++) + { + if (left[i].NativeOffset != right[i].NativeOffset || + left[i].ILOffset != right[i].ILOffset) + { + return false; + } + } + + return true; + } + + private static bool DebugVariablesMatch(DebugVarInfo[] left, DebugVarInfo[] right) + { + int count = left?.Length ?? 0; + if (count != (right?.Length ?? 0)) + return false; + + for (int i = 0; i < count; i++) + { + if (left[i].VarNumber != right[i].VarNumber) + return false; + + DebugVarRangeInfo[] leftRanges = left[i].Ranges; + DebugVarRangeInfo[] rightRanges = right[i].Ranges; + int rangeCount = leftRanges?.Length ?? 0; + if (rangeCount != (rightRanges?.Length ?? 0)) + return false; + + for (int j = 0; j < rangeCount; j++) + { + DebugVarRangeInfo leftRange = leftRanges[j]; + DebugVarRangeInfo rightRange = rightRanges[j]; + VarLoc leftLocation = leftRange.VarLoc; + VarLoc rightLocation = rightRange.VarLoc; + if (leftRange.StartOffset != rightRange.StartOffset || + leftRange.EndOffset != rightRange.EndOffset || + leftLocation.A != rightLocation.A || + leftLocation.B != rightLocation.B || + leftLocation.C != rightLocation.C || + leftLocation.D != rightLocation.D) + { + return false; + } + } + } + + return true; + } + + private static bool DebugEhClausesMatch(DebugEHClauseInfo[] left, DebugEHClauseInfo[] right) + { + int count = left?.Length ?? 0; + if (count != (right?.Length ?? 0)) + return false; + + for (int i = 0; i < count; i++) + { + if (left[i].TryOffset != right[i].TryOffset || + left[i].TryLength != right[i].TryLength || + left[i].HandlerOffset != right[i].HandlerOffset || + left[i].HandlerLength != right[i].HandlerLength) + { + return false; + } + } + + return true; + } + + private static bool TypesMatch(TypeDesc[] left, TypeDesc[] right) + { + int count = left?.Length ?? 0; + if (count != (right?.Length ?? 0)) + return false; + + for (int i = 0; i < count; i++) + { + if (!ReferenceEquals(left[i], right[i])) + return false; + } + + return true; + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalCompilationOptions.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalCompilationOptions.cs new file mode 100644 index 00000000000000..8c361f336ae30c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalCompilationOptions.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +using Internal.JitInterface; +using Internal.TypeSystem; + +namespace ILCompiler +{ + internal readonly struct IncrementalUpdateRequest + { + internal IncrementalUpdateRequest(string updatedAssemblyPath, string outputObjectPath) + { + UpdatedAssemblyPath = updatedAssemblyPath; + OutputObjectPath = outputObjectPath; + } + + internal string UpdatedAssemblyPath { get; } + internal string OutputObjectPath { get; } + } + + internal readonly struct IncrementalUpdateResult + { + internal IncrementalUpdateResult( + bool succeeded, + string reason, + int changedMethodCount, + int recompiledMethodCount, + long patchedByteCount) + { + Succeeded = succeeded; + Reason = reason; + ChangedMethodCount = changedMethodCount; + RecompiledMethodCount = recompiledMethodCount; + PatchedByteCount = patchedByteCount; + } + + internal bool Succeeded { get; } + internal bool RequiresCleanCompilation => !Succeeded; + internal string Reason { get; } + internal int ChangedMethodCount { get; } + internal int RecompiledMethodCount { get; } + internal long PatchedByteCount { get; } + } + + internal sealed class IncrementalCompilationOptions + { + private IncrementalCompilationOptions(IncrementalUpdateRequest[] updates) + { + Updates = updates; + } + + internal IncrementalUpdateRequest[] Updates { get; } + + internal static bool IsEnvironmentRequested => + Environment.GetEnvironmentVariable(IncrementalFailureContract.EnableVariable) is not null || + Environment.GetEnvironmentVariable(IncrementalFailureContract.UpdatedAssembliesVariable) is not null || + Environment.GetEnvironmentVariable(IncrementalFailureContract.OutputObjectsVariable) is not null; + + internal static IncrementalCompilationOptions ReadEnvironment() + { + string enabled = Environment.GetEnvironmentVariable(IncrementalFailureContract.EnableVariable); + string updatedAssemblies = Environment.GetEnvironmentVariable(IncrementalFailureContract.UpdatedAssembliesVariable); + string outputObjects = Environment.GetEnvironmentVariable(IncrementalFailureContract.OutputObjectsVariable); + if (enabled is null && updatedAssemblies is null && outputObjects is null) + return null; + if (!string.Equals(enabled, "1", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"{IncrementalFailureContract.EnableVariable} must be exactly '1' when incremental experiment variables are present."); + } + + if (string.IsNullOrWhiteSpace(updatedAssemblies) || + string.IsNullOrWhiteSpace(outputObjects)) + { + throw new InvalidOperationException( + $"The incremental experiment requires {IncrementalFailureContract.UpdatedAssembliesVariable} and " + + $"{IncrementalFailureContract.OutputObjectsVariable}."); + } + + string[] assemblies = updatedAssemblies.Split( + Path.PathSeparator, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + string[] outputs = outputObjects.Split( + Path.PathSeparator, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (assemblies.Length == 0 || assemblies.Length != outputs.Length) + { + throw new InvalidOperationException( + "Incremental updated-assembly and output-object lists must have the same nonzero length."); + } + + var outputPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var requests = new IncrementalUpdateRequest[assemblies.Length]; + for (int i = 0; i < requests.Length; i++) + { + string assemblyPath; + string outputPath; + try + { + assemblyPath = Path.GetFullPath(assemblies[i]); + outputPath = Path.GetFullPath(outputs[i]); + } + catch (Exception ex) when (ex is ArgumentException or + IOException or + UnauthorizedAccessException or + NotSupportedException or + System.Security.SecurityException) + { + throw new InvalidOperationException( + $"An incremental input or output path is invalid: {ex.Message}", + ex); + } + + if (!outputPaths.Add(outputPath)) + { + throw new InvalidOperationException( + $"The incremental output path occurs more than once: '{outputPath}'."); + } + + requests[i] = new IncrementalUpdateRequest(assemblyPath, outputPath); + } + + return new IncrementalCompilationOptions(requests); + } + } + + internal static class IncrementalCompilationFingerprint + { + internal static bool TryCreate( + CompilerTypeSystemContext context, + InstructionSetSupport instructionSetSupport, + string configurationDescription, + out byte[] hash, + out string reason) + { + hash = null; + reason = null; + + try + { + using var data = new MemoryStream(); + using (var writer = new BinaryWriter(data, Encoding.UTF8, leaveOpen: true)) + { + writer.Write("NativeAOT-incremental-fast-coff-v1"); + TargetDetails target = context.Target; + writer.Write((int)target.Architecture); + writer.Write((int)target.OperatingSystem); + writer.Write((int)target.Abi); + writer.Write((int)target.MaximumSimdVectorLength); + writer.Write(configurationDescription); + WriteInstructionSets(writer, instructionSetSupport.SupportedFlags); + WriteInstructionSets(writer, instructionSetSupport.ExplicitlyUnsupportedFlags); + WriteInstructionSets(writer, instructionSetSupport.OptimisticFlags); + WriteInstructionSets(writer, instructionSetSupport.NonSpecifiableFlags); + WriteFiles(writer, "input", context.InputFilePaths); + WriteFiles(writer, "reference", context.ReferenceFilePaths); + WriteRelevantEnvironment(writer); + } + + hash = SHA256.HashData(data.GetBuffer().AsSpan(0, checked((int)data.Length))); + return true; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + reason = $"configuration-fingerprint-failed:{ex.Message}"; + return false; + } + } + + private static void WriteInstructionSets(BinaryWriter writer, InstructionSetFlags flags) + { + var values = new List(); + foreach (InstructionSet instructionSet in flags) + values.Add((int)instructionSet); + + values.Sort(); + writer.Write(values.Count); + foreach (int value in values) + writer.Write(value); + } + + private static void WriteFiles( + BinaryWriter writer, + string role, + IReadOnlyDictionary files) + { + var entries = new List>(files); + entries.Sort((left, right) => StringComparer.Ordinal.Compare(left.Key, right.Key)); + writer.Write(role); + writer.Write(entries.Count); + foreach (KeyValuePair entry in entries) + { + string path = Path.GetFullPath(entry.Value); + writer.Write(entry.Key); + writer.Write(path); + using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + writer.Write(stream.Length); + writer.Write(SHA256.HashData(stream)); + } + } + + private static void WriteRelevantEnvironment(BinaryWriter writer) + { + var entries = new List>(); + foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) + { + string key = (string)entry.Key; + if ((key.StartsWith("COMPlus_", StringComparison.OrdinalIgnoreCase) || + key.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)) && + !key.StartsWith("DOTNET_ILC_INCREMENTAL", StringComparison.OrdinalIgnoreCase)) + { + entries.Add(new KeyValuePair(key, (string)entry.Value)); + } + } + + entries.Sort((left, right) => StringComparer.OrdinalIgnoreCase.Compare(left.Key, right.Key)); + writer.Write(entries.Count); + foreach (KeyValuePair entry in entries) + { + writer.Write(entry.Key.ToUpperInvariant()); + writer.Write(entry.Value); + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalObjectPatch.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalObjectPatch.cs new file mode 100644 index 00000000000000..081d250359bb51 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/IncrementalObjectPatch.cs @@ -0,0 +1,737 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security; +using System.Security.Cryptography; +using Internal.Text; +using ILCompiler.DependencyAnalysis; +using ILCompiler.ObjectWriter; +using ObjectData = ILCompiler.DependencyAnalysis.ObjectNode.ObjectData; + +namespace ILCompiler +{ + internal sealed class IncrementalObjectLayout + { + private readonly Dictionary _entries; + private string _failureReason; + + internal IncrementalObjectLayout(IReadOnlyCollection nodes) + { + _entries = new Dictionary(nodes.Count); + foreach (ObjectNode node in nodes) + { + if (!_entries.TryAdd(node, null)) + { + _failureReason = $"The incremental candidate '{node.GetType().Name}' occurs more than once."; + break; + } + } + } + + internal string FailureReason => _failureReason; + internal IEnumerable Entries => _entries.Values; + + internal void RecordNode( + object nodeObject, + int sectionIndex, + long sectionOffset, + object dataObject, + bool isComdat) + { + if (nodeObject is not ObjectNode node || dataObject is not ObjectData data) + { + _failureReason ??= "The object writer supplied an unsupported incremental emission record."; + return; + } + + if (!_entries.TryGetValue(node, out Entry existing)) + return; + + if (existing is not null) + { + _failureReason ??= $"The incremental candidate '{node.GetType().Name}' was emitted more than once."; + return; + } + + _entries[node] = new Entry( + node, + sectionIndex, + sectionOffset, + data.Alignment, + isComdat, + (byte[])data.Data.Clone(), + data.Relocs is null ? Array.Empty() : (Relocation[])data.Relocs.Clone(), + data.DefinedSymbols is null ? + Array.Empty() : + (ISymbolDefinitionNode[])data.DefinedSymbols.Clone()); + } + + internal void Complete(Func resolver) + { + if (_failureReason is not null) + return; + + var ranges = new List(_entries.Count); + foreach (KeyValuePair pair in _entries) + { + Entry entry = pair.Value; + if (entry is null) + { + _failureReason = $"The incremental candidate '{pair.Key.GetType().Name}' was not emitted."; + return; + } + + long? fileOffset = resolver( + entry.SectionIndex, + entry.SectionOffset, + entry.Data.Length); + if (!fileOffset.HasValue) + { + _failureReason = $"The object-file location of '{pair.Key.GetType().Name}' could not be resolved."; + return; + } + + entry.FileOffset = fileOffset.GetValueOrDefault(); + ranges.Add(entry); + } + + ranges.Sort((left, right) => left.FileOffset.CompareTo(right.FileOffset)); + for (int i = 1; i < ranges.Count; i++) + { + Entry previous = ranges[i - 1]; + Entry current = ranges[i]; + if (current.FileOffset < checked(previous.FileOffset + previous.Data.Length)) + { + _failureReason = "Incremental candidate object ranges overlap."; + return; + } + } + } + + internal bool TryGetEntry(ObjectNode node, out Entry entry) => _entries.TryGetValue(node, out entry); + + internal sealed class Entry + { + internal Entry( + ObjectNode node, + int sectionIndex, + long sectionOffset, + int alignment, + bool isComdat, + byte[] data, + Relocation[] relocations, + ISymbolDefinitionNode[] definedSymbols) + { + Node = node; + SectionIndex = sectionIndex; + SectionOffset = sectionOffset; + Alignment = alignment; + IsComdat = isComdat; + Data = data; + Relocations = relocations; + DefinedSymbols = definedSymbols; + RelocationTargetOffsets = new int[relocations.Length]; + for (int i = 0; i < relocations.Length; i++) + RelocationTargetOffsets[i] = relocations[i].Target.Offset; + + DefinedSymbolOffsets = new int[definedSymbols.Length]; + for (int i = 0; i < definedSymbols.Length; i++) + DefinedSymbolOffsets[i] = definedSymbols[i].Offset; + } + + internal ObjectNode Node { get; } + internal int SectionIndex { get; } + internal long SectionOffset { get; } + internal int Alignment { get; } + internal bool IsComdat { get; } + internal byte[] Data { get; } + internal Relocation[] Relocations { get; } + internal int[] RelocationTargetOffsets { get; } + internal ISymbolDefinitionNode[] DefinedSymbols { get; } + internal int[] DefinedSymbolOffsets { get; } + internal long FileOffset { get; set; } + } + } + + internal sealed class IncrementalObjectBaseline : IDisposable + { + private readonly FileStream _baseline; + private readonly IncrementalObjectLayout _layout; + private readonly byte[] _assemblyHash; + private readonly byte[] _configurationHash; + private readonly long _length; + private bool _disposed; + + private IncrementalObjectBaseline( + FileStream baseline, + IncrementalObjectLayout layout, + byte[] assemblyHash, + byte[] configurationHash, + long length) + { + _baseline = baseline; + _layout = layout; + _assemblyHash = (byte[])assemblyHash.Clone(); + _configurationHash = (byte[])configurationHash.Clone(); + _length = length; + } + + internal static bool TryOpenLocked( + string objectFilePath, + IncrementalObjectLayout layout, + long emittedObjectLength, + byte[] emittedObjectHash, + byte[] assemblyHash, + byte[] configurationHash, + out IncrementalObjectBaseline baseline, + out string reason) + { + baseline = null; + reason = null; + FileStream stream = null; + + if (layout.FailureReason is not null) + { + reason = layout.FailureReason; + return false; + } + + try + { + stream = new FileStream(objectFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + if (stream.Length != emittedObjectLength) + { + reason = "The baseline object length changed after emission."; + return false; + } + + byte[] actualHash = SHA256.HashData(stream); + if (!CryptographicOperations.FixedTimeEquals(actualHash, emittedObjectHash)) + { + reason = "The baseline object hash changed after emission."; + return false; + } + + foreach (IncrementalObjectLayout.Entry entry in layout.Entries) + { + if (entry.IsComdat) + { + reason = "COMDAT method bodies are not supported by incremental object patching."; + return false; + } + + if (!TryValidateBaselineFragment(stream, entry, out reason)) + return false; + } + + stream.Position = 0; + baseline = new IncrementalObjectBaseline( + stream, + layout, + assemblyHash, + configurationHash, + emittedObjectLength); + stream = null; + return true; + } + catch (Exception ex) when (IsExpectedFileException(ex)) + { + reason = $"The baseline object could not be locked and verified: {ex.Message}"; + return false; + } + finally + { + stream?.Dispose(); + } + } + + internal bool TryStagePatchedObject( + string outputObjectPath, + IReadOnlyCollection nodes, + NodeFactory factory, + byte[] assemblyHash, + byte[] configurationHash, + out IncrementalStagedObject stagedObject, + out long patchedByteCount, + out string reason) + { + ObjectDisposedException.ThrowIf(_disposed, this); + stagedObject = null; + patchedByteCount = 0; + reason = null; + + if (!CryptographicOperations.FixedTimeEquals(_assemblyHash, assemblyHash) || + !CryptographicOperations.FixedTimeEquals(_configurationHash, configurationHash)) + { + reason = "The baseline assembly or compilation configuration does not match."; + return false; + } + + string outputPath = Path.GetFullPath(outputObjectPath); + if (File.Exists(outputPath)) + { + reason = "The incremental output path already exists."; + return false; + } + + if (!TryBuildPatches(nodes, factory, out List patches, out reason)) + return false; + + string directory = Path.GetDirectoryName(outputPath); + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) + { + reason = "The incremental output directory does not exist."; + return false; + } + + string temporaryPath = Path.Combine( + directory, + $".{Path.GetFileName(outputPath)}.{Guid.NewGuid():N}.tmp"); + + try + { + using (var output = new FileStream( + temporaryPath, + FileMode.CreateNew, + FileAccess.ReadWrite, + FileShare.None)) + { + _baseline.Position = 0; + _baseline.CopyTo(output); + if (output.Length != _length) + { + reason = "The copied baseline object has an unexpected length."; + return false; + } + + foreach (Patch patch in patches) + { + output.Position = patch.FileOffset; + output.Write(patch.Data); + patchedByteCount += patch.Data.Length; + } + + if (output.Length != _length) + { + reason = "Patching changed the object-file length."; + return false; + } + + output.Flush(flushToDisk: true); + } + + stagedObject = new IncrementalStagedObject(temporaryPath, outputPath); + temporaryPath = null; + return true; + } + catch (Exception ex) when (IsExpectedFileException(ex)) + { + reason = $"The incremental object could not be staged: {ex.Message}"; + return false; + } + finally + { + if (temporaryPath is not null) + { + try + { + File.Delete(temporaryPath); + } + catch (Exception ex) when (IsExpectedFileException(ex)) + { + reason = AppendFailure( + reason, + $"The staged incremental object could not be deleted: {ex.Message}"); + } + } + + } + } + + internal bool TryWritePatchedObject( + string outputObjectPath, + IReadOnlyCollection nodes, + NodeFactory factory, + byte[] assemblyHash, + byte[] configurationHash, + out long patchedByteCount, + out string reason) + { + if (!TryStagePatchedObject( + outputObjectPath, + nodes, + factory, + assemblyHash, + configurationHash, + out IncrementalStagedObject stagedObject, + out patchedByteCount, + out reason)) + { + return false; + } + + if (stagedObject.TryPublish(out reason)) + return true; + + if (!stagedObject.TryCleanup(out string cleanupFailure)) + reason = AppendFailure(reason, cleanupFailure); + return false; + } + + public void Dispose() + { + if (!_disposed) + { + _baseline.Dispose(); + _disposed = true; + } + } + + private bool TryBuildPatches( + IReadOnlyCollection nodes, + NodeFactory factory, + out List patches, + out string reason) + { + patches = new List(); + reason = null; + var seen = new HashSet(); + + foreach (ObjectNode node in nodes) + { + if (!seen.Add(node)) + { + reason = "An incremental object node was supplied more than once."; + return false; + } + + if (!_layout.TryGetEntry(node, out IncrementalObjectLayout.Entry entry) || entry is null) + { + reason = "An incremental object node was not present in the baseline layout."; + return false; + } + + if (entry.IsComdat) + { + reason = "COMDAT method bodies are not supported by incremental object patching."; + return false; + } + + ObjectData current = node.GetData(factory, relocsOnly: false); + if (current.Alignment != entry.Alignment || + current.Data.Length != entry.Data.Length) + { + reason = "An incremental method changed its object size or alignment."; + return false; + } + + if (!HaveSameSymbols( + entry.DefinedSymbols, + entry.DefinedSymbolOffsets, + current.DefinedSymbols ?? Array.Empty())) + { + reason = "An incremental method changed its defined symbols."; + return false; + } + + if (!TryCreateRelocationMask( + entry.Data.Length, + entry.Relocations, + entry.RelocationTargetOffsets, + current.Relocs ?? Array.Empty(), + entry.Data, + current.Data, + out bool[] relocationMask, + out reason)) + { + return false; + } + + int runStart = -1; + for (int i = 0; i < current.Data.Length; i++) + { + bool differs = current.Data[i] != entry.Data[i]; + if (differs && relocationMask[i]) + { + reason = "An incremental method changed a relocation addend."; + return false; + } + + if (differs && runStart < 0) + { + runStart = i; + } + else if ((!differs || relocationMask[i]) && runStart >= 0) + { + patches.Add(CreatePatch(entry, current.Data, runStart, i)); + runStart = -1; + } + } + + if (runStart >= 0) + patches.Add(CreatePatch(entry, current.Data, runStart, current.Data.Length)); + } + + patches.Sort((left, right) => left.FileOffset.CompareTo(right.FileOffset)); + for (int i = 1; i < patches.Count; i++) + { + Patch previous = patches[i - 1]; + Patch current = patches[i]; + if (current.FileOffset < checked(previous.FileOffset + previous.Data.Length)) + { + reason = "Incremental object patches overlap."; + return false; + } + } + + return true; + } + + private static Patch CreatePatch( + IncrementalObjectLayout.Entry entry, + byte[] data, + int start, + int end) + { + var bytes = new byte[end - start]; + Array.Copy(data, start, bytes, 0, bytes.Length); + return new Patch(checked(entry.FileOffset + start), bytes); + } + + private static bool TryValidateBaselineFragment( + FileStream stream, + IncrementalObjectLayout.Entry entry, + out string reason) + { + if (entry.FileOffset < 0 || + entry.Data.Length > stream.Length || + entry.FileOffset > stream.Length - entry.Data.Length) + { + reason = "A recorded incremental object range is outside the baseline object."; + return false; + } + + if (!TryCreateRelocationMask( + entry.Data.Length, + entry.Relocations, + entry.RelocationTargetOffsets, + entry.Relocations, + entry.Data, + entry.Data, + out bool[] relocationMask, + out reason)) + { + return false; + } + + var actual = new byte[entry.Data.Length]; + stream.Position = entry.FileOffset; + stream.ReadExactly(actual); + for (int i = 0; i < actual.Length; i++) + { + if (!relocationMask[i] && actual[i] != entry.Data[i]) + { + reason = "A non-relocation byte in the baseline object does not match recorded object data."; + return false; + } + } + + return true; + } + + private static bool TryCreateRelocationMask( + int dataLength, + Relocation[] baseline, + int[] baselineTargetOffsets, + Relocation[] current, + byte[] baselineData, + byte[] currentData, + out bool[] mask, + out string reason) + { + mask = null; + reason = null; + if (baseline.Length != current.Length) + { + reason = "An incremental method changed its relocation count."; + return false; + } + + mask = new bool[dataLength]; + for (int i = 0; i < baseline.Length; i++) + { + Relocation left = baseline[i]; + Relocation right = current[i]; + if (left.RelocType != right.RelocType || + left.Offset != right.Offset || + !ReferenceEquals(left.Target, right.Target) || + baselineTargetOffsets[i] != right.Target.Offset) + { + reason = "An incremental method changed a relocation."; + return false; + } + + if (!TryGetWindowsX64RelocationWidth(left.RelocType, out int width) || + left.Offset < 0 || + left.Offset > dataLength - width) + { + reason = $"Relocation type '{left.RelocType}' or its range is unsupported."; + return false; + } + + for (int j = 0; j < width; j++) + { + int offset = left.Offset + j; + if (mask[offset]) + { + reason = "Relocation spans overlap."; + return false; + } + + mask[offset] = true; + if (baselineData[offset] != currentData[offset]) + { + reason = "An incremental method changed a relocation addend."; + return false; + } + } + } + + return true; + } + + internal static bool TryGetWindowsX64RelocationWidth(RelocType relocType, out int width) + { + switch (relocType) + { + case RelocType.IMAGE_REL_BASED_ABSOLUTE: + case RelocType.IMAGE_REL_BASED_ADDR32NB: + case RelocType.IMAGE_REL_BASED_HIGHLOW: + case RelocType.IMAGE_REL_BASED_REL32: + case RelocType.IMAGE_REL_BASED_RELPTR32: + case RelocType.IMAGE_REL_SECREL: + width = 4; + return true; + + case RelocType.IMAGE_REL_BASED_DIR64: + width = 8; + return true; + + default: + width = 0; + return false; + } + } + + private static bool HaveSameSymbols( + ISymbolDefinitionNode[] baseline, + int[] baselineOffsets, + ISymbolDefinitionNode[] current) + { + if (baseline.Length != current.Length) + return false; + + for (int i = 0; i < baseline.Length; i++) + { + if (!ReferenceEquals(baseline[i], current[i]) || + baselineOffsets[i] != current[i].Offset) + return false; + } + + return true; + } + + private static bool IsExpectedFileException(Exception ex) => + ex is IOException or + UnauthorizedAccessException or + DirectoryNotFoundException or + PathTooLongException or + NotSupportedException or + SecurityException; + + internal static string AppendFailure(string reason, string additionalFailure) => + string.IsNullOrEmpty(reason) ? additionalFailure : $"{reason} {additionalFailure}"; + + private readonly struct Patch + { + internal Patch(long fileOffset, byte[] data) + { + FileOffset = fileOffset; + Data = data; + } + + internal long FileOffset { get; } + internal byte[] Data { get; } + } + } + + internal sealed class IncrementalStagedObject + { + private string _temporaryPath; + private bool _published; + + internal IncrementalStagedObject(string temporaryPath, string outputPath) + { + _temporaryPath = temporaryPath; + OutputPath = outputPath; + } + + internal string OutputPath { get; } + internal string TemporaryPath => _temporaryPath; + + internal bool TryPublish(out string reason) + { + reason = null; + if (_published || _temporaryPath is null) + { + reason = "The incremental object is not staged."; + return false; + } + + try + { + File.Move(_temporaryPath, OutputPath, overwrite: false); + _temporaryPath = null; + _published = true; + return true; + } + catch (Exception ex) when (IsExpectedFileException(ex)) + { + reason = $"The staged incremental object could not be published: {ex.Message}"; + return false; + } + } + + internal bool TryCleanup(out string reason) + { + reason = null; + string path = _published ? OutputPath : _temporaryPath; + if (path is null) + return true; + + try + { + File.Delete(path); + _temporaryPath = null; + _published = false; + return true; + } + catch (Exception ex) when (IsExpectedFileException(ex)) + { + reason = $"The incremental object '{path}' could not be deleted: {ex.Message}"; + return false; + } + } + + private static bool IsExpectedFileException(Exception ex) => + ex is IOException or + UnauthorizedAccessException or + DirectoryNotFoundException or + PathTooLongException or + NotSupportedException or + SecurityException; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.Incremental.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.Incremental.cs new file mode 100644 index 00000000000000..de6d76bd250755 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.Incremental.cs @@ -0,0 +1,568 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata.Ecma335; + +using ILCompiler.DependencyAnalysis; +using ILCompiler.DependencyAnalysisFramework; +using ILCompiler.ObjectWriter; + +using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; + +namespace ILCompiler +{ + public sealed partial class RyuJitCompilation + { + private sealed class IncrementalCompilationSession : IDisposable + { + private readonly RyuJitCompilation _owner; + private readonly IncrementalAssemblyBaseline _assemblyBaseline; + private readonly IncrementalBodyUpdate[] _updates; + private readonly List _candidates; + private readonly Dictionary _methodBaselines; + private readonly byte[] _configurationHash; + private IncrementalObjectBaseline _objectBaseline; + private IncrementalBodyUpdate _previousUpdate; + private SessionState _state; + + private IncrementalCompilationSession( + RyuJitCompilation owner, + IncrementalAssemblyBaseline assemblyBaseline, + IncrementalBodyUpdate[] updates, + List candidates, + Dictionary methodBaselines, + byte[] configurationHash, + IncrementalObjectLayout layout) + { + _owner = owner; + _assemblyBaseline = assemblyBaseline; + _updates = updates; + _candidates = candidates; + _methodBaselines = methodBaselines; + _configurationHash = configurationHash; + Layout = layout; + _state = SessionState.Prepared; + } + + internal IncrementalObjectLayout Layout { get; } + + internal static bool TryPrepare( + RyuJitCompilation owner, + string baselineObjectPath, + IReadOnlyCollection> nodes, + ObjectWritingOptions objectWritingOptions, + ObjectDumper dumper, + out IncrementalCompilationSession session, + out string reason) + { + session = null; + reason = null; + if (objectWritingOptions != ObjectWritingOptions.GenerateUnwindInfo) + { + reason = "unsupported-object-writing-options"; + return false; + } + if (dumper is not null) + { + reason = "object-dump-output-is-unsupported"; + return false; + } + + CompilerTypeSystemContext context = owner.TypeSystemContext; + if (context.InputFilePaths.Count != 1) + { + reason = "single-primary-input-required"; + return false; + } + + string inputSimpleName = null; + string inputPath = null; + foreach (KeyValuePair input in context.InputFilePaths) + { + inputSimpleName = input.Key; + inputPath = Path.GetFullPath(input.Value); + } + + EcmaModule inputModule = context.GetModuleForSimpleName(inputSimpleName); + if (!IncrementalAssemblyBaseline.TryCreate( + inputModule, + inputPath, + out IncrementalAssemblyBaseline assemblyBaseline, + out reason)) + { + return false; + } + if (!IncrementalCompilationFingerprint.TryCreate( + context, + owner.InstructionSetSupport, + owner._incrementalConfigurationDescription, + out byte[] configurationHash, + out reason)) + { + return false; + } + + string baselineOutput = Path.GetFullPath(baselineObjectPath); + string finalBaselineOutput = baselineOutput.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase) ? + baselineOutput.Substring(0, baselineOutput.Length - ".tmp".Length) : + baselineOutput; + var updateObjects = new IncrementalBodyUpdate[owner._incrementalOptions.Updates.Length]; + var changedTokens = new HashSet(); + for (int i = 0; i < updateObjects.Length; i++) + { + IncrementalUpdateRequest request = owner._incrementalOptions.Updates[i]; + string updatedAssembly = Path.GetFullPath(request.UpdatedAssemblyPath); + string outputObject = Path.GetFullPath(request.OutputObjectPath); + if (string.Equals(updatedAssembly, inputPath, StringComparison.OrdinalIgnoreCase)) + { + reason = "updated-assembly-must-not-be-the-baseline-input"; + return false; + } + if (string.Equals(outputObject, baselineOutput, StringComparison.OrdinalIgnoreCase) || + string.Equals(outputObject, finalBaselineOutput, StringComparison.OrdinalIgnoreCase) || + string.Equals(outputObject, inputPath, StringComparison.OrdinalIgnoreCase) || + string.Equals(outputObject, updatedAssembly, StringComparison.OrdinalIgnoreCase)) + { + reason = "incremental-output-path-collides-with-an-input-or-baseline-output"; + return false; + } + if (File.Exists(outputObject)) + { + reason = $"incremental-output-already-exists:{outputObject}"; + return false; + } + + try + { + if (!IncrementalBodyUpdate.TryCreate( + owner._incrementalBaseILProvider, + assemblyBaseline, + updatedAssembly, + allowUnchangedTarget: i != 0, + out IncrementalBodyUpdate update, + out reason)) + { + return false; + } + + updateObjects[i] = update; + foreach (int token in update.ChangedMethodTokens) + changedTokens.Add(token); + } + catch (BadImageFormatException) + { + reason = $"invalid-updated-assembly:{updatedAssembly}"; + return false; + } + } + + var candidateByToken = new Dictionary(); + foreach (DependencyNodeCore node in nodes) + { + if (node is not MethodCodeNode methodCodeNode || + methodCodeNode.Method.GetTypicalMethodDefinition() is not EcmaMethod ecmaMethod || + ecmaMethod.Module != inputModule) + { + continue; + } + + int token = MetadataTokens.GetToken(ecmaMethod.Handle); + if (!changedTokens.Contains(token)) + continue; + + if (!candidateByToken.TryAdd(token, methodCodeNode)) + { + reason = $"changed-method-has-multiple-code-nodes:{token:X8}"; + return false; + } + } + + if (candidateByToken.Count != changedTokens.Count) + { + reason = $"changed-method-node-count-mismatch:{changedTokens.Count}:{candidateByToken.Count}"; + return false; + } + + var candidates = new List(candidateByToken.Count); + var baselines = new Dictionary(candidateByToken.Count); + foreach (KeyValuePair pair in candidateByToken) + { + MethodCodeNode candidate = pair.Value; + MethodDesc method = candidate.Method; + if (!candidate.Marked || + method.HasInstantiation || + method.OwningType.HasInstantiation || + candidate.IsSpecialUnboxingThunk || + candidate.HasConditionalStaticDependencies || + candidate.EHInfo is not null || + ((RyuJitNodeFactory)owner.NodeFactory).CanFoldMethodBodyForIncrementalCompilation(method)) + { + reason = $"changed-method-node-is-not-eligible:{method}"; + return false; + } + + for (int i = 0; i < updateObjects.Length; i++) + { + if (updateObjects[i].IsChangedMethod(method) && + !updateObjects[i].CanOverlayChangedMethod(method, out reason)) + { + reason = $"{reason}:{method}"; + return false; + } + } + + if (!MethodBaseline.TryCapture(candidate, owner.NodeFactory, out MethodBaseline baseline, out reason)) + { + reason = $"{reason}:{method}"; + return false; + } + + candidates.Add(candidate); + baselines.Add(candidate, baseline); + } + + var objectNodes = new List(candidates.Count); + foreach (MethodCodeNode candidate in candidates) + objectNodes.Add(candidate); + + session = new IncrementalCompilationSession( + owner, + assemblyBaseline, + updateObjects, + candidates, + baselines, + configurationHash, + new IncrementalObjectLayout(objectNodes)); + return true; + } + + internal bool TryAttachBaseline( + string baselineObjectPath, + long emittedObjectLength, + byte[] emittedObjectHash, + out string reason) + { + if (_state != SessionState.Prepared) + { + reason = "incremental-session-is-not-prepared"; + return false; + } + + if (!IncrementalObjectBaseline.TryOpenLocked( + baselineObjectPath, + Layout, + emittedObjectLength, + emittedObjectHash, + _assemblyBaseline.ImageHash, + _configurationHash, + out _objectBaseline, + out reason)) + { + return false; + } + + _state = SessionState.Ready; + return true; + } + + internal IncrementalUpdateResult EmitUpdate( + int updateIndex, + out IncrementalStagedObject stagedObject) + { + stagedObject = null; + if (_state != SessionState.Ready) + { + return Failure("incremental-session-is-not-ready", 0, 0); + } + if ((uint)updateIndex >= (uint)_updates.Length) + return Failure("incremental-update-index-is-invalid", 0, 0); + + IncrementalBodyUpdate update = _updates[updateIndex]; + HashSet dirtyTokens = IncrementalBodyUpdate.GetAffectedMethodTokens( + update.ChangedMethodTokens, + _previousUpdate?.ChangedMethodTokens); + var dirtyMethods = new List(); + foreach (MethodCodeNode candidate in _candidates) + { + if (update.TryGetTargetMethodToken(candidate.Method, out _, out int token) && + dirtyTokens.Contains(token)) + { + dirtyMethods.Add(candidate); + } + } + + int expectedCount = dirtyTokens.Count; + if (dirtyMethods.Count != expectedCount) + { + return Failure( + $"changed-method-node-count-mismatch:{expectedCount}:{dirtyMethods.Count}", + update.ChangedMethodCount, + dirtyMethods.Count); + } + + if (File.Exists(_owner._incrementalOptions.Updates[updateIndex].OutputObjectPath)) + { + return Failure( + "incremental-output-already-exists", + update.ChangedMethodCount, + dirtyMethods.Count); + } + + if (!IncrementalCompilationFingerprint.TryCreate( + _owner.TypeSystemContext, + _owner.InstructionSetSupport, + _owner._incrementalConfigurationDescription, + out byte[] currentConfigurationHash, + out string reason)) + { + return Failure(reason, update.ChangedMethodCount, dirtyMethods.Count); + } + if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals( + _configurationHash, + currentConfigurationHash)) + { + return Failure( + "compilation-configuration-changed", + update.ChangedMethodCount, + dirtyMethods.Count); + } + + _state = SessionState.Updating; + try + { + _owner._incrementalCurrentILProvider = update; + foreach (MethodCodeNode method in dirtyMethods) + method.ResetForIncrementalCompilation(); + + _owner.CompileSingleThreaded(dirtyMethods); + foreach (MethodCodeNode method in dirtyMethods) + { + if (!_methodBaselines[method].Matches(method, _owner.NodeFactory, out reason)) + { + return PoisonedFailure( + $"incremental-method-state-changed:{method.Method}:{reason}", + update.ChangedMethodCount, + dirtyMethods.Count); + } + } + + var objectNodes = new List(dirtyMethods.Count); + foreach (MethodCodeNode method in dirtyMethods) + objectNodes.Add(method); + + if (!_objectBaseline.TryStagePatchedObject( + _owner._incrementalOptions.Updates[updateIndex].OutputObjectPath, + objectNodes, + _owner.NodeFactory, + _assemblyBaseline.ImageHash, + currentConfigurationHash, + out stagedObject, + out long patchedByteCount, + out reason)) + { + return PoisonedFailure( + $"fast-object-patch-rejected:{reason}", + update.ChangedMethodCount, + dirtyMethods.Count); + } + + _previousUpdate = update; + _state = SessionState.Ready; + return new IncrementalUpdateResult( + succeeded: true, + reason: null, + update.ChangedMethodCount, + dirtyMethods.Count, + patchedByteCount); + } + catch + { + _state = SessionState.Poisoned; + throw; + } + finally + { + _owner._incrementalCurrentILProvider = null; + } + } + + internal void Poison() + { + _owner._incrementalCurrentILProvider = null; + if (_state != SessionState.Disposed) + _state = SessionState.Poisoned; + } + + public void Dispose() + { + _owner._incrementalCurrentILProvider = null; + _objectBaseline?.Dispose(); + _state = SessionState.Disposed; + } + + private IncrementalUpdateResult Failure( + string reason, + int changedMethodCount, + int recompiledMethodCount) + { + return new IncrementalUpdateResult( + succeeded: false, + reason, + changedMethodCount, + recompiledMethodCount, + patchedByteCount: 0); + } + + private IncrementalUpdateResult PoisonedFailure( + string reason, + int changedMethodCount, + int recompiledMethodCount) + { + _state = SessionState.Poisoned; + return Failure(reason, changedMethodCount, recompiledMethodCount); + } + + private enum SessionState + { + Prepared, + Ready, + Updating, + Poisoned, + Disposed, + } + } + + private sealed class MethodBaseline + { + private readonly IncrementalDependencyEntry[] _staticDependencies; + private readonly IncrementalConditionalDependencyEntry[] _conditionalDependencies; + private readonly IncrementalCodeState _codeState; + + private MethodBaseline( + IncrementalDependencyEntry[] staticDependencies, + IncrementalConditionalDependencyEntry[] conditionalDependencies, + in IncrementalCodeState codeState) + { + _staticDependencies = staticDependencies; + _conditionalDependencies = conditionalDependencies; + _codeState = codeState; + } + + internal static bool TryCapture( + MethodCodeNode node, + NodeFactory factory, + out MethodBaseline baseline, + out string reason) + { + baseline = null; + if (!node.Marked) + { + reason = "method-node-is-not-marked"; + return false; + } + + var staticDependencies = new List(); + foreach (DependencyNodeCore.DependencyListEntry dependency in + node.GetStaticDependencies(factory)) + { + if (!dependency.Node.Marked) + { + reason = $"unmarked-static-dependency:{staticDependencies.Count}"; + return false; + } + + staticDependencies.Add(new IncrementalDependencyEntry( + dependency.Node, + dependency.Reason, + dependency.Node.Marked)); + } + + var conditionalDependencies = new List(); + foreach (DependencyNodeCore.CombinedDependencyListEntry dependency in + node.GetConditionalStaticDependencies(factory)) + { + if (!dependency.Node.Marked || + (dependency.OtherReasonNode is not null && !dependency.OtherReasonNode.Marked)) + { + reason = $"unmarked-conditional-dependency:{conditionalDependencies.Count}"; + return false; + } + + conditionalDependencies.Add(new IncrementalConditionalDependencyEntry( + dependency.Node, + dependency.OtherReasonNode, + dependency.Reason, + dependency.Node.Marked, + dependency.OtherReasonNode?.Marked ?? true)); + } + + IncrementalCodeState state = new IncrementalCodeState(node); + baseline = new MethodBaseline( + staticDependencies.ToArray(), + conditionalDependencies.ToArray(), + state); + reason = null; + return true; + } + + internal bool Matches(MethodCodeNode node, NodeFactory factory, out string reason) + { + if (!node.Marked) + { + reason = "method-node-is-not-marked"; + return false; + } + + var staticDependencies = new List(); + foreach (DependencyNodeCore.DependencyListEntry dependency in + node.GetStaticDependencies(factory)) + { + staticDependencies.Add(new IncrementalDependencyEntry( + dependency.Node, + dependency.Reason, + dependency.Node.Marked)); + } + + var conditionalDependencies = new List(); + foreach (DependencyNodeCore.CombinedDependencyListEntry dependency in + node.GetConditionalStaticDependencies(factory)) + { + conditionalDependencies.Add(new IncrementalConditionalDependencyEntry( + dependency.Node, + dependency.OtherReasonNode, + dependency.Reason, + dependency.Node.Marked, + dependency.OtherReasonNode?.Marked ?? true)); + } + + if (!IncrementalDependencyValidator.Matches( + _staticDependencies, + _conditionalDependencies, + staticDependencies, + conditionalDependencies, + out reason)) + { + return false; + } + + return IncrementalCodeStateValidator.Matches(_codeState, node, out reason); + } + } + } + + internal sealed class IncrementalCompilationException : Exception + { + internal IncrementalCompilationException(string reason) + : base($"Incremental compilation requires a clean compilation: {reason}") + { + Reason = reason; + HResult = IncrementalFailureContract.FailureHResult; + } + + internal string Reason { get; } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.cs index 5c635bf69e3b4c..b3d52a92426cea 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilation.cs @@ -4,6 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.ExceptionServices; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -20,7 +23,7 @@ namespace ILCompiler { - public sealed class RyuJitCompilation : Compilation + public sealed partial class RyuJitCompilation : Compilation { private readonly ConditionalWeakTable _corinfos = new ConditionalWeakTable(); internal readonly RyuJitCompilationOptions _compilationOptions; @@ -29,6 +32,11 @@ public sealed class RyuJitCompilation : Compilation private readonly MethodImportationErrorProvider _methodImportationErrorProvider; private readonly ReadOnlyFieldPolicy _readOnlyFieldPolicy; private readonly int _parallelism; + private readonly ILProvider _incrementalBaseILProvider; + private ILProvider _incrementalCurrentILProvider; + private bool _incrementalOutputsPublished; + private readonly IncrementalCompilationOptions _incrementalOptions; + private readonly string _incrementalConfigurationDescription; public InstructionSetSupport InstructionSetSupport { get; } @@ -48,7 +56,9 @@ internal RyuJitCompilation( MethodLayoutAlgorithm methodLayoutAlgorithm, FileLayoutAlgorithm fileLayoutAlgorithm, int parallelism, - string orderFile) + string orderFile, + IncrementalCompilationOptions incrementalOptions, + string incrementalConfigurationDescription) : base(dependencyGraph, nodeFactory, roots, ilProvider, debugInformationProvider, inliningPolicy, logger) { _compilationOptions = options; @@ -63,6 +73,9 @@ internal RyuJitCompilation( _parallelism = parallelism; _fileLayoutOptimizer = new FileLayoutOptimizer(logger, methodLayoutAlgorithm, fileLayoutAlgorithm, profileDataManager, nodeFactory, orderFile); + _incrementalBaseILProvider = new BaselineILProvider(this); + _incrementalOptions = incrementalOptions; + _incrementalConfigurationDescription = incrementalConfigurationDescription; } public ProfileDataManager ProfileData => _profileDataManager; @@ -116,7 +129,192 @@ protected override void CompileInternal(string outputFile, ObjectDumper dumper) if ((_compilationOptions & RyuJitCompilationOptions.ControlFlowGuardAnnotations) != 0) options |= ObjectWritingOptions.ControlFlowGuard; - ObjectWriter.ObjectWriter.EmitObject(outputFile, nodes, NodeFactory, options, dumper, _logger); + if (_incrementalOptions is null) + { + ObjectWriter.ObjectWriter.EmitObject(outputFile, nodes, NodeFactory, options, dumper, _logger); + return; + } + + if (!IncrementalCompilationSession.TryPrepare( + this, + outputFile, + nodes, + options, + dumper, + out IncrementalCompilationSession session, + out string reason)) + { + throw new IncrementalCompilationException(reason); + } + + using (session) + { + var stagedObjects = new List(); + try + { + EmitIncrementalObject( + outputFile, + nodes, + options, + dumper, + session.Layout, + out long emittedObjectLength, + out byte[] emittedObjectHash); + + if (!session.TryAttachBaseline( + outputFile, + emittedObjectLength, + emittedObjectHash, + out reason)) + { + throw new IncrementalCompilationException(reason); + } + + for (int i = 0; i < _incrementalOptions.Updates.Length; i++) + { + IncrementalUpdateResult result = session.EmitUpdate( + i, + out IncrementalStagedObject stagedObject); + if (!result.Succeeded) + throw new IncrementalCompilationException(result.Reason); + stagedObjects.Add(stagedObject); + + _logger.LogMessage( + $"Incremental update {i + 1} staged: " + + $"{result.ChangedMethodCount} changed definitions, " + + $"{result.RecompiledMethodCount} recompiled nodes, " + + $"{result.PatchedByteCount} patched bytes."); + } + + TypeSystemContext.LogWarnings(_logger); + if (_logger.HasLoggedErrors) + { + throw new IncrementalCompilationException( + "compiler diagnostics prevent incremental output publication"); + } + + foreach (IncrementalStagedObject stagedObject in stagedObjects) + { + if (!stagedObject.TryPublish(out reason)) + { + session.Poison(); + throw new IncrementalCompilationException( + $"incremental-output-publication-failed:{reason}"); + } + } + + _incrementalOutputsPublished = true; + _logger.LogMessage("All incremental outputs were published."); + stagedObjects.Clear(); + } + catch (IncrementalCompilationException ex) + { + session.Poison(); + string cleanupFailure = CleanupIncrementalOutputs(stagedObjects); + throw cleanupFailure is null ? + ex : + new IncrementalCompilationException( + IncrementalObjectBaseline.AppendFailure(ex.Reason, cleanupFailure)); + } + catch (Exception ex) + { + session.Poison(); + string cleanupFailure = CleanupIncrementalOutputs(stagedObjects); + if (cleanupFailure is not null) + { + throw new AggregateException( + ex, + new IOException(cleanupFailure)); + } + + throw; + } + } + } + + public override MethodIL GetMethodIL(MethodDesc method) => + _incrementalCurrentILProvider?.GetMethodIL(method) ?? base.GetMethodIL(method); + + private MethodIL GetBaselineMethodIL(MethodDesc method) => base.GetMethodIL(method); + + internal bool GetIncrementalOutputPublicationStatus() => + _incrementalOutputsPublished; + + private void EmitIncrementalObject( + string outputFile, + IReadOnlyCollection nodes, + ObjectWritingOptions options, + ObjectDumper dumper, + IncrementalObjectLayout layout, + out long objectLength, + out byte[] objectHash) + { + object[] arguments = + { + outputFile, + nodes, + NodeFactory, + options, + dumper, + _logger, + new Action(layout.RecordNode), + new Action>(layout.Complete), + null, + null, + }; + + try + { + IncrementalObjectWriterAccess.EmitMethod.Invoke(null, arguments); + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw; + } + + objectLength = (long)arguments[8]; + objectHash = (byte[])arguments[9]; + } + + private static string CleanupIncrementalOutputs( + IReadOnlyList stagedObjects) + { + string reason = null; + foreach (IncrementalStagedObject stagedObject in stagedObjects) + { + if (!stagedObject.TryCleanup(out string cleanupFailure)) + reason = IncrementalObjectBaseline.AppendFailure(reason, cleanupFailure); + } + + return reason; + } + + private sealed class BaselineILProvider : ILProvider + { + private readonly RyuJitCompilation _compilation; + + internal BaselineILProvider(RyuJitCompilation compilation) + { + _compilation = compilation; + } + + public override MethodIL GetMethodIL(MethodDesc method) => + _compilation.GetBaselineMethodIL(method); + } + + private static class IncrementalObjectWriterAccess + { + // ILCompiler.Compiler and ILCompiler.RyuJit compile overlapping linked sources, so + // InternalsVisibleTo would make duplicate internal types ambiguous. Keep this + // experiment-only boundary internal and fail loudly if the reflected seam changes. + internal static readonly MethodInfo EmitMethod = + typeof(ObjectWriter.ObjectWriter).GetMethod( + "EmitObjectForIncrementalCompilation", + BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new MissingMethodException( + typeof(ObjectWriter.ObjectWriter).FullName, + "EmitObjectForIncrementalCompilation"); } protected override void ComputeDependencyNodeDependencies(List> obj) diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs index bbec7799701b31..9376cfc3f5aa0d 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs @@ -3,12 +3,16 @@ using System; using System.Collections.Generic; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; using ILCompiler.DependencyAnalysis; using ILCompiler.DependencyAnalysisFramework; using Internal.IL; using Internal.JitInterface; +using Internal.TypeSystem; namespace ILCompiler { @@ -23,6 +27,7 @@ public sealed class RyuJitCompilationBuilder : CompilationBuilder private ProfileDataManager _profileDataManager; private string _orderFile; private string _jitPath; + private string _incrementalCommandLineConfiguration; public RyuJitCompilationBuilder(CompilerTypeSystemContext context, CompilationModuleGroup group) : base(context, group, @@ -55,6 +60,49 @@ public RyuJitCompilationBuilder FileLayoutAlgorithms(MethodLayoutAlgorithm metho return this; } + internal void SetIncrementalCommandLineConfiguration(string configurationDescription) + { + ArgumentException.ThrowIfNullOrEmpty(configurationDescription); + _incrementalCommandLineConfiguration = configurationDescription; + } + + internal static bool TryValidateIncrementalCommandLineConfiguration( + bool exports, + bool dependencyGraph, + bool scannerDependencyGraph, + bool ilDump, + bool map, + bool mstat, + bool sourceLink, + bool metadataLog, + bool reachability, + out string description, + out string reason) + { + description = + $"exports={exports};dependencygraph={dependencyGraph};" + + $"scannerdependencygraph={scannerDependencyGraph};ildump={ilDump};map={map};" + + $"mstat={mstat};sourcelink={sourceLink};metadatalog={metadataLog};" + + $"reachability={reachability}"; + + if (exports || + dependencyGraph || + scannerDependencyGraph || + ilDump || + map || + mstat || + sourceLink || + metadataLog || + reachability) + { + reason = "exports, dependency logs, IL dumps, map/mstat/SourceLink/metadata logs, and reachability instrumentation are unsupported"; + return false; + } + + reason = null; + return true; + } + public override CompilationBuilder UseBackendOptions(IEnumerable options) { var builder = default(ArrayBuilder>); @@ -93,6 +141,41 @@ protected override ILProvider GetILProvider() public override ICompilation ToCompilation() { + IncrementalCompilationOptions incrementalOptions; + try + { + incrementalOptions = IncrementalCompilationOptions.ReadEnvironment(); + } + catch (InvalidOperationException ex) + { + throw new IncrementalCompilationException(ex.Message); + } + string incrementalConfiguration = null; + if (incrementalOptions is not null) + { + if (_incrementalCommandLineConfiguration is null) + { + throw new IncrementalCompilationException( + "the driver did not validate and fingerprint command-line configuration"); + } + + if (!IncrementalBuilderAccess.TryGetBaseConfiguration( + this, + out string baseConfiguration, + out string baseReason)) + { + throw new IncrementalCompilationException(baseReason); + } + + string unsupportedReason = GetIncrementalCompilationUnsupportedReason(); + if (unsupportedReason is not null) + throw new IncrementalCompilationException(unsupportedReason); + + incrementalConfiguration = + $"{baseConfiguration};{GetIncrementalConfigurationDescription()};" + + _incrementalCommandLineConfiguration; + } + ArrayBuilder jitFlagBuilder = default(ArrayBuilder); switch (_optimizationMode) @@ -166,7 +249,106 @@ public override ICompilation ToCompilation() _methodLayoutAlgorithm, _fileLayoutAlgorithm, _parallelism, - _orderFile); + _orderFile, + incrementalOptions, + incrementalConfiguration); + } + + private string GetIncrementalCompilationUnsupportedReason() + { + if (!OperatingSystem.IsWindows() || + _context.Target.OperatingSystem != TargetOS.Windows || + _context.Target.Architecture != TargetArchitecture.X64 || + _context.Target.Abi != TargetAbi.NativeAot) + { + return "only a Windows host targeting Windows x64 NativeAOT COFF is supported"; + } + if (_context.InputFilePaths.Count != 1 || + _compilationGroup is not SingleFileCompilationModuleGroup) + { + return "a single primary input in a single-file compilation is required"; + } + if (_optimizationMode != OptimizationMode.None) + return "optimization must be disabled"; + if (_parallelism != 1) + return "parallelism must be exactly one"; + if (_inliningPolicy is not null) + return "the scanner and custom inlining policies must be disabled"; + if (_methodBodyFolding != MethodBodyFoldingMode.None) + return "method-body folding must be disabled"; + if (_debugInformationProvider is not NullDebugInformationProvider) + return "native debug information must be disabled"; + if (_mitigationOptions != 0 || _dehydrate || _useDwarf5 || _resilient) + return "security mitigations, dehydration, resilience, and DWARF options are unsupported"; + if (_profileDataManager is not null || _orderFile is not null) + return "profile data and custom file ordering are unsupported"; + if (_methodLayoutAlgorithm != MethodLayoutAlgorithm.DefaultSort || + _fileLayoutAlgorithm != FileLayoutAlgorithm.DefaultSort) + { + return "default method and file layouts are required"; + } + if (_ryujitOptions.Length != 0 || _jitPath is not null) + return "custom JIT options and JIT paths are unsupported"; + + return null; + } + + private string GetIncrementalConfigurationDescription() + { + var builder = new StringBuilder(); + builder.Append("optimization=").Append((int)_optimizationMode); + builder.Append(";parallelism=").Append(_parallelism); + builder.Append(";group=").Append(_compilationGroup.GetType().AssemblyQualifiedName); + builder.Append(";metadata=").Append(_metadataManager.GetType().AssemblyQualifiedName); + builder.Append(";interop=").Append(_interopStubManager.GetType().AssemblyQualifiedName); + builder.Append(";vtable=").Append(_vtableSliceProvider.GetType().AssemblyQualifiedName); + builder.Append(";dictionary=").Append(_dictionaryLayoutProvider.GetType().AssemblyQualifiedName); + builder.Append(";threadstatics=").Append(_inlinedThreadStatics.GetType().AssemblyQualifiedName); + builder.Append(";devirtualization=").Append(_devirtualizationManager.GetType().AssemblyQualifiedName); + builder.Append(";typemap=").Append(_typeMapManager.GetType().AssemblyQualifiedName); + builder.Append(";readonly=").Append(_readOnlyFieldPolicy.GetType().AssemblyQualifiedName); + builder.Append(";methodimport=").Append(_methodImportationErrorProvider.GetType().AssemblyQualifiedName); + builder.Append(";methodlayout=").Append((int)_methodLayoutAlgorithm); + builder.Append(";filelayout=").Append((int)_fileLayoutAlgorithm); + builder.Append(";folding=").Append((int)_methodBodyFolding); + builder.Append(";mitigations=").Append((int)_mitigationOptions); + return builder.ToString(); + } + + private static class IncrementalBuilderAccess + { + // ILCompiler.Compiler and ILCompiler.RyuJit compile overlapping linked sources, so + // InternalsVisibleTo would make duplicate internal types ambiguous. Keep this + // experiment-only boundary internal and fail loudly if the reflected seam changes. + private static readonly MethodInfo s_getBaseConfiguration = + typeof(CompilationBuilder).GetMethod( + "TryGetIncrementalBaseConfiguration", + BindingFlags.Instance | BindingFlags.NonPublic) ?? + throw new MissingMethodException( + typeof(CompilationBuilder).FullName, + "TryGetIncrementalBaseConfiguration"); + + internal static bool TryGetBaseConfiguration( + CompilationBuilder builder, + out string description, + out string reason) + { + object[] arguments = { null, null }; + bool result; + try + { + result = (bool)s_getBaseConfiguration.Invoke(builder, arguments); + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw; + } + + description = (string)arguments[0]; + reason = (string)arguments[1]; + return result; + } } } } diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/ILCompiler.RyuJit.csproj b/src/coreclr/tools/aot/ILCompiler.RyuJit/ILCompiler.RyuJit.csproj index 8fa7b6865ae5ca..62a031a67c02a2 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/ILCompiler.RyuJit.csproj +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/ILCompiler.RyuJit.csproj @@ -18,6 +18,10 @@ false + + + + @@ -28,9 +32,17 @@ + + + + + + + Compiler\IncrementalFailureContract.cs + diff --git a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs index 875bbeaf73b9e7..015bfb217b50d1 100644 --- a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs +++ b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs @@ -302,6 +302,13 @@ public ILCompilerRootCommand(string[] args) : base(".NET Native IL Compiler") try { string makeReproPath = result.GetValue(MakeReproPath); + if (makeReproPath is not null && + IncrementalDriverException.IsEnvironmentRequested) + { + throw new IncrementalDriverException( + "repro-package output is unsupported"); + } + if (makeReproPath != null) { // Create a repro package in the specified path @@ -318,6 +325,16 @@ public ILCompilerRootCommand(string[] args) : base(".NET Native IL Compiler") return new Program(this).Run(); } + catch (Exception ex) when (IncrementalFailureContract.IsCleanFallbackRequested( + ex, + IncrementalDriverException.IsEnvironmentRequested)) + { + Console.Error.WriteLine( + $"ILC_INCREMENTAL_REJECTED: {ex.Message}"); + Console.Error.WriteLine( + "Run a new clean compilation with all DOTNET_ILC_INCREMENTAL* variables removed."); + return IncrementalFailureContract.CleanFallbackExitCode; + } #if DEBUG catch (CodeGenerationFailedException ex) when (DumpReproArguments(ex)) { diff --git a/src/coreclr/tools/aot/ILCompiler/IncrementalFailureContract.cs b/src/coreclr/tools/aot/ILCompiler/IncrementalFailureContract.cs new file mode 100644 index 00000000000000..37e530c8504603 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler/IncrementalFailureContract.cs @@ -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. + +using System; + +namespace ILCompiler +{ + internal static class IncrementalFailureContract + { + internal const int CleanFallbackExitCode = 85; + internal const int FailureHResult = unchecked((int)0x80131C85); + internal const string EnableVariable = "DOTNET_ILC_INCREMENTAL"; + internal const string OutputObjectsVariable = "DOTNET_ILC_INCREMENTAL_OUTPUT_OBJECTS"; + internal const string UpdatedAssembliesVariable = "DOTNET_ILC_INCREMENTAL_UPDATED_ASSEMBLIES"; + + internal static bool IsCleanFallbackRequested(Exception exception, bool isEnvironmentRequested) => + isEnvironmentRequested && exception.HResult == FailureHResult; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 34305bc9ebf504..65ab71d641dc9e 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -9,9 +9,12 @@ using System.CommandLine.Help; using System.CommandLine.Parsing; using System.IO; +using System.Reflection; using System.Reflection.Metadata; using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Text; using System.Xml; @@ -74,6 +77,13 @@ public int Run() string outputFilePath = Get(_command.OutputFilePath); if (outputFilePath == null) throw new CommandLineException("Output filename must be specified (--out )"); + bool incrementalCompilationRequested = IsIncrementalEnvironmentRequested(); + string incrementalCommandLineConfiguration = ValidateIncrementalCommandLine(); + if (incrementalCompilationRequested && File.Exists(outputFilePath)) + { + throw new IncrementalDriverException( + "the baseline output already exists"); + } var suppressedWarningCategories = new List(); if (Get(_command.NoTrimWarn)) @@ -600,6 +610,9 @@ void RunScanner() } string ilDump = Get(_command.IlDump); + string mapFileName = Get(_command.MapFileName); + string mstatFileName = Get(_command.MstatFileName); + string sourceLinkFileName = Get(_command.SourceLinkFileName); DebugInformationProvider debugInfoProvider = Get(_command.EnableDebugInfo) ? (ilDump == null ? new DebugInformationProvider() : new ILAssemblyGeneratingMethodDebugInfoProvider(ilDump, new EcmaOnlyDebugInformationProvider())) : new NullDebugInformationProvider(); @@ -632,11 +645,11 @@ void RunScanner() .UseDwarf5(Get(_command.UseDwarf5)) .UseResilience(resilient); - ICompilation compilation = builder.ToCompilation(); + ConfigureIncrementalCommandLineFingerprint( + (RyuJitCompilationBuilder)builder, + incrementalCommandLineConfiguration); - string mapFileName = Get(_command.MapFileName); - string mstatFileName = Get(_command.MstatFileName); - string sourceLinkFileName = Get(_command.SourceLinkFileName); + ICompilation compilation = builder.ToCompilation(); List dumpers = new List(); @@ -651,7 +664,41 @@ void RunScanner() // Write to a temporary file and rename on success to avoid leaving partial files on failure string tempOutputFilePath = outputFilePath + ".tmp"; - CompilationResults compilationResults = compilation.Compile(tempOutputFilePath, ObjectDumper.Compose(dumpers)); + CompilationResults compilationResults; + try + { + compilationResults = compilation.Compile( + tempOutputFilePath, + ObjectDumper.Compose(dumpers)); + } + catch (Exception ex) when (incrementalCompilationRequested) + { + string cleanupFailure; + try + { + cleanupFailure = CleanupIncrementalDriverOutputs( + tempOutputFilePath, + IncrementalCompilerAccess.GetOutputPublicationStatus(compilation)); + } + catch (Exception cleanupException) + { + throw new AggregateException(ex, cleanupException); + } + + if (ex.HResult == IncrementalFailureContract.FailureHResult) + { + if (cleanupFailure is null) + throw; + + throw new IncrementalDriverException( + AppendFailure(ex.Message, cleanupFailure)); + } + + if (cleanupFailure is not null) + throw new AggregateException(ex, new IOException(cleanupFailure)); + throw; + } + string exportsFile = Get(_command.ExportsFile); if (exportsFile != null) { @@ -669,7 +716,8 @@ void RunScanner() defFileWriter.EmitExportedMethods(); } - typeSystemContext.LogWarnings(logger); + if (!incrementalCompilationRequested) + typeSystemContext.LogWarnings(logger); if (dgmlLogFileName != null) compilationResults.WriteDependencyLog(dgmlLogFileName); @@ -730,6 +778,16 @@ static bool IsRelatedToInvalidInput(MethodDesc method) // and return error code to avoid misleading build systems into thinking the compilation succeeded. if (logger.HasLoggedErrors) { + if (incrementalCompilationRequested) + { + string cleanupFailure = CleanupIncrementalDriverOutputs( + tempOutputFilePath, + includeUpdateOutputs: true); + throw new IncrementalDriverException(AppendFailure( + "compiler diagnostics prevent incremental output publication", + cleanupFailure)); + } + try { File.Delete(tempOutputFilePath); @@ -744,7 +802,38 @@ static bool IsRelatedToInvalidInput(MethodDesc method) } // Rename the temporary file to the final output file - File.Move(tempOutputFilePath, outputFilePath, overwrite: true); + try + { + File.Move( + tempOutputFilePath, + outputFilePath, + overwrite: !incrementalCompilationRequested); + } + catch (Exception ex) when (incrementalCompilationRequested) + { + string cleanupFailure; + try + { + cleanupFailure = CleanupIncrementalDriverOutputs( + tempOutputFilePath, + includeUpdateOutputs: true); + } + catch (Exception cleanupException) + { + throw new AggregateException(ex, cleanupException); + } + + if (IsExpectedFileException(ex)) + { + throw new IncrementalDriverException(AppendFailure( + $"the baseline object could not be published: {ex.Message}", + cleanupFailure)); + } + + if (cleanupFailure is not null) + throw new AggregateException(ex, new IOException(cleanupFailure)); + throw; + } return 0; } @@ -838,6 +927,211 @@ private static IEnumerable ProcessWarningCodes(IEnumerable warningC private T Get(Option option) => _command.Result.GetValue(option); + private static bool IsIncrementalEnvironmentRequested() => + IncrementalDriverException.IsEnvironmentRequested; + + private static string CleanupIncrementalDriverOutputs( + string temporaryBaselinePath, + bool includeUpdateOutputs) + { + var paths = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Path.GetFullPath(temporaryBaselinePath), + }; + if (includeUpdateOutputs) + { + string configuredOutputs = + Environment.GetEnvironmentVariable(IncrementalFailureContract.OutputObjectsVariable); + if (!string.IsNullOrWhiteSpace(configuredOutputs)) + { + foreach (string output in configuredOutputs.Split( + Path.PathSeparator, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + paths.Add(Path.GetFullPath(output)); + } + } + } + + string reason = null; + foreach (string path in paths) + { + try + { + File.Delete(path); + } + catch (Exception ex) when (IsExpectedFileException(ex)) + { + reason = AppendFailure( + reason, + $"The incremental driver output '{path}' could not be deleted: {ex.Message}"); + } + } + + return reason; + } + + private static bool IsExpectedFileException(Exception ex) => + ex is IOException or + UnauthorizedAccessException or + DirectoryNotFoundException or + PathTooLongException or + NotSupportedException or + System.Security.SecurityException; + + private static string AppendFailure(string reason, string additionalFailure) => + string.IsNullOrEmpty(additionalFailure) ? + reason : + string.IsNullOrEmpty(reason) ? additionalFailure : $"{reason} {additionalFailure}"; + + private string ValidateIncrementalCommandLine() + { + if (!IsIncrementalEnvironmentRequested()) + return null; + + if (!IncrementalCompilerAccess.TryValidateCommandLineConfiguration( + exports: + Get(_command.ExportsFile) is not null || + Get(_command.ExportDynamicSymbols).Length != 0 || + Get(_command.ExportUnmanagedEntryPoints) || + Get(_command.UnmanagedEntryPointsAssemblies).Length != 0, + dependencyGraph: + Get(_command.DgmlLogFileName) is not null || + Get(_command.GenerateFullDgmlLog), + scannerDependencyGraph: + Get(_command.ScanDgmlLogFileName) is not null || + Get(_command.GenerateFullScanDgmlLog), + ilDump: Get(_command.IlDump) is not null, + map: Get(_command.MapFileName) is not null, + mstat: Get(_command.MstatFileName) is not null, + sourceLink: Get(_command.SourceLinkFileName) is not null, + metadataLog: Get(_command.MetadataLogFileName) is not null, + reachability: + Get(_command.InstrumentReachability) || + Get(_command.UseReachability) is not null, + out string description, + out string reason)) + { + throw new IncrementalDriverException(reason); + } + + return description; + } + + private void ConfigureIncrementalCommandLineFingerprint( + RyuJitCompilationBuilder builder, + string incrementalCommandLineConfiguration) + { + if (incrementalCommandLineConfiguration is null) + return; + + var commandLine = new StringBuilder(); + foreach (Token token in _command.Result.Tokens) + { + commandLine.Append(token.Value); + commandLine.Append('\0'); + } + + string fingerprint = Convert.ToHexString( + SHA256.HashData(Encoding.UTF8.GetBytes(commandLine.ToString()))); + IncrementalCompilerAccess.SetCommandLineConfiguration( + builder, + $"{incrementalCommandLineConfiguration};commandline={fingerprint}"); + } + + private static class IncrementalCompilerAccess + { + // ilc and ILCompiler.RyuJit compile overlapping linked sources, so InternalsVisibleTo + // would make duplicate internal types ambiguous. Keep this experiment-only boundary + // internal and fail loudly if any reflected seam changes. + private static readonly MethodInfo s_validateCommandLineConfiguration = + typeof(RyuJitCompilationBuilder).GetMethod( + "TryValidateIncrementalCommandLineConfiguration", + BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new MissingMethodException( + typeof(RyuJitCompilationBuilder).FullName, + "TryValidateIncrementalCommandLineConfiguration"); + + private static readonly MethodInfo s_setCommandLineConfiguration = + typeof(RyuJitCompilationBuilder).GetMethod( + "SetIncrementalCommandLineConfiguration", + BindingFlags.NonPublic | BindingFlags.Instance) ?? + throw new MissingMethodException( + typeof(RyuJitCompilationBuilder).FullName, + "SetIncrementalCommandLineConfiguration"); + + private static readonly MethodInfo s_getOutputPublicationStatus = + typeof(RyuJitCompilation).GetMethod( + "GetIncrementalOutputPublicationStatus", + BindingFlags.NonPublic | BindingFlags.Instance) ?? + throw new MissingMethodException( + typeof(RyuJitCompilation).FullName, + "GetIncrementalOutputPublicationStatus"); + + internal static bool TryValidateCommandLineConfiguration( + bool exports, + bool dependencyGraph, + bool scannerDependencyGraph, + bool ilDump, + bool map, + bool mstat, + bool sourceLink, + bool metadataLog, + bool reachability, + out string description, + out string reason) + { + object[] arguments = + { + exports, + dependencyGraph, + scannerDependencyGraph, + ilDump, + map, + mstat, + sourceLink, + metadataLog, + reachability, + null, + null, + }; + + bool result = (bool)Invoke(s_validateCommandLineConfiguration, null, arguments); + description = (string)arguments[9]; + reason = (string)arguments[10]; + return result; + } + + internal static void SetCommandLineConfiguration( + RyuJitCompilationBuilder builder, + string configuration) + { + Invoke( + s_setCommandLineConfiguration, + builder, + new object[] { configuration }); + } + + internal static bool GetOutputPublicationStatus(ICompilation compilation) => + (bool)Invoke( + s_getOutputPublicationStatus, + compilation, + Array.Empty()); + + private static object Invoke(MethodInfo method, object instance, object[] arguments) + { + try + { + return method.Invoke(instance, arguments); + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw; + } + } + } + private static int Main(string[] args) => new ILCompilerRootCommand(args) .UseVersion() @@ -851,4 +1145,18 @@ private static int Main(string[] args) => EnableDefaultExceptionHandler = false }); } + + internal sealed class IncrementalDriverException : Exception + { + internal static bool IsEnvironmentRequested => + Environment.GetEnvironmentVariable(IncrementalFailureContract.EnableVariable) is not null || + Environment.GetEnvironmentVariable(IncrementalFailureContract.UpdatedAssembliesVariable) is not null || + Environment.GetEnvironmentVariable(IncrementalFailureContract.OutputObjectsVariable) is not null; + + internal IncrementalDriverException(string reason) + : base($"Incremental compilation requires a clean compilation: {reason}") + { + HResult = IncrementalFailureContract.FailureHResult; + } + } } diff --git a/src/coreclr/tools/aot/ILCompiler/experiments/IncrementalCompilation/README.md b/src/coreclr/tools/aot/ILCompiler/experiments/IncrementalCompilation/README.md new file mode 100644 index 00000000000000..6d9c3f1eea23bb --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler/experiments/IncrementalCompilation/README.md @@ -0,0 +1,125 @@ +# NativeAOT incremental compilation experiment + +This directory documents a disabled-by-default, internal NativeAOT experiment. It is not a +supported product feature. The implementation recompiles a prevalidated finite set of method +bodies in one retained compiler process and patches copies of the original Windows COFF object. +It does not re-emit the dependency graph or object file. + +## Activation + +Set all three environment variables for one `ilc` invocation: + +```text +DOTNET_ILC_INCREMENTAL=1 +DOTNET_ILC_INCREMENTAL_UPDATED_ASSEMBLIES=[;...] +DOTNET_ILC_INCREMENTAL_OUTPUT_OBJECTS=[;...] +``` + +The two lists use the platform path separator and must have the same nonzero length. All updates +are validated before the clean baseline object is emitted. Each requested output must not exist. +An unsupported or failed update prints `ILC_INCREMENTAL_REJECTED`, exits with code 85, and removes +all incremental outputs it created. The experiment never silently falls back. A generic driver +owns fallback: after exit 85 it starts a new process with all `DOTNET_ILC_INCREMENTAL*` variables +removed. Other exit codes are ordinary compiler failures and must not be treated as clean-fallback +requests. + +## Deliberately narrow envelope + +The experiment rejects configurations outside all of these conditions: + +- Windows x64 NativeAOT COFF, one primary input, and single-file compilation. +- `OptimizationMode.None` and exactly one compilation thread. +- Scanner, custom inlining, preinitialization, method-body folding, native debug information, + profile data, custom ordering, custom JIT options/path, CFG, resilience, dehydration, and + DWARF options disabled. +- Export files, dynamic or generated unmanaged exports, compiler and scanner DGML (including full + dependency logs), IL dumps, map/mstat/SourceLink/metadata logs, repro packages, and reachability + modes disabled. +- Default method and file layouts and only unwind-info object writing. +- Changed methods are non-generic, non-constructor leaf methods with exactly one marked + `MethodCodeNode`; no unboxing thunk, conditional dependency, folding eligibility, or EH. +- The PE length, MVID, metadata method count, every non-body byte, and every method-body shape + field remain identical after masking only timestamp, checksum, strong-name, debug-directory, + and encoded body ranges. +- Opcode streams are identical and contain only argument loads, constants, arithmetic, + bitwise/shift, simple conversions, and `ret`; only explicit integer or floating-point constant + operands may change. +- Selected COFF fragments retain exact size, alignment, symbols, relocations, addends, GC info, + frames, EH state, debug state, ordered dependencies, reasons, and marked state. Only the + explicit Windows x64 relocation-width allowlist is accepted. COMDAT, duplicate, overlapping, + or out-of-bounds ranges are rejected. + +The baseline assembly bytes come from the primary input file and are byte-verified against the +`EcmaModule` already used by the graph. The immutable configuration fingerprint includes typed +target/generics/ISA/compiler-policy values, every +primary and reference input hash, relevant process environment, and a hash of the complete parsed +command-token stream. The emitted object is bound by length and SHA-256, reopened once for +verification, and retained under a non-writable, non-deletable handle. Every output is copied from +that same verified handle, patched into a unique same-directory file, and flushed to disk. + +All requested updates are staged before any final path is visible. Publication then uses a +non-overwriting atomic rename for each file. If any rename fails, the compiler poisons the session, +attempts to delete every staged and already-published output, and reports every cleanup failure. +Filesystems do not provide a cross-file rename transaction: an external lock or permissions change +that also prevents rollback can leave part of a multi-file batch visible, and this is reported +loudly rather than hidden. Compiler diagnostics are finalized before publication, and a later +failure to publish the baseline object also triggers update-output cleanup. Incremental mode also +requires the baseline final path to be absent and publishes it without overwrite, so filesystem +aliases cannot silently replace an update output. + +Every update starts from the immutable original object. For sequential updates, the dirty set is +the union of methods changed by the current and immediately preceding request. This makes +edit/different-method/revert sequences restore baseline bytes instead of accumulating prior +patches. Any failure after IL provider replacement poisons the session permanently. + +## Differential validation + +The `IncrementalCompilation` smoke-test project contains a tiny Windows x64 fixture. The harness +changes one IL constant without changing the PE identity or body shape, performs an edit and +revert in one retained compilation, independently clean-compiles the edited assembly, and +compares complete object-file SHA-256 hashes. The project is a priority-0 NativeAOT test, so the +incremental path runs automatically when this test is built on its supported platform. + +After building `ILCompiler_publish`, run on Windows x64: + +```powershell +src\tests\build.cmd nativeaot Release test ` + nativeaot\SmokeTests\IncrementalCompilation\IncrementalCompilation.csproj +``` + +The harness writes +`artifacts\tests\coreclr\obj\windows.x64.Release\Managed\nativeaot\SmokeTests\IncrementalCompilation\IncrementalCompilation\native\incremental-differential\run.log` +and the four compared objects beside it. These build-only artifacts stay outside the Helix +payload. A local validation run took 2,424.021 ms for the clean +baseline plus retained edit/revert and 2,185.357 ms for the independent clean edited compilation. +These end-to-end totals validate correctness and do not measure isolated update latency. The +updated objects both hashed +`0BE481A1B058F826D4FCD0E013DEFC544BF4382E16CE8C5549EF94AE1F73666F`; the baseline and reverted +objects both hashed `0CB70EB6CAA77ABC4C6F120AE64C1AF3F6370A37AEC4FD54FBF8A96179E44497`. + +## Original experiment evidence + +The motivating v10.0.11 experiment reported **291.837 ms** incremental versus **640.395 s** +clean (**2,194x**) for its measured case. Its output SHA-256 was +`B3140045782498DC4A06F712C2DAA329B732D6340DFCD3D80AD4181E17844206`; it reused +**13,455,307 / 13,455,308** nodes, changed one byte, and produced a **1.57 MB** object. The first +clean baseline was still required and retained **34–36 GiB** of state. + +Those measurements describe one experimental workload and do not establish general performance, +memory, correctness, or determinism characteristics. + +## Not included and productization gaps + +The implementation intentionally excludes the v10 profiling recorder, RDM-specific drivers, +candidate/report scripts and data, clean-build optimization waves, dependency-graph +randomization, table-preservation changes, general relocation cloning, and full object +re-emission. + +Before productization this would need a supported command-line/API contract, broader target and +method coverage, cross-process cache serialization, linker-level differential and stress +coverage, configuration-schema versioning, memory-lifetime controls, diagnostics, telemetry, +and a supported clean-fallback owner. The experiment's cross-assembly calls use fail-loud +reflection shims because the compiler assemblies compile overlapping linked internal sources; +productization requires direct, compile-time-checked internal seams and removal of those shims. +It currently retains the complete clean compiler graph and requires callers to perform any clean +differential comparison separately. diff --git a/src/coreclr/tools/aot/ILCompiler/experiments/IncrementalCompilation/run-differential.ps1 b/src/coreclr/tools/aot/ILCompiler/experiments/IncrementalCompilation/run-differential.ps1 new file mode 100644 index 00000000000000..c0e3fce7ff2563 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler/experiments/IncrementalCompilation/run-differential.ps1 @@ -0,0 +1,223 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the MIT license. + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$IlcPath, + + [Parameter(Mandatory)] + [string]$ResponseFile, + + [Parameter(Mandatory)] + [string]$BaselineAssembly, + + [Parameter(Mandatory)] + [string]$OutputDirectory +) + +$ErrorActionPreference = 'Stop' + +if ($env:OS -eq 'Windows_NT' -and ![IO.Path]::HasExtension($IlcPath)) { + $IlcPath += '.exe' +} +$IlcPath = (Resolve-Path -LiteralPath $IlcPath).Path +$ResponseFile = (Resolve-Path -LiteralPath $ResponseFile).Path +$BaselineAssembly = (Resolve-Path -LiteralPath $BaselineAssembly).Path +$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +[IO.Directory]::CreateDirectory($OutputDirectory) | Out-Null + +$workDirectory = Join-Path $OutputDirectory 'incremental-differential' +if (Test-Path -LiteralPath $workDirectory) { + Remove-Item -LiteralPath $workDirectory -Recurse -Force +} +[IO.Directory]::CreateDirectory($workDirectory) | Out-Null + +$updatedAssembly = Join-Path $workDirectory 'updated.dll' +$revertedAssembly = Join-Path $workDirectory 'reverted.dll' +$baselineObject = Join-Path $workDirectory 'baseline.obj' +$incrementalUpdatedObject = Join-Path $workDirectory 'incremental-updated.obj' +$incrementalRevertedObject = Join-Path $workDirectory 'incremental-reverted.obj' +$cleanUpdatedObject = Join-Path $workDirectory 'clean-updated.obj' +$baselineResponseFile = Join-Path $workDirectory 'baseline.rsp' +$cleanResponseFile = Join-Path $workDirectory 'clean-updated.rsp' +$logPath = Join-Path $workDirectory 'run.log' + +$image = [IO.File]::ReadAllBytes($BaselineAssembly) +$oldConstant = [BitConverter]::GetBytes([int]0x61234567) +$newConstant = [BitConverter]::GetBytes([int]0x61234568) +$match = -1 +for ($i = 0; $i -le $image.Length - $oldConstant.Length; $i++) { + $equal = $true + for ($j = 0; $j -lt $oldConstant.Length; $j++) { + if ($image[$i + $j] -ne $oldConstant[$j]) { + $equal = $false + break + } + } + if ($equal) { + if ($match -ge 0) { + throw 'The fixture constant is not unique in the baseline assembly.' + } + $match = $i + } +} +if ($match -lt 0) { + throw 'The fixture constant was not found in the baseline assembly.' +} +[Array]::Copy($newConstant, 0, $image, $match, $newConstant.Length) +[IO.File]::WriteAllBytes($updatedAssembly, $image) +[IO.File]::Copy($BaselineAssembly, $revertedAssembly) + +function Get-NormalizedPath([string]$value) { + $trimmed = $value.Trim().Trim('"') + try { + return [IO.Path]::GetFullPath($trimmed) + } + catch { + return $null + } +} + +function New-ResponseFile( + [string]$path, + [string]$inputAssembly, + [string]$outputObject) { + $foundInput = $false + $foundOutput = $false + $lines = [Collections.Generic.List[string]]::new() + foreach ($line in [IO.File]::ReadAllLines($ResponseFile)) { + $trimmed = $line.Trim() + if ($trimmed -match '(?i)^(-o|--out):') { + $lines.Add("-o:$outputObject") + $foundOutput = $true + continue + } + if ($trimmed -match '(?i)^(--parallelism|-O|--Os|--Ot|--optimize|--optimize-space|--optimize-time|--debug|-g|--exportsfile|--export-dynamic-symbol|--export-unmanaged-entrypoints|--generateunmanagedentrypoints|--dgmllog|--scandgmllog|--guard|--ildump|--map|--mstat|--sourcelink|--metadatalog|--methodbodyfolding|--reachability|--resilient)(:.*)?$') { + continue + } + if ((Get-NormalizedPath $trimmed) -eq $BaselineAssembly) { + $lines.Add($inputAssembly) + $foundInput = $true + continue + } + $lines.Add($line) + } + if (!$foundInput -or !$foundOutput) { + throw "Could not identify the input assembly and output object in '$ResponseFile'." + } + $lines.Add('--parallelism:1') + $lines.Add('--noscan') + $lines.Add('--nopreinitstatics') + $lines.Add('--methodbodyfolding:none') + [IO.File]::WriteAllLines($path, $lines) +} + +New-ResponseFile $baselineResponseFile $BaselineAssembly $baselineObject +New-ResponseFile $cleanResponseFile $updatedAssembly $cleanUpdatedObject + +function Write-LogLine([string]$value) { + [IO.File]::AppendAllText($logPath, $value + [Environment]::NewLine) + Write-Host $value +} + +function Invoke-Ilc( + [string]$label, + [string]$response, + [string]$updatedAssemblies, + [string]$outputObjects) { + $oldEnable = $env:DOTNET_ILC_INCREMENTAL + $oldAssemblies = $env:DOTNET_ILC_INCREMENTAL_UPDATED_ASSEMBLIES + $oldObjects = $env:DOTNET_ILC_INCREMENTAL_OUTPUT_OBJECTS + try { + if ([string]::IsNullOrEmpty($updatedAssemblies)) { + Remove-Item Env:DOTNET_ILC_INCREMENTAL -ErrorAction SilentlyContinue + Remove-Item Env:DOTNET_ILC_INCREMENTAL_UPDATED_ASSEMBLIES -ErrorAction SilentlyContinue + Remove-Item Env:DOTNET_ILC_INCREMENTAL_OUTPUT_OBJECTS -ErrorAction SilentlyContinue + } + else { + $env:DOTNET_ILC_INCREMENTAL = '1' + $env:DOTNET_ILC_INCREMENTAL_UPDATED_ASSEMBLIES = $updatedAssemblies + $env:DOTNET_ILC_INCREMENTAL_OUTPUT_OBJECTS = $outputObjects + } + + $stopwatch = [Diagnostics.Stopwatch]::StartNew() + $startInfo = New-Object Diagnostics.ProcessStartInfo + $startInfo.FileName = $IlcPath + $startInfo.Arguments = '@"' + $response + '"' + $startInfo.WorkingDirectory = (Get-Location).Path + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $process = New-Object Diagnostics.Process + $process.StartInfo = $startInfo + try { + $null = $process.Start() + $standardOutput = $process.StandardOutput.ReadToEndAsync() + $standardError = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + [IO.File]::AppendAllText($logPath, $standardOutput.Result) + [IO.File]::AppendAllText($logPath, $standardError.Result) + $exitCode = $process.ExitCode + } + finally { + $process.Dispose() + } + $stopwatch.Stop() + Write-LogLine "$label milliseconds=$($stopwatch.Elapsed.TotalMilliseconds.ToString('F3', [Globalization.CultureInfo]::InvariantCulture)) exit=$exitCode" + if ($exitCode -ne 0) { + if ($exitCode -eq 85) { + throw "$label requested an explicit clean fallback (exit 85). See '$logPath'." + } + throw "$label failed with exit code $exitCode. See '$logPath'." + } + } + finally { + $env:DOTNET_ILC_INCREMENTAL = $oldEnable + $env:DOTNET_ILC_INCREMENTAL_UPDATED_ASSEMBLIES = $oldAssemblies + $env:DOTNET_ILC_INCREMENTAL_OUTPUT_OBJECTS = $oldObjects + } +} + +function Get-Sha256([string]$path) { + $stream = [IO.File]::OpenRead($path) + try { + $algorithm = [Security.Cryptography.SHA256]::Create() + try { + return [BitConverter]::ToString($algorithm.ComputeHash($stream)).Replace('-', '') + } + finally { + $algorithm.Dispose() + } + } + finally { + $stream.Dispose() + } +} + +$separator = [IO.Path]::PathSeparator +Invoke-Ilc ` + 'incremental-edit-revert' ` + $baselineResponseFile ` + "$updatedAssembly$separator$revertedAssembly" ` + "$incrementalUpdatedObject$separator$incrementalRevertedObject" +Invoke-Ilc 'clean-updated' $cleanResponseFile $null $null + +$baselineHash = Get-Sha256 $baselineObject +$updatedHash = Get-Sha256 $incrementalUpdatedObject +$cleanHash = Get-Sha256 $cleanUpdatedObject +$revertedHash = Get-Sha256 $incrementalRevertedObject + +if ($updatedHash -ne $cleanHash) { + throw "Incremental and clean updated objects differ: $updatedHash != $cleanHash" +} +if ($revertedHash -ne $baselineHash) { + throw "Incremental revert and baseline objects differ: $revertedHash != $baselineHash" +} + +Write-LogLine "baseline_sha256=$baselineHash" +Write-LogLine "incremental_updated_sha256=$updatedHash" +Write-LogLine "clean_updated_sha256=$cleanHash" +Write-LogLine "incremental_reverted_sha256=$revertedHash" +Write-Host "Incremental differential comparison passed. Log: $logPath" diff --git a/src/tests/nativeaot/SmokeTests/IncrementalCompilation/IncrementalCompilation.cs b/src/tests/nativeaot/SmokeTests/IncrementalCompilation/IncrementalCompilation.cs new file mode 100644 index 00000000000000..432d2d697e7cc8 --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/IncrementalCompilation/IncrementalCompilation.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +if (Environment.GetEnvironmentVariable("Never") == "Ever") +{ + Console.WriteLine(IncrementalFixture.GetValue(1)); +} + +return 100; + +static class IncrementalFixture +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static int GetValue(int value) => value + 0x61234567; +} diff --git a/src/tests/nativeaot/SmokeTests/IncrementalCompilation/IncrementalCompilation.csproj b/src/tests/nativeaot/SmokeTests/IncrementalCompilation/IncrementalCompilation.csproj new file mode 100644 index 00000000000000..5fc54adeec3fb2 --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/IncrementalCompilation/IncrementalCompilation.csproj @@ -0,0 +1,18 @@ + + + Exe + 0 + true + true + false + + + + + + + + +