├── .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