Skip to content

Commit

Permalink
Simplify the recorder async process by storing and writing just a delta
Browse files Browse the repository at this point in the history
  -- use Stack<int> not Option<int list> to remove the update method
  -- rewrite the references to AsyncLocal value including the whole of CallStack,instance()
  -- without the instance/update changes we had dangling metadata-id references that pointed at initializeTrace@### instead
  • Loading branch information
SteveGilham committed Dec 12, 2021
1 parent 657a648 commit e64bb8a
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 70 deletions.
39 changes: 39 additions & 0 deletions AltCover.Async/AltCover.Async.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net46</TargetFrameworks>
<AssemblyName>AltCover.Async</AssemblyName>
<RootNamespace>AltCover.Recorder</RootNamespace>
<ContinuousIntegrationBuild>false</ContinuousIntegrationBuild>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>TRACE</DefineConstants>
<!-- OtherFlags>$(OtherFlags) - -standalone</OtherFlags -->
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>TRACE;DEBUG;CODE_ANALYSIS</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Include="Library.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FSharp.Compiler.Tools" Version="10.2.3" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net46" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Remove="System.ValueTuple" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="DotNet.ReproducibleBuilds" Version="1.1.1" />
<PackageReference Update="FSharp.Core" Version="4.1.18" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions AltCover.Async/Library.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace AltCover.Recorder

open System.Collections.Generic
open System.Threading

module Instance =
module I =
module private CallTrack =

let attr =
System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6")

let value = AsyncLocal<Stack<int>>()

let instance () =
match value.Value with
| null -> value.Value <- Stack<int>()
| _ -> ()

value.Value
21 changes: 6 additions & 15 deletions AltCover.Engine/AltCover.Engine.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
<RootNamespace>AltCover</RootNamespace>
<AssemblyName>AltCover.Engine</AssemblyName>
<GlobalDefineConstants>RUNNER</GlobalDefineConstants>
<!-- If static linking, fix up in the Unpack documentation stage!!-->
<!-- Maybe this to publish -->
<!-- CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies -->
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Debug'">
Expand Down Expand Up @@ -72,8 +69,8 @@
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\_Binaries\AltCover.Recorder\Release+AnyCPU\net20\AltCover.Recorder.dll">
<Link>AltCover.Recorder.net20.dll</Link>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\_Binaries\AltCover.Recorder\Release+AnyCPU\net46\AltCover.Recorder.dll">
<Link>AltCover.Recorder.net46.dll</Link>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\_Binaries\AltCover.Async\Release+AnyCPU\net46\AltCover.Async.dll">
<Link>AltCover.Async.net46.dll</Link>
</EmbeddedResource>
</ItemGroup>

Expand All @@ -85,26 +82,20 @@
</PackageReference>
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="System.ValueTuple" />
<!-- Last static linkable version; the local version of [Nullable] kills later version linkage -->
<!-- PackageReference Include="Manatee.Json" Version="11.0.4" / -->
<!-- Last official version; moved to ThirdParty; see https://github.com/gregsdennis/Manatee.Json and https://graphqello.com/ -->
<!-- PackageReference Include="Manatee.Json" Version="13.0.5" / -->
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.2" Condition="'$(TargetFramework)' == 'net472'">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- Maybe this to publish -->
<!-- PackageReference Include="System.Buffers" version="4.5.1">
<PrivateAssets>All</PrivateAssets>
<Publish>true</Publish>
</PackageReference -->
</ItemGroup>

<ItemGroup>
<Reference Include="Manatee.Json">
<HintPath>..\ThirdParty\Manatee.Json.dll</HintPath>
<!-- Last static linkable version; the local version of [Nullable] kills later version linkage -->
<!-- PackageReference Include="Manatee.Json" Version="11.0.4" / -->
<!-- Last official version; moved to ThirdParty; see https://github.com/gregsdennis/Manatee.Json and https://graphqello.com/ -->
<!-- PackageReference Include="Manatee.Json" Version="13.0.5" / -->
</Reference>
<Reference Include="Mono.Options">
<HintPath>..\ThirdParty\Mono.Options.dll</HintPath>
Expand Down
180 changes: 164 additions & 16 deletions AltCover.Engine/Instrument.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,26 +1171,174 @@ module internal Instrument =
RecordingAssembly = recordingAssembly
RecorderSource = stream }

[<System.Diagnostics.CodeAnalysis.SuppressMessage("Gendarme.Rules.Correctness",
"EnsureLocalDisposalRule",
Justification = "Return confusing Gendarme -- TODO")>]
let private loadClr4AssemblyFromResources (stream: Stream) =
AssemblyDefinition.ReadAssembly(stream)
|> prepareAssemblyDefinition
[<ExcludeFromCodeCoverage; NoComparison; AutoSerializable(false)>]
type CallTracker =
{ CallTrack: TypeDefinition
Value: PropertyDefinition
GetValue: MethodDefinition
Instance: MethodDefinition
Field: FieldDefinition
FieldType: GenericInstanceType
Maker: MethodDefinition }

// make the assembly net46 targeted
[<SuppressMessage("Gendarme.Rules.Exceptions",
"InstantiateArgumentExceptionCorrectlyRule",
Justification = "Library method inlined")>]
[<SuppressMessage("Microsoft.Usage",
"CA2208:InstantiateArgumentExceptionsCorrectly",
Justification = "Library method inlined")>]
let framework46 (make: MethodDefinition) (r: AssemblyDefinition) =
let constructor =
make.Body.Instructions
|> Seq.filter (fun i -> i.OpCode = OpCodes.Newobj)
|> Seq.map (fun i -> i.Operand :?> MethodReference)
|> Seq.head
|> r.MainModule.ImportReference

let blob =
"01 00 1a 2e 4e 45 54 46 72 61 6d 65 77 6f 72 6b 2c 56 65 72 73 69 6f 6e 3d 76 34 2e 36 01 00 54 0e 14 46 72 61 6d 65 77 6f 72 6b 44 69 73 70 6c 61 79 4e 61 6d 65 12 2e 4e 45 54 20 46 72 61 6d 65 77 6f 72 6b 20 34 2e 36"
.Split(' ')
|> Array.map (fun x -> Convert.ToByte(x, 16))

let inject = CustomAttribute(constructor, blob)

r.CustomAttributes.Add inject

let internal uprateRecorder (recorder: AssemblyDefinition) =
let m = recorder.MainModule

// make it net4+
let uprateReferences () =
m.AssemblyReferences
|> Seq.filter (fun a -> a.Version = Version(2, 0, 0, 0))
|> Seq.iter (fun a -> a.Version <- Version(4, 0, 0, 0))

m.RuntimeVersion <- "v4.0.30319"
m.Runtime <- TargetRuntime.Net_4_0

uprateReferences ()

use stream =
Assembly
.GetExecutingAssembly()
.GetManifestResourceStream("AltCover.AltCover.Async.net46.dll")

use delta = AssemblyDefinition.ReadAssembly(stream)

// get a handle on the property
let readCallTrackType (m: ModuleDefinition) =
let calltrack =
m.GetType("AltCover.Recorder.Instance/I/CallTrack")

let value =
calltrack.Properties
|> Seq.find (fun m -> m.Name = "value")

let getValue = value.GetMethod

let field =
((getValue.Body.Instructions |> Seq.head).Operand :?> FieldReference)
.Resolve()

{ CallTrack = calltrack
Value = value
GetValue = getValue
Instance =
calltrack.Methods
|> Seq.find (fun m -> m.Name = "instance")
Field = field
FieldType = field.FieldType :?> GenericInstanceType
Maker =
field
.Resolve()
.DeclaringType.GetStaticConstructor() }

let net20 = readCallTrackType m
let asy46 = readCallTrackType delta.MainModule

let field =
((net20.GetValue.Body.Instructions |> Seq.head)
.Operand
:?> FieldReference)
.Resolve()

let localAsync = field.FieldType.Resolve()
let oldtype = field.FieldType :?> GenericInstanceType

// replace the type references in the recorder
let generics = asy46.FieldType.GenericArguments
generics.Clear()

net20.FieldType.GenericArguments
|> Seq.iter (m.ImportReference >> generics.Add)

let async2 = (asy46.FieldType |> m.ImportReference)

let maker =
asy46.Maker.Body.Instructions
|> Seq.filter (fun i -> i.OpCode = OpCodes.Newobj)
|> Seq.map (fun i -> i.Operand :?> MethodReference)
|> Seq.skip 1
|> Seq.head
|> m.ImportReference

let build =
net20.Maker.Body.Instructions
|> Seq.filter (fun i -> i.OpCode = OpCodes.Newobj)
|> Seq.find
(fun i -> (i.Operand :?> MethodReference).DeclaringType.Name = oldtype.Name)

net20.Field.FieldType <- async2
net20.Value.PropertyType <- async2
net20.GetValue.ReturnType <- async2
build.Operand <- maker

let copyInstance (old: CallTracker) (updated: MethodDefinition) =
let body = old.Instance.Body
let worker = body.GetILProcessor()
let initialBody = body.Instructions |> Seq.toList
let head = initialBody |> Seq.head

bulkInsertBefore
worker
head
(updated.Body.Instructions
|> Seq.map
(fun i ->
match i.OpCode.FlowControl with
| FlowControl.Call ->
i.Operand <-
let mr = (i.Operand :?> MethodReference)

if mr.DeclaringType.FullName = old.CallTrack.FullName then
old.GetValue :> MethodReference
else
mr |> m.ImportReference

i
| _ -> i))
true
|> ignore

initialBody |> Seq.iter worker.Remove

copyInstance net20 asy46.Instance

// delete the placeholder type
m
.GetType("AltCover.Recorder.Instance/I")
.NestedTypes.Remove(localAsync)
|> ignore

framework46 asy46.Maker recorder

let private finishVisit (state: InstrumentContext) =
try
use stream =
Assembly
.GetExecutingAssembly()
.GetManifestResourceStream("AltCover.AltCover.Recorder.net46.dll")

let clr4 =
state.AsyncSupport
|> Option.map (fun _ -> loadClr4AssemblyFromResources stream)
let recorder = state.RecordingAssembly

let recorder =
Option.defaultValue state.RecordingAssembly clr4
state.AsyncSupport
|> Option.iter (fun _ -> uprateRecorder recorder)

// write the module ID values
let keys = modules |> Seq.distinct |> Seq.toList
Expand Down
6 changes: 6 additions & 0 deletions AltCover.Recorder.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Items", "Build Items"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Sample25", "Samples\Sample25\Sample25.fsproj", "{54E57C76-339C-496E-B6EF-832534C7D545}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AltCover.Async", "AltCover.Async\AltCover.Async.fsproj", "{9E8E72B3-FA30-4B09-8577-B9A1BB4F3126}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -59,6 +61,10 @@ Global
{54E57C76-339C-496E-B6EF-832534C7D545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54E57C76-339C-496E-B6EF-832534C7D545}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54E57C76-339C-496E-B6EF-832534C7D545}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E8E72B3-FA30-4B09-8577-B9A1BB4F3126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E8E72B3-FA30-4B09-8577-B9A1BB4F3126}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E8E72B3-FA30-4B09-8577-B9A1BB4F3126}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E8E72B3-FA30-4B09-8577-B9A1BB4F3126}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
51 changes: 15 additions & 36 deletions AltCover.Recorder/Recorder.fs
Original file line number Diff line number Diff line change
Expand Up @@ -172,52 +172,31 @@ module Instance =
/// Gets or sets the current test method
/// </summary>
module private CallTrack =
// .field assembly static initonly class AltCover.Recorder.Instance/I/AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>> value@175
// .custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
// 01 00 00 00 00 00 00 00
//...
// IL_0095: newobj instance void class AltCover.Recorder.Instance/I/AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>>::.ctor()
// IL_009a: stsfld class AltCover.Recorder.Instance/I/AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>> '<StartupCode$AltCover-Recorder>.$Recorder'::value@175
//...
// IL_0000: ldsfld class AltCover.Recorder.Instance/I/AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>> '<StartupCode$AltCover-Recorder>.$Recorder'::value@175
// IL_0005: ret
// vs
// .field assembly static initonly class [mscorlib]System.Threading.AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>> value@175
// .custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
// 01 00 00 00 00 00 00 00
// )
//...
// IL_0095: newobj instance void class [mscorlib]System.Threading.AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>>::.ctor()
// IL_009a: stsfld class [mscorlib]System.Threading.AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>> '<StartupCode$AltCover-Recorder>.$Recorder'::value@175
//...
// IL_0000: ldsfld class [mscorlib]System.Threading.AsyncLocal`1<class Microsoft.FSharp.Core.FSharpOption`1<class Microsoft.FSharp.Collections.FSharpList`1<int32>>> '<StartupCode$AltCover-Recorder>.$Recorder'::value@175
// IL_0005: ret

let value = AsyncLocal<Option<int list>>()

let private update l = value.Value <- Some l

let value = AsyncLocal<Stack<int>>()

// no race conditions here
let instance () =
match value.Value with
| None -> update []
| null -> value.Value <- Stack<int>()
| _ -> ()

value.Value.Value
value.Value

let private look op =
let i = instance ()

match i.Count with
| 0 -> None
| _ -> Some(op i)

let peek () =
match instance () with
| [] -> ([], None)
| h :: xs -> (xs, Some h)
let peek () = look (fun i -> i.Peek())

let push x = update (x :: instance ())
let push x = instance().Push x

let pop () =
let (stack, head) = peek ()
update stack
head
let pop () = look (fun i -> i.Pop())

let internal callerId () = CallTrack.peek () |> snd
let internal callerId () = CallTrack.peek ()
let internal push x = CallTrack.push x
let internal pop () = CallTrack.pop ()

Expand Down
Loading

0 comments on commit e64bb8a

Please sign in to comment.