├── .gitattributes ├── .gitignore ├── README.md ├── live-debugging ├── .Dockerignore ├── Dockerfile ├── LiveDebug.fsproj ├── Program.fs ├── README.md ├── build.sh └── run.sh ├── memory-leaks ├── .Dockerignore ├── Dockerfile.2.2 ├── Dockerfile.3.0 ├── MemoryLeak.2.2.fsproj ├── MemoryLeak.3.0.fsproj ├── Program.fs ├── README.md ├── WebRoot │ └── main.css ├── build.sh ├── dump.sh ├── start.sh └── web.config ├── profile-cpu ├── .Dockerignore ├── Copy.fs ├── Dockerfile ├── Profile.fsproj ├── Program.fs ├── README.md ├── build.sh ├── start.sh └── trace.sh ├── slides.pdf └── slides.pptx /.gitattributes: -------------------------------------------------------------------------------- 1 | slides.pdf filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .ionide 3 | bin 4 | obj 5 | dump 6 | trace -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .Net Core Profiling examples 2 | 3 | This repository contains the slides and code for my OpenF# 2019 presentation **An introduction to profiling and performance analysis in .NET Core 3**. It consists of the slides at the top-level (in .ppt and .pdf variants) as individual folders for each of the three worked examples I cover in the talk. 4 | 5 | ## Prerequisites 6 | 7 | Each example is a self-contained project with dockerfiles to build and run the sample, so the only real requirement should be having docker installed on your machine. Check the `README.md` in each folder for additional details. 8 | 9 | ## Topics 10 | 11 | ### Debugging memory leaks 12 | 13 | This sample covers how to use the [dotnet-dump] tool to collect and analyze memory dumps of a running application. It will also cover more in-depth investigations using LLDB directly. 14 | 15 | ### Profiling CPU usage 16 | 17 | This sample covers how to use [dotnet-trace] and [speedscope] to collect and view CPU traces for your application. It also demonstrates the use of the [perf] tool on Linux for on-device profiling when a graphical user interface isn't available. 18 | 19 | ### Diagnosing logic errors 20 | 21 | This sample covers how to use your debugger's thread information together with [dotnet-sos] to investigate incorrect logic in your application. 22 | 23 | 24 | ## Useful links 25 | 26 | * the [dotnet/diagnostics] repository 27 | * the .Net Core 3.0 SDK [download page](https://dotnet.microsoft.com/download/dotnet-core/3.0) 28 | 29 | [dotnet/diagnostics]: https://github.com/dotnet/diagnostics/ 30 | [dotnet-dump]: https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-dump-instructions.md 31 | [dotnet-trace]: https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-trace-instructions.md 32 | [dotnet-counters]: https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-counters-instructions.md 33 | [dotnet-sos]: https://github.com/dotnet/diagnostics/blob/master/documentation/installing-sos-instructions.md 34 | [speedscope]: https://github.com/jlfwong/speedscope 35 | [perf]: https://perf.wiki.kernel.org/index.php/Main_Page 36 | -------------------------------------------------------------------------------- /live-debugging/.Dockerignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | *.sh 4 | *.md -------------------------------------------------------------------------------- /live-debugging/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 as build 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN dotnet publish -c Release -r debian.10-x64 -f netcoreapp3.0 8 | 9 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 as debug_tools 10 | 11 | # lldb version is 7, which is ok 12 | RUN apt update && apt install lldb -y 13 | 14 | ENV TOOL_VERSION=3.0.0-preview9.19454.1 15 | 16 | # install dotnet tools for dump and sos 17 | ENV PATH=$PATH:$HOME/.dotnet/tools 18 | RUN dotnet tool install -g dotnet-symbol 19 | RUN dotnet tool install -g dotnet-sos --version ${TOOL_VERSION} && ~/.dotnet/tools/dotnet-sos install 20 | 21 | FROM debug_tools 22 | 23 | COPY --from=build /app/bin/Release/netcoreapp3.0/debian.10-x64/publish /app 24 | 25 | ENV PATH=/root/.dotnet/tools:$PATH 26 | 27 | WORKDIR /app 28 | 29 | CMD [ "lldb"; "./StackOverflow" ] -------------------------------------------------------------------------------- /live-debugging/LiveDebug.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | portable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /live-debugging/Program.fs: -------------------------------------------------------------------------------- 1 | open FSharpx.Collections 2 | open Newtonsoft.Json 3 | open Newtonsoft.Json.Linq 4 | 5 | 6 | /// A converter that handles json arrays that contain at least one item and makes an 7 | /// FSharpx.Collections.NonEmptyList from the non-empty array 8 | type NonEmptyListConverter() = 9 | inherit JsonConverter() 10 | 11 | let openNonEmptyListType = typedefof> 12 | let openResizeArrayType = typedefof> 13 | let nelAssy = openNonEmptyListType.Assembly 14 | let mutable counter = 0 15 | 16 | let ofSeqFn = nelAssy.GetType("FSharpx.Collections.NonEmptyList").GetMethod("OfSeq") 17 | 18 | override __.CanConvert t = t.IsGenericType && t.GetGenericTypeDefinition() = openNonEmptyListType 19 | 20 | override __.WriteJson(writer, nonEmptyList, serializer) = 21 | let innerType = nonEmptyList.GetType().GetGenericArguments().[0] 22 | let enumerator: System.Collections.IEnumerator = (nonEmptyList :?> System.Collections.IEnumerable).GetEnumerator() 23 | writer.WriteStartArray() 24 | while enumerator.MoveNext() do 25 | serializer.Serialize(writer, enumerator.Current, innerType) 26 | writer.WriteEndArray() 27 | 28 | 29 | // TODO: This method has a bug that causes the stackoverflow 30 | [] 31 | override __.ReadJson(reader, destTy, _existingValue, serializer) = 32 | if reader.TokenType = JsonToken.StartArray 33 | then 34 | let innerTy = destTy.GenericTypeArguments.[0] 35 | let listTy = openResizeArrayType.MakeGenericType([| innerTy |]) 36 | 37 | let list = listTy.GetConstructor([||]).Invoke([||]) 38 | 39 | let add = 40 | let addM = listTy.GetMethod("Add") 41 | fun item -> addM.Invoke(list, [| item |]) |> ignore 42 | 43 | ignore <| reader.Read() // advance past start array 44 | while reader.TokenType <> JsonToken.EndArray do 45 | let v = serializer.Deserialize(reader, innerTy) 46 | //ignore (reader.Read()) //TODO: uncomment this to fix the algorithm 47 | add (box v) 48 | 49 | box (ofSeqFn.MakeGenericMethod([| innerTy |]).Invoke(null, [| list |])) 50 | else 51 | failwithf "Unknown start token %s while deserializing nonempty list" (string reader.TokenType) 52 | 53 | let converter = NonEmptyListConverter() :> JsonConverter 54 | 55 | [] 56 | let main argv = 57 | let nonEmptyList = NonEmptyList.ofList [1;2;3;4;5;6;7] 58 | printfn "serializing %A" nonEmptyList 59 | let serialized = JsonConvert.SerializeObject(nonEmptyList, [| converter |]) 60 | printfn "serialized to %s" serialized 61 | let deserialized = JsonConvert.DeserializeObject>(serialized, [| converter |]) 62 | printfn "deserialized as %A" deserialized 63 | 64 | if nonEmptyList = deserialized 65 | then 0 66 | else 1 -------------------------------------------------------------------------------- /live-debugging/README.md: -------------------------------------------------------------------------------- 1 | # Diagnosing incorrect code 2 | 3 | This sample illustrates how to use [lldb] and [dotnet-sos] to inspect logic errors that happen in your application. The sample is derived from a situation I encountered while writing a custom `JsonConverter` for the `NonEmptyList` type from [FSharpx.Collections]. The scenario is that I was having trouble parsing a list of items due to a logic error I had introduced. In this sample we'll develop a similar `JsonConverter` and experience the same issue I saw, then debug it using [lldb]. 4 | 5 | ## Repository layout 6 | 7 | This sample consists of a `.netcoreapp3.0` sample application that has the [Newtonsoft.Json] and [FSharpx.Collections] libraries installed. While the program runs attempts to serialize and deserialize an instance of a `NonEmptyList`. 8 | 9 | It also contains the Dockerfile for building and running the application. The intent is that you will be able to run the application in a container and run it under [lldb] to analyze the failure. 10 | 11 | ## Running the application 12 | 13 | First you'll need to build the application: `./build.sh`. This will result in a named container: `live-debug`. 14 | 15 | Then, you'll run the application in its container: `./run.sh`. This script will start the container, which runs the application under [lldb]'s active debugger. 16 | 17 | Once the container launches, type `lldb LiveDebug` to load the program into [lldb]. Once [lldb] is loaded, type `run` to run the application. It should seemingly hang almost immediately, after which you can `CTRL+C` to halt execution and inspect the state of the program. 18 | 19 | ## Analyzing the application 20 | 21 | From here you can inspect the stacktraces of the various threads to see if you can intuit what the erroneous callstack is. You should use the following commands: 22 | 23 | * `thread backtrace [all]` 24 | * dump the stack trace for the current thread (or all threads if `all` is provided) 25 | * `thread select [thread_number]` 26 | * set the thread that's selected for various other commands 27 | * `clrstack` 28 | * dump the managed stack trace for the selected CLR thread 29 | 30 | ## End result 31 | 32 | After a bit of time, you should be able to find a stack containing our calls. This is our failing stack. The loop will in json reading, so we should inspect the `ReadJson` method of our converter where you will indeed find that the particular element of the array we're consuming isn't being 'advanced', so we always stay at the current position. 33 | 34 | [lldb]: https://lldb.llvm.org/ 35 | [dotnet-sos]: https://github.com/dotnet/diagnostics/blob/master/documentation/installing-sos-instructions.md 36 | [Newtonsoft.Json]: https://github.com/JamesNK/Newtonsoft.Json 37 | [FSharpx.Collections]: https://github.com/fsprojects/FSharpx.Collections -------------------------------------------------------------------------------- /live-debugging/build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | docker build -t live-debug -f Dockerfile . -------------------------------------------------------------------------------- /live-debugging/run.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | docker run --cap-add SYS_ADMIN --cap-add SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined -it --rm --name live-debug live-debug /bin/bash -------------------------------------------------------------------------------- /memory-leaks/.Dockerignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | *.sh 4 | dump -------------------------------------------------------------------------------- /memory-leaks/Dockerfile.2.2: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2 as build 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN dotnet publish -c Release -r debian.9-x64 -f netcoreapp2.2 MemoryLeak.2.2.fsproj 8 | 9 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2 as debug_tools 10 | 11 | # lldb version is 7, which is ok 12 | RUN apt update && apt install lldb -y 13 | 14 | ENV TOOL_VERSION=3.0.0-preview9.19454.1 15 | 16 | # install dotnet tools for dump and sos 17 | ENV PATH=$PATH:$HOME/.dotnet/tools 18 | RUN dotnet tool install -g dotnet-symbol 19 | RUN dotnet tool install -g dotnet-sos --version ${TOOL_VERSION} && ~/.dotnet/tools/dotnet-sos install 20 | RUN dotnet tool install -g dotnet-dump --version ${TOOL_VERSION} 21 | RUN dotnet tool install -g dotnet-trace --version ${TOOL_VERSION} 22 | 23 | FROM debug_tools 24 | 25 | COPY --from=build /app/bin/Release/netcoreapp2.2/debian.9-x64/publish /app 26 | 27 | ENV PATH=/root/.dotnet/tools:$PATH 28 | 29 | WORKDIR /app 30 | 31 | CMD [ "./MemoryLeak.2.2" ] -------------------------------------------------------------------------------- /memory-leaks/Dockerfile.3.0: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 as build 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN dotnet publish -c Release -r debian.10-x64 -f netcoreapp3.0 MemoryLeak.3.0.fsproj 8 | 9 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 as debug_tools 10 | 11 | # lldb version is 7, which is ok 12 | RUN apt update && apt install lldb -y 13 | 14 | ENV TOOL_VERSION=3.0.0-preview9.19454.1 15 | 16 | # install dotnet tools for dump and sos 17 | ENV PATH=$PATH:$HOME/.dotnet/tools 18 | RUN dotnet tool install -g dotnet-symbol 19 | RUN dotnet tool install -g dotnet-sos --version ${TOOL_VERSION} && ~/.dotnet/tools/dotnet-sos install 20 | RUN dotnet tool install -g dotnet-dump --version ${TOOL_VERSION} 21 | 22 | FROM debug_tools 23 | 24 | COPY --from=build /app/bin/Release/netcoreapp3.0/debian.10-x64/publish /app 25 | 26 | ENV PATH=/root/.dotnet/tools:$PATH 27 | 28 | WORKDIR /app 29 | 30 | CMD [ "./MemoryLeak.3.0" ] -------------------------------------------------------------------------------- /memory-leaks/MemoryLeak.2.2.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | portable 6 | Exe 7 | false 8 | OutOfProcess 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /memory-leaks/MemoryLeak.3.0.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | portable 6 | Exe 7 | false 8 | OutOfProcess 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /memory-leaks/Program.fs: -------------------------------------------------------------------------------- 1 | module MemoryLeak.App 2 | 3 | open System 4 | open System.IO 5 | open Microsoft.AspNetCore.Builder 6 | open Microsoft.AspNetCore.Hosting 7 | open Microsoft.Extensions.Logging 8 | open Microsoft.Extensions.DependencyInjection 9 | open Giraffe 10 | open Prometheus 11 | 12 | let webApp = GET >=> route "/" >=> setStatusCode 200 >=> text "Hi" 13 | 14 | let configureApp (app : IApplicationBuilder) = 15 | app.UseHttpMetrics() 16 | .UseMetricServer() 17 | .UseGiraffe(webApp) 18 | 19 | let configureServices (services : IServiceCollection) = 20 | services.AddGiraffe() |> ignore 21 | 22 | [] 23 | let main _ = 24 | let contentRoot = Directory.GetCurrentDirectory() 25 | let webRoot = Path.Combine(contentRoot, "WebRoot") 26 | 27 | let stats = DotNetRuntime.DotNetRuntimeStatsBuilder.Default().StartCollecting() 28 | 29 | 30 | WebHostBuilder() 31 | .UseKestrel() 32 | .UseContentRoot(contentRoot) 33 | .UseWebRoot(webRoot) 34 | .Configure(Action configureApp) 35 | .ConfigureServices(configureServices) 36 | .Build() 37 | .RunAsync() 38 | |> Async.AwaitTask 39 | |> Async.RunSynchronously 40 | printfn "done" 41 | 0 -------------------------------------------------------------------------------- /memory-leaks/README.md: -------------------------------------------------------------------------------- 1 | # Diagnosing memory leaks 2 | 3 | This sample illustrates how to diagnose a memory leak in an application. The sample is derived from a situation I encountered at my job. The scenario is that, while executing on the .Net Core 2.2 Runtime, the implementation of the `System.Diagnostics.EventListener` type appears to leak quite a bit of memory, up to ~1GB per process, seemingly unnecessarily. In this sample we'll run the application, collect, and investigate dumps to find the root cause. 4 | 5 | ## Repository layout 6 | 7 | This sample consists of a `.netcoreapp2.2` [Giraffe] application that has the [prometheus-net.DotNetRuntime] library installed to export metrics around the .Net Core runtime Garbage Collector, ThreadPool, and JIT. 8 | 9 | It also contains two Dockerfiles for building and running the application, one for the 2.2 runtime and one for the 3.0 runtime. The intent is that you can capture dumps from both scenarios and investigate them. 10 | 11 | ## Capturing the dumps 12 | 13 | First you'll need to build the application for the runtime you care to test: `./build.sh 2.2` or `./build.sh 3.0` 14 | This will result in a named container: `memory-leak:2.2` or `memory-leak:3.0`. 15 | 16 | Then, you'll run the application in its container: `./start.sh 2.2` or `./start.sh 3.0`. These scripts will start the container with a particular name so that the following scripts can connect. 17 | 18 | Finally, you'll capture a dump using `./dump.sh 2.2` or `./dump.sh 3.0`. These helper scripts will connect to the container, take a memory dump, and copy it to the `dump` folder in this sample with the version and timestamp appended. You should take repeated memory dumps a bit apart, since the only way to confirm a leak is to see the upward trend over time in memory allocated. 19 | 20 | ## Analyzing the dump 21 | 22 | From here you can load the dump into [lldb] with SoS loaded (via `[dotnet-sos] install`), or directly into [dotnet-dump]. Once loaded, you are free to investigate. I'd suggest using commands like 23 | 24 | * `dumpheap`: get lists of the counts and sizes of instances of CLR types 25 | * expect to see `System.String` and `system.Char[]` get pretty large! 26 | * narrow down to specific types (`-type ` argument) 27 | * `-min` or `-max` to only return types that have instances at least that big in memory 28 | * `dumpobj `: get details of an object instance 29 | * `gcroot `: get 30 | 31 | ## End result 32 | 33 | You should be able to see that there are some _very_ large strings, and if you're in [lldb] you should be able to use `memory read +0xc` to read the first few bytes of the string. You can add the `--count ` parameter to read even more bytes, and you should see that it's XML. Specifically the XML from the .Net Runtime event source. Read over and over again. 34 | 35 | ## Next steps 36 | 37 | From here you've identified that the event listener is to blame, so you log/find [an issue](https://github.com/djluck/prometheus-net.DotNetRuntime/issues/6#). But until that's solved, you have tp make some choices 38 | 39 | * if metrics are important, derive them some other way 40 | * disable this metrics collection 41 | * ??? 42 | 43 | [prometheus-net.DotNetRuntime]: (https://github.com/djluck/prometheus-net.DotNetRuntime) 44 | [Giraffe]: (https://github.com/giraffe-fsharp/Giraffe) 45 | [lldb]: https://lldb.llvm.org/ 46 | [dotnet-sos]: https://github.com/dotnet/diagnostics/blob/master/documentation/installing-sos-instructions.md 47 | [dotnet-dump]: https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-dump-instructions.md 48 | -------------------------------------------------------------------------------- /memory-leaks/WebRoot/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | color: #333; 4 | font-size: .9em; 5 | } 6 | 7 | h1 { 8 | font-size: 1.5em; 9 | color: #334499; 10 | } -------------------------------------------------------------------------------- /memory-leaks/build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | if [ -z "$1" ] 4 | then 5 | echo "usage: build.sh [2.2|3.0]" 6 | exit 1 7 | fi 8 | 9 | TFM=$1 10 | 11 | # build the app in the docker container 12 | docker build -t memory-leak:"$TFM" -f Dockerfile."$TFM" . 13 | -------------------------------------------------------------------------------- /memory-leaks/dump.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | if [ -z "$1" ] 4 | then 5 | echo "usage: dump.sh [2.2|3.0]" 6 | exit 1 7 | fi 8 | 9 | TFM=$1 10 | 11 | OLD_CMD='/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.4/createdump --full --name /dump/dump_$TFM_$(date --iso-8601=''seconds'') 1' 12 | NEW_CMD='dotnet dump collect -p 1 -o /dump/dump_$TFM_$(date --iso-8601=''seconds'')' 13 | 14 | if [ "2.2" = "$1" ] 15 | then 16 | CMD=$OLD_CMD 17 | else 18 | CMD=$NEW_CMD 19 | fi 20 | 21 | # take a dump from the container and put it in the '/dump' folder with a timestamped name 22 | docker exec memoryleak_"$TFM" sh -c "$CMD" -------------------------------------------------------------------------------- /memory-leaks/start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | if [ -z "$1" ] 4 | then 5 | echo "usage: start.sh [2.2|3.0]" 6 | exit 1 7 | fi 8 | 9 | TFM=$1 10 | 11 | # start the app in the background with a particular name 12 | docker run --cap-add SYS_ADMIN --cap-add SYS_PTRACE -v "$(pwd)/dump":/dump --rm -p=5000:80 --name memoryleak_"$TFM" memory-leak:"$TFM" & -------------------------------------------------------------------------------- /memory-leaks/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /profile-cpu/.Dockerignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | trace 4 | *.sh -------------------------------------------------------------------------------- /profile-cpu/Copy.fs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.Serialization 2 | 3 | module TypeCache = 4 | open FSharp.Reflection 5 | 6 | // Have to use concurrentdictionary here because dictionaries thrown on non-locked access: 7 | (* Error Message: 8 | System.InvalidOperationException : Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct. 9 | Stack Trace: 10 | at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) *) 11 | type Dict<'a, 'b> = System.Collections.Concurrent.ConcurrentDictionary<'a, 'b> 12 | 13 | /// cached access to FSharpType.IsUnion to prevent repeated access to reflection members 14 | let isUnion = 15 | let cache = Dict() 16 | 17 | fun (ty: System.Type) -> 18 | cache.GetOrAdd(ty, (fun ty -> FSharpType.IsUnion(ty, true))) 19 | 20 | /// cached access to FSharpType.IsRecord to prevent repeated access to reflection members 21 | let isRecord = 22 | let cache = Dict() 23 | 24 | fun (ty: System.Type) -> 25 | cache.GetOrAdd(ty, (fun ty -> FSharpType.IsRecord(ty, true))) 26 | 27 | 28 | namespace System.Text.Json.Serialization 29 | 30 | open System 31 | open System.Text.Json 32 | open FSharp.Reflection 33 | 34 | type internal RecordProperty = 35 | { 36 | Name: string 37 | Type: Type 38 | Ignore: bool 39 | } 40 | 41 | type JsonRecordConverter<'T>() = 42 | inherit JsonConverter<'T>() 43 | 44 | static let fieldProps = 45 | FSharpType.GetRecordFields(typeof<'T>, true) 46 | |> Array.map (fun p -> 47 | let name = 48 | match p.GetCustomAttributes(typeof, true) with 49 | | [| :? JsonPropertyNameAttribute as name |] -> name.Name 50 | | _ -> p.Name 51 | let ignore = 52 | p.GetCustomAttributes(typeof, true) 53 | |> Array.isEmpty 54 | |> not 55 | { Name = name; Type = p.PropertyType; Ignore = ignore } 56 | ) 57 | 58 | static let expectedFieldCount = 59 | fieldProps 60 | |> Seq.filter (fun p -> not p.Ignore) 61 | |> Seq.length 62 | 63 | static let ctor = FSharpValue.PreComputeRecordConstructor(typeof<'T>, true) 64 | 65 | static let dector = FSharpValue.PreComputeRecordReader(typeof<'T>, true) 66 | 67 | static let fieldIndex (reader: byref) = 68 | let mutable found = ValueNone 69 | let mutable i = 0 70 | while found.IsNone && i < fieldProps.Length do 71 | let p = fieldProps.[i] 72 | if reader.ValueTextEquals(p.Name.AsSpan()) then 73 | found <- ValueSome (struct (i, p)) 74 | else 75 | i <- i + 1 76 | found 77 | 78 | override __.Read(reader, typeToConvert, options) = 79 | if reader.TokenType <> JsonTokenType.StartObject then 80 | raise (JsonException("Failed to parse record type " + typeToConvert.FullName + ", expected JSON object, found " + string reader.TokenType)) 81 | 82 | let fields = Array.zeroCreate fieldProps.Length 83 | let mutable cont = true 84 | let mutable fieldsFound = 0 85 | while cont && reader.Read() do 86 | match reader.TokenType with 87 | | JsonTokenType.EndObject -> 88 | cont <- false 89 | | JsonTokenType.PropertyName -> 90 | match fieldIndex &reader with 91 | | ValueSome (i, p) when not p.Ignore -> 92 | fieldsFound <- fieldsFound + 1 93 | fields.[i] <- JsonSerializer.Deserialize(&reader, p.Type, options) 94 | | _ -> 95 | reader.Skip() 96 | | _ -> () 97 | 98 | if fieldsFound < expectedFieldCount then 99 | raise (JsonException("Missing field for record type " + typeToConvert.FullName)) 100 | ctor fields :?> 'T 101 | 102 | override __.Write(writer, value, options) = 103 | writer.WriteStartObject() 104 | (fieldProps, dector value) 105 | ||> Array.iter2 (fun p v -> 106 | if not p.Ignore then 107 | writer.WritePropertyName(p.Name) 108 | JsonSerializer.Serialize(writer, v, options)) 109 | writer.WriteEndObject() 110 | 111 | type JsonRecordConverter() = 112 | inherit JsonConverterFactory() 113 | 114 | static member internal CanConvert(typeToConvert) = 115 | TypeCache.isRecord typeToConvert 116 | 117 | static member internal CreateConverter(typeToConvert) = 118 | typedefof> 119 | .MakeGenericType([|typeToConvert|]) 120 | .GetConstructor([||]) 121 | .Invoke([||]) 122 | :?> JsonConverter 123 | 124 | override __.CanConvert(typeToConvert) = 125 | JsonRecordConverter.CanConvert(typeToConvert) 126 | 127 | override __.CreateConverter(typeToConvert, _options) = 128 | JsonRecordConverter.CreateConverter(typeToConvert) 129 | -------------------------------------------------------------------------------- /profile-cpu/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 as build 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN dotnet publish -c Release -r debian.10-x64 -f netcoreapp3.0 8 | 9 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0 as debug_tools 10 | 11 | ENV TOOL_VERSION=3.0.0-preview9.19454.1 12 | 13 | # install the `perf` tool 14 | RUN apt update && apt install -y linux-perf 15 | 16 | 17 | # install dotnet tools for dump and sos 18 | ENV PATH=$PATH:$HOME/.dotnet/tools 19 | RUN dotnet tool install -g dotnet-symbol 20 | RUN dotnet tool install -g dotnet-trace --version ${TOOL_VERSION} 21 | 22 | FROM debug_tools 23 | 24 | COPY --from=build /app/bin/Release/netcoreapp3.0/debian.10-x64/publish /app 25 | 26 | ENV PATH=/root/.dotnet/tools:$PATH 27 | ENV COMPlus_PerfMapEnabled=1 28 | 29 | WORKDIR /app 30 | 31 | # preload framework PDBs 32 | RUN dotnet symbol ./Profile 33 | 34 | CMD [ "./Profile" ] -------------------------------------------------------------------------------- /profile-cpu/Profile.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | portable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /profile-cpu/Program.fs: -------------------------------------------------------------------------------- 1 |  2 | open System 3 | open System.Text.Json 4 | open System.Text.Json.Serialization 5 | 6 | type MyRecord = 7 | { Name: string 8 | Active: bool 9 | CreatedDate: DateTimeOffset } 10 | 11 | let sample = 12 | { Name = "Sample" 13 | Active = false 14 | CreatedDate = DateTimeOffset.UtcNow } 15 | 16 | let options: JsonSerializerOptions = 17 | let o = JsonSerializerOptions() 18 | o.Converters.Add(JsonRecordConverter()) 19 | o 20 | 21 | [] 22 | let main _argv = 23 | let mutable counter = 0 24 | while true do 25 | let serialized = JsonSerializer.Serialize(sample, options) 26 | //let deserialized = JsonSerializer.Deserialize(serialized, options) 27 | 28 | if counter % 100_000 = 0 29 | then () 30 | // printfn "finished iteration %d" counter 31 | // printfn "%A" deserialized 32 | counter <- counter + 1 33 | 0 34 | -------------------------------------------------------------------------------- /profile-cpu/README.md: -------------------------------------------------------------------------------- 1 | # Profiling CPU Usage 2 | 3 | This sample illustrates how to visualize CPU profiling information to make changes to your application. The sample is derived from a situation I encountered while investigating the [FSharp.SystemTextJson] serialization library for JSON. The scenario is that, while adding Benchmark.Net tests showing comparisons between Newtonsoft.Json and Fsharp.SystemTextJson for record and union (de)serialization, FSharp.SystemTextJson showed much worse results in both speed and memory. In this sample we'll run a similar profile setup and learn how to generate flamegraphs to identify hot spots in your program. 4 | 5 | ## Repository layout 6 | 7 | This sample consists of a `.netcoreapp3.0` sample application that has the [FSharp.SystemTextJson] library installed. While the program runs it continuously attempts to (de)serialize a JSON string for a record type into an instance of that record. 8 | 9 | It also contains the Dockerfile for building and running the application. The intent is that you will be able to run the application in a container, grab a profile from the app and analyze it. 10 | 11 | ## Capturing the trace 12 | 13 | First you'll need to build the application: `./build.sh`. This will result in a named container: `profile-cpu`. 14 | 15 | Then, you'll run the application in its container: `./start.sh`. This script will start the container with a particular name so that the trace script can connect to it consistently. 16 | 17 | Finally, you'll capture a profile trace using `./trace.sh`. This helper script will connect to the container, take a cpu profile with [dotnet-trace] for a few seconds, and copy it to the `trace` folder in this folder with a timestamp appended. 18 | 19 | ## Analyzing the dump 20 | 21 | From here you can load the dump into [speedscope] for visualization. Once loaded, you are free to investigate. What you should be able to see is that the majority of the time spent in the application is in the `FSharp.Reflection.FSharpType` helper functions for creating instances of records and reading values from instances of records. 22 | 23 | ## End result 24 | 25 | For further reading you could look at the source code of the functions involved and see that they are very standard runtime reflection calls (PropertyInfo, MethodInfo, etc), which of course is quite slow. There's not really an easy way to sidestep usage of these functions aside from using IL generation at runtime to compute optimized readers and writers, which of course the wonderful Tarmil is [doing already](https://github.com/Tarmil/FSharp.SystemTextJson/pull/15) 26 | 27 | ## Extra credit 28 | 29 | The provided set up requires you to take a trace and relocate that trace to a system with a browser so that you can run the NodeJs-based tool [speedscope] for visualization. This can be awkward if you want to do analysis on a remote server, so for that scenario it's also possible to use the `perf record` and `perf report` tools. This will give you a terminal-based UI for tree analysis that's also quite powerful. For more details check out the ones on the dotnet/diagnostics repository [here](https://github.com/dotnet/diagnostics/blob/master/documentation/tutorial/app_running_slow_highcpu.md) 30 | 31 | [FSharp.SystemTextJson]: https://github.com/Tarmil/FSharp.SystemTextJson 32 | [speedscope]: https://github.com/jlfwong/speedscope 33 | [dotnet-trace]: https://github.com/dotnet/diagnostics/blob/master/documentation/dotnet-trace-instructions.md 34 | -------------------------------------------------------------------------------- /profile-cpu/build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | docker build -t profile-cpu -f Dockerfile . -------------------------------------------------------------------------------- /profile-cpu/start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | # note that we add the SYS_ADMIN capability here so that we can use `perf` if necessary 4 | docker run --cap-add SYS_ADMIN --privileged -v "$(pwd)/trace":/trace --rm --name profile-cpu profile-cpu & -------------------------------------------------------------------------------- /profile-cpu/trace.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | TRACE_CMD='dotnet trace collect -p 1 --format speedscope -o /trace/trace_$(date --iso-8601=''seconds'')' 4 | 5 | docker exec -it profile-cpu sh -c "$TRACE_CMD" -------------------------------------------------------------------------------- /slides.pdf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1d33afb473950bdfff7448a3d1e61734cc2bc199fb8faddea75b9a6b8faa7eb1 3 | size 448692908 4 | -------------------------------------------------------------------------------- /slides.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baronfel/openfsharp-dotnetcore-profiling/59f1deaa771c0dd4540a7d74c2a0c17a998b550b/slides.pptx --------------------------------------------------------------------------------