diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1cb3d1c
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,144 @@
+# EditorConfig is awesome:http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# All Files
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = false
+trim_trailing_whitespace = true
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = true:silent
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_indent_labels = one_less_than_current
+csharp_style_deconstructed_variable_declaration = true:suggestion
+dotnet_diagnostic.NUnit2006.severity = silent
+dotnet_diagnostic.NUnit2005.severity = silent
+dotnet_diagnostic.NUnit2004.severity = silent
+dotnet_diagnostic.NUnit2003.severity = silent
+dotnet_diagnostic.NUnit2002.severity = silent
+dotnet_diagnostic.NUnit2001.severity = silent
+
+# Solution Files
+[*.sln]
+indent_style = tab
+
+# XML Project Files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# Configuration Files
+[*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset}]
+indent_size = 2
+
+# Txt/Markdown Files
+[*.{md,txt}]
+trim_trailing_whitespace = false
+
+# Web Files
+[*.{htm,html,js,ts,css,scss,less}]
+indent_size = 2
+insert_final_newline = true
+
+# Bash Files
+[*.sh]
+end_of_line = lf
+
+[*.{cs,vb}]
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+
+# Verify settings
+[*.{received,verified}.{txt,xml,json}]
+charset = "utf-8-bom"
+end_of_line = lf
+indent_size = unset
+indent_style = unset
+insert_final_newline = false
+tab_width = unset
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..859725c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+*.sh text eol=lf
+*.verified.txt text eol=lf working-tree-encoding=UTF-8
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..e707902
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [xoofx]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..02791fb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,18 @@
+name: ci
+
+on:
+ push:
+ paths-ignore:
+ - 'doc/**'
+ - 'img/**'
+ - 'readme.md'
+ pull_request:
+
+jobs:
+ build:
+ runs-on: 'windows-latest'
+ steps:
+ - name: "Build, Test, Pack and Publish"
+ uses: xoofx/.github/.github/actions/dotnet-releaser-action@main
+ with:
+ NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8f100b4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,375 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Rider
+.idea/
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage.*[.json, .xml, .info]
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Rust
+/lib/blake3_dotnet/target
+Cargo.lock
+
+# Tmp folders
+tmp/
+[Tt]emp/
+
+# Remove artifacts produced by dotnet-releaser
+artifacts-dotnet-releaser/
+
+# Verify
+*.received.*
\ No newline at end of file
diff --git a/doc/readme.md b/doc/readme.md
new file mode 100644
index 0000000..733a4f4
--- /dev/null
+++ b/doc/readme.md
@@ -0,0 +1,3 @@
+# Ultra User Guide
+
+This is a default project description.
diff --git a/img/ultra.png b/img/ultra.png
new file mode 100644
index 0000000..707cb10
Binary files /dev/null and b/img/ultra.png differ
diff --git a/img/ultra.svg b/img/ultra.svg
new file mode 100644
index 0000000..26960a5
--- /dev/null
+++ b/img/ultra.svg
@@ -0,0 +1,105 @@
+
+
diff --git a/license.txt b/license.txt
new file mode 100644
index 0000000..96b3bad
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,23 @@
+Copyright (c) 2024, Alexandre Mutel
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification
+, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..3f8d42c
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,21 @@
+# Ultra [![ci](https://github.com/xoofx/Ultra/actions/workflows/ci.yml/badge.svg)](https://github.com/xoofx/Ultra/actions/workflows/ci.yml) [![NuGet](https://img.shields.io/nuget/v/Ultra.svg)](https://www.nuget.org/packages/Ultra/)
+
+
+
+This is a default project description.
+
+## ✨ Features
+
+- TODO
+
+## 📖 User Guide
+
+For more details on how to use Ultra, please visit the [user guide](https://github.com/xoofx/Ultra/blob/main/doc/readme.md).
+
+## 🪪 License
+
+This software is released under the [BSD-2-Clause license](https://opensource.org/licenses/BSD-2-Clause).
+
+## 🤗 Author
+
+Alexandre Mutel aka [xoofx](https://xoofx.github.io).
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..1d2d9e0
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+ enable
+ enable
+
+
\ No newline at end of file
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
new file mode 100644
index 0000000..43f6e05
--- /dev/null
+++ b/src/Directory.Packages.props
@@ -0,0 +1,14 @@
+
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ultra.Core/EtwConverterToFirefox.cs b/src/Ultra.Core/EtwConverterToFirefox.cs
new file mode 100644
index 0000000..3971f7b
--- /dev/null
+++ b/src/Ultra.Core/EtwConverterToFirefox.cs
@@ -0,0 +1,833 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using ByteSizeLib;
+using Microsoft.Diagnostics.Symbols;
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Etlx;
+using Microsoft.Diagnostics.Tracing.Parsers;
+using Microsoft.Diagnostics.Tracing.Parsers.Clr;
+using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
+using Ultra.Core.Markers;
+
+namespace Ultra.Core;
+
+public class EtwConverterToFirefox : IDisposable
+{
+ private readonly Dictionary _mapModuleFileIndexToFirefox;
+ private readonly HashSet _setManagedModules;
+ private readonly Dictionary _mapCallStackIndexToFirefox;
+ private readonly Dictionary _mapCodeAddressIndexToFirefox;
+ private readonly Dictionary _mapCodeAddressIndexToMethodIndexFirefox;
+ private readonly Dictionary _mapMethodIndexToFirefox;
+ private readonly Dictionary _mapStringToFirefox;
+ private SymbolReader? _symbolReader;
+ private ETWTraceEventSource _etl;
+ private ModuleFileIndex _clrJitModuleIndex = ModuleFileIndex.Invalid;
+ private ModuleFileIndex _coreClrModuleIndex = ModuleFileIndex.Invalid;
+
+ private const int CategoryOther = 0;
+ private const int CategoryKernel = 1;
+ private const int CategoryNative = 2;
+ private const int CategoryManaged = 3;
+ private const int CategoryGC = 4;
+ private const int CategoryJit = 5;
+ private const int CategoryClr = 6;
+
+ public EtwConverterToFirefox()
+ {
+ _mapModuleFileIndexToFirefox = new();
+ _mapCallStackIndexToFirefox = new();
+ _mapCodeAddressIndexToFirefox = new();
+ _mapCodeAddressIndexToMethodIndexFirefox = new Dictionary();
+ _mapMethodIndexToFirefox = new();
+ _mapStringToFirefox = new Dictionary(StringComparer.Ordinal);
+ _setManagedModules = new HashSet();
+ }
+
+ public FirefoxProfiler.Profile Convert(string traceFilePath, List processIds, EtwUltraProfilerOptions options)
+ {
+ const double MinimumCpuTimeBeforeThreadIsVisible = 10.0;
+
+ _etl = new ETWTraceEventSource(traceFilePath);
+
+ using var log = TraceLog.OpenOrConvert(traceFilePath);
+
+ // Console.Out
+ var symbolPath = options.GetCachedSymbolPath();
+ var symbolPathText = symbolPath.ToString();
+
+ _symbolReader = new SymbolReader(TextWriter.Null, symbolPathText);
+ _symbolReader.Options = SymbolReaderOptions.None;
+ _symbolReader.SecurityCheck = (pdbPath) => true;
+
+ var profile = new FirefoxProfiler.Profile
+ {
+ Meta =
+ {
+ StartTime = double.MaxValue,
+ EndTime = 0.0f,
+ ProfilingStartTime = double.MaxValue,
+ ProfilingEndTime = 0.0f,
+ Version = 29,
+ PreprocessedProfileVersion = 51,
+ Product = string.Empty,
+ InitialSelectedThreads = [],
+ Platform = $"{log.OSName} {log.OSVersion} {log.OSBuild}",
+ Oscpu = $"{log.OSName} {log.OSVersion} {log.OSBuild}",
+ LogicalCPUs = log.NumberOfProcessors,
+ DoesNotUseFrameImplementation = true,
+ Symbolicated = true,
+ SampleUnits = new FirefoxProfiler.SampleUnits
+ {
+ Time = "ms",
+ EventDelay = "ms",
+ ThreadCPUDelta = "ns"
+ },
+ InitialVisibleThreads = [],
+ Stackwalk = 1,
+ Interval = log.SampleProfileInterval.TotalMilliseconds,
+ Categories =
+ [
+ new FirefoxProfiler.Category()
+ {
+ Name = "Other",
+ Color = FirefoxProfiler.ProfileColor.Grey,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = "Kernel",
+ Color = FirefoxProfiler.ProfileColor.Orange,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = "Native",
+ Color = FirefoxProfiler.ProfileColor.Blue,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET",
+ Color = FirefoxProfiler.ProfileColor.Green,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET GC",
+ Color = FirefoxProfiler.ProfileColor.Yellow,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET JIT",
+ Color = FirefoxProfiler.ProfileColor.Purple,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ new FirefoxProfiler.Category()
+ {
+ Name = ".NET CLR",
+ Color = FirefoxProfiler.ProfileColor.Grey,
+ Subcategories =
+ {
+ "Other",
+ }
+ },
+ ]
+ }
+ };
+
+ profile.Meta.Abi = RuntimeInformation.RuntimeIdentifier;
+ profile.Meta.MarkerSchema.Add(JitCompileEvent.Schema());
+ profile.Meta.MarkerSchema.Add(GCEvent.Schema());
+ profile.Meta.MarkerSchema.Add(GCHeapStatsEvent.Schema());
+ profile.Meta.MarkerSchema.Add(GCAllocationTickEvent.Schema());
+ profile.Meta.MarkerSchema.Add(GCSuspendExecutionEngineEvent.Schema());
+ profile.Meta.MarkerSchema.Add(GCRestartExecutionEngineEvent.Schema());
+
+ // MSNT_SystemTrace/Image/KernelBase - ThreadID="-1" ProcessorNumber="9" ImageBase="0xfffff80074000000"
+
+ // We don't have access to physical CPUs
+ //profile.Meta.PhysicalCPUs = Environment.ProcessorCount / 2;
+ //profile.Meta.CPUName = ""; // TBD
+
+ int profileThreadIndex = 0;
+
+ foreach (var processId in processIds)
+ {
+ // Reset all maps and default values before processing a new process
+ _mapModuleFileIndexToFirefox.Clear();
+ _setManagedModules.Clear();
+ _clrJitModuleIndex = ModuleFileIndex.Invalid;
+ _coreClrModuleIndex = ModuleFileIndex.Invalid;
+
+ var process = log.Processes.LastProcessWithID(processId);
+
+ if (profile.Meta.Product == string.Empty)
+ {
+ profile.Meta.Product = process.Name;
+ }
+
+ var processStartTime = new DateTimeOffset(process.StartTime.ToUniversalTime()).ToUnixTimeMilliseconds();
+ var processEndTime = new DateTimeOffset(process.EndTime.ToUniversalTime()).ToUnixTimeMilliseconds();
+ if (processStartTime < profile.Meta.StartTime)
+ {
+ profile.Meta.StartTime = processStartTime;
+ }
+ if (processEndTime > profile.Meta.EndTime)
+ {
+ profile.Meta.EndTime = processEndTime;
+ }
+
+ var profilingStartTime = process.StartTimeRelativeMsec;
+ if (profilingStartTime < profile.Meta.ProfilingStartTime)
+ {
+ profile.Meta.ProfilingStartTime = profilingStartTime;
+ }
+ var profilingEndTime = process.EndTimeRelativeMsec;
+ if (profilingEndTime > profile.Meta.ProfilingEndTime)
+ {
+ profile.Meta.ProfilingEndTime = profilingEndTime;
+ }
+
+ options.LogProgress?.Invoke($"Loading Modules for process {process.Name}");
+
+ var allModules = process.LoadedModules.ToList();
+ foreach (var module in allModules)
+ {
+ options.LogStepProgress?.Invoke($"Loading Symbols for Module `{module.Name}`, ImageSize: {ByteSize.FromBytes(module.ModuleFile.ImageSize)}");
+
+ var lib = new FirefoxProfiler.Lib
+ {
+ Name = module.Name,
+ AddressStart = module.ImageBase,
+ AddressEnd = module.ModuleFile.ImageEnd,
+ Path = module.ModuleFile.FilePath,
+ DebugPath = module.ModuleFile.PdbName,
+ DebugName = module.ModuleFile.PdbName,
+ BreakpadId = $"0x{module.ModuleID:X16}",
+ Arch = "x64" // TODO
+ };
+
+ log.CodeAddresses.LookupSymbolsForModule(_symbolReader, module.ModuleFile);
+ _mapModuleFileIndexToFirefox.Add(module.ModuleFile.ModuleFileIndex, profile.Libs.Count);
+ profile.Libs.Add(lib);
+
+ var fileName = Path.GetFileName(module.FilePath);
+ if (fileName.Equals("clrjit.dll", StringComparison.OrdinalIgnoreCase))
+ {
+ _clrJitModuleIndex = module.ModuleFile.ModuleFileIndex;
+ }
+ else if (fileName.Equals("coreclr.dll", StringComparison.OrdinalIgnoreCase))
+ {
+ _coreClrModuleIndex = module.ModuleFile.ModuleFileIndex;
+ }
+
+ if (module is TraceManagedModule managedModule)
+ {
+ _setManagedModules.Add(managedModule.ModuleFile.ModuleFileIndex);
+
+ foreach (var otherModule in allModules.Where(x => x is not TraceManagedModule))
+ {
+ if (string.Equals(managedModule.FilePath, otherModule.FilePath, StringComparison.OrdinalIgnoreCase))
+ {
+ _setManagedModules.Add(otherModule.ModuleFile.ModuleFileIndex);
+ }
+ }
+ }
+ }
+
+ Dictionary jitCompilePendingMethodId = new();
+
+ double maxCpuTime = 0;
+ int threadIndexWithMaxCpuTime = -1;
+
+ List<(double, GCHeapStatsEvent)> gcHeapStatsEvents = new();
+
+ // Sort threads by CPU time
+ var threads = process.Threads.ToList();
+ threads.Sort((a, b) => b.CPUMSec.CompareTo(a.CPUMSec));
+
+ Stack<(double, GCSuspendExecutionEngineEvent)> gcSuspendEEEvents = new();
+ Stack gcRestartEEEvents = new();
+ Stack<(double, GCEvent)> gcStartStopEvents = new();
+
+ // Add threads
+ foreach (var thread in threads)
+ {
+ _mapCallStackIndexToFirefox.Clear();
+ _mapCodeAddressIndexToFirefox.Clear();
+ _mapMethodIndexToFirefox.Clear();
+ _mapStringToFirefox.Clear();
+ _mapCodeAddressIndexToMethodIndexFirefox.Clear();
+
+ gcSuspendEEEvents.Clear();
+ gcRestartEEEvents.Clear();
+
+ var profileThread = new FirefoxProfiler.Thread
+ {
+ Name = thread.ThreadInfo is not null ? $"{thread.ThreadInfo} ({thread.ThreadID})" : $"Thread ({thread.ThreadID})",
+ ProcessStartupTime = thread.StartTimeRelativeMSec,
+ RegisterTime = thread.StartTimeRelativeMSec,
+ ProcessShutdownTime = thread.EndTimeRelativeMSec,
+ UnregisterTime = thread.EndTimeRelativeMSec,
+ ProcessType = "default",
+ Pid = $"{process.ProcessID}",
+ Tid = $"{thread.ThreadID}",
+ ShowMarkersInTimeline = true
+ };
+
+ options.LogProgress?.Invoke($"Converting Events for Thread: {profileThread.Name}");
+
+ var samples = profileThread.Samples;
+ var markers = profileThread.Markers;
+
+ samples.ThreadCPUDelta = new List();
+ samples.TimeDeltas = new List();
+ samples.WeightType = "samples";
+
+ const TraceEventID GCStartEventID = (TraceEventID)1;
+ const TraceEventID GCStopEventID = (TraceEventID)2;
+ const TraceEventID GCRestartEEStopEventID = (TraceEventID)3;
+ const TraceEventID GCHeapStatsEventID = (TraceEventID)4;
+ const TraceEventID GCCreateSegmentEventID = (TraceEventID)5;
+ const TraceEventID GCFreeSegmentEventID = (TraceEventID)6;
+ const TraceEventID GCRestartEEStartEventID = (TraceEventID)7;
+ const TraceEventID GCSuspendEEStopEventID = (TraceEventID)8;
+ const TraceEventID GCSuspendEEStartEventID = (TraceEventID)9;
+ const TraceEventID GCAllocationTickEventID = (TraceEventID)10;
+
+ double startTime = 0;
+ int currentThread = -1;
+ double switchTimeInMsec = 0.0;
+ //double switchTimeOutMsec = 0.0;
+ foreach (var evt in thread.EventsInThread)
+ {
+ if (evt.Opcode != (TraceEventOpcode)46)
+ {
+ if (evt.Opcode == (TraceEventOpcode)0x24 && evt is CSwitchTraceData switchTraceData)
+ {
+ if (evt.ThreadID == thread.ThreadID && switchTraceData.OldThreadID != thread.ThreadID)
+ {
+ // Old Thread -> This Thread
+ // Switch-in
+ switchTimeInMsec = evt.TimeStampRelativeMSec;
+ }
+ //else if (evt.ThreadID != thread.ThreadID && switchTraceData.OldThreadID == thread.ThreadID)
+ //{
+ // // This Thread -> Other Thread
+ // // Switch-out
+ // switchTimeOutMsec = evt.TimeStampRelativeMSec;
+ //}
+ }
+
+ if (evt.ThreadID == thread.ThreadID)
+ {
+ if (evt is MethodJittingStartedTraceData methodJittingStarted)
+ {
+ var signature = methodJittingStarted.MethodSignature;
+ var indexOfParent = signature.IndexOf('(');
+ if (indexOfParent >= 0)
+ {
+ signature = signature.Substring(indexOfParent);
+ }
+
+ var jitCompile = new JitCompileEvent
+ {
+ FullName = $"{methodJittingStarted.MethodNamespace}.{methodJittingStarted.MethodName}{signature}",
+ MethodILSize = methodJittingStarted.MethodILSize
+ };
+
+ jitCompilePendingMethodId.Add(methodJittingStarted.MethodID, (jitCompile, evt.TimeStampRelativeMSec));
+ }
+ else if (evt is MethodLoadUnloadTraceDataBase methodLoadUnloadVerbose)
+ {
+ if (jitCompilePendingMethodId.TryGetValue(methodLoadUnloadVerbose.MethodID, out var jitCompilePair))
+ {
+ jitCompilePendingMethodId.Remove(methodLoadUnloadVerbose.MethodID);
+
+ markers.StartTime.Add(jitCompilePair.Item2);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryJit);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(profileThreadIndex);
+ markers.Name.Add(GetFirefoxString("JitCompile", profileThread));
+ markers.Data.Add(jitCompilePair.Item1);
+ markers.Length++;
+ }
+ }
+ else if (evt is GCHeapStatsTraceData gcHeapStats)
+ {
+ markers.StartTime.Add(evt.TimeStampRelativeMSec);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGC);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Instance);
+ markers.ThreadId.Add(profileThreadIndex);
+ markers.Name.Add(GetFirefoxString($"GCHeapStats", profileThread));
+
+ var heapStatEvent = new GCHeapStatsEvent
+ {
+ TotalHeapSize = gcHeapStats.TotalHeapSize,
+ TotalPromoted = gcHeapStats.TotalPromoted,
+ GenerationSize0 = gcHeapStats.GenerationSize0,
+ TotalPromotedSize0 = gcHeapStats.TotalPromotedSize0,
+ GenerationSize1 = gcHeapStats.GenerationSize1,
+ TotalPromotedSize1 = gcHeapStats.TotalPromotedSize1,
+ GenerationSize2 = gcHeapStats.GenerationSize2,
+ TotalPromotedSize2 = gcHeapStats.TotalPromotedSize2,
+ GenerationSize3 = gcHeapStats.GenerationSize3,
+ TotalPromotedSize3 = gcHeapStats.TotalPromotedSize3,
+ GenerationSize4 = gcHeapStats.GenerationSize4,
+ TotalPromotedSize4 = gcHeapStats.TotalPromotedSize4,
+ FinalizationPromotedSize = gcHeapStats.FinalizationPromotedSize,
+ FinalizationPromotedCount = gcHeapStats.FinalizationPromotedCount,
+ PinnedObjectCount = gcHeapStats.PinnedObjectCount,
+ SinkBlockCount = gcHeapStats.SinkBlockCount,
+ GCHandleCount = gcHeapStats.GCHandleCount
+ };
+
+ gcHeapStatsEvents.Add((evt.TimeStampRelativeMSec, heapStatEvent));
+
+ markers.Data.Add(heapStatEvent);
+ markers.Length++;
+ }
+ else if (evt is GCAllocationTickTraceData allocationTick)
+ {
+ markers.StartTime.Add(evt.TimeStampRelativeMSec);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGC);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Instance);
+ markers.ThreadId.Add(profileThreadIndex);
+ markers.Name.Add(GetFirefoxString($"GC Alloc ({thread.ThreadID})", profileThread));
+
+ var allocationTickEvent = new GCAllocationTickEvent
+ {
+ AllocationAmount = allocationTick.AllocationAmount,
+ AllocationKind = allocationTick.AllocationKind switch
+ {
+ GCAllocationKind.Small => "Small",
+ GCAllocationKind.Large => "Large",
+ GCAllocationKind.Pinned => "Pinned",
+ _ => "Unknown"
+ },
+ TypeName = allocationTick.TypeName,
+ HeapIndex = allocationTick.HeapIndex
+ };
+ markers.Data.Add(allocationTickEvent);
+ markers.Length++;
+ }
+ else if (evt.ProviderGuid == ClrTraceEventParser.ProviderGuid)
+ {
+ if (evt is GCStartTraceData gcStart)
+ {
+ var gcEvent = new GCEvent
+ {
+ Reason = gcStart.Reason.ToString(),
+ Count = gcStart.Count,
+ Depth = gcStart.Depth,
+ GCType = gcStart.Type.ToString()
+ };
+
+ gcStartStopEvents.Push((evt.TimeStampRelativeMSec, gcEvent));
+ }
+ else if (evt is GCEndTraceData gcEnd)
+ {
+ var (gcEventStartTime, gcEvent) = gcStartStopEvents.Pop();
+
+ markers.StartTime.Add(gcEventStartTime);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGC);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(profileThreadIndex);
+ markers.Name.Add(GetFirefoxString($"GC Event", profileThread));
+ markers.Data.Add(gcEvent);
+ markers.Length++;
+ }
+ else if (evt is GCSuspendEETraceData gcSuspendEE)
+ {
+ var gcSuspendEEEvent = new GCSuspendExecutionEngineEvent
+ {
+ Reason = gcSuspendEE.Reason.ToString(),
+ Count = gcSuspendEE.Count
+ };
+
+ gcSuspendEEEvents.Push((evt.TimeStampRelativeMSec, gcSuspendEEEvent));
+ }
+ else if (evt.ID == GCSuspendEEStopEventID && evt is GCNoUserDataTraceData && gcSuspendEEEvents.Count > 0)
+ {
+ var (gcSuspendEEEventStartTime, gcSuspendEEEvent) = gcSuspendEEEvents.Pop();
+
+ markers.StartTime.Add(gcSuspendEEEventStartTime);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGC);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(profileThreadIndex);
+ markers.Name.Add(GetFirefoxString($"GC Suspend EE", profileThread));
+ markers.Data.Add(gcSuspendEEEvent);
+ markers.Length++;
+ }
+ else if (evt.ID == GCRestartEEStartEventID && evt is GCNoUserDataTraceData)
+ {
+ gcRestartEEEvents.Push(evt.TimeStampRelativeMSec);
+ }
+ else if (evt.ID == GCRestartEEStopEventID && evt is GCNoUserDataTraceData && gcRestartEEEvents.Count > 0)
+ {
+ var gcRestartEEEventStartTime = gcRestartEEEvents.Pop();
+
+ markers.StartTime.Add(gcRestartEEEventStartTime);
+ markers.EndTime.Add(evt.TimeStampRelativeMSec);
+ markers.Category.Add(CategoryGC);
+ markers.Phase.Add(FirefoxProfiler.MarkerPhase.Interval);
+ markers.ThreadId.Add(profileThreadIndex);
+ markers.Name.Add(GetFirefoxString($"GC Restart EE", profileThread));
+ markers.Data.Add(null);
+ markers.Length++;
+ }
+ }
+ }
+
+ continue;
+ }
+
+ if (evt.ProcessID != processId || evt.ThreadID != thread.ThreadID)
+ {
+ continue;
+ }
+
+ //Console.WriteLine($"PERF {evt}");
+
+ var callStackIndex = evt.CallStackIndex();
+ if (callStackIndex == CallStackIndex.Invalid)
+ {
+ continue;
+ }
+
+ // Add sample
+ var firefoxCallStackIndex = ProcessCallStack(callStackIndex, log, profileThread);
+
+ var deltaTime = evt.TimeStampRelativeMSec - startTime;
+ samples.TimeDeltas.Add(deltaTime);
+ samples.Stack.Add(firefoxCallStackIndex);
+ var cpuDeltaMs = (long)((evt.TimeStampRelativeMSec - switchTimeInMsec) * 1_000_000.0);
+ if (cpuDeltaMs > 0)
+ {
+ samples.ThreadCPUDelta.Add((int)cpuDeltaMs);
+ }
+ else
+ {
+ samples.ThreadCPUDelta.Add(0);
+ }
+ switchTimeInMsec = evt.TimeStampRelativeMSec;
+ samples.Length++;
+ startTime = evt.TimeStampRelativeMSec;
+ }
+
+ profile.Threads.Add(profileThread);
+
+ // Make visible threads in the UI that consume a minimum amount of CPU time
+ if (thread.CPUMSec > MinimumCpuTimeBeforeThreadIsVisible)
+ {
+ profile.Meta.InitialVisibleThreads.Add(profileThreadIndex);
+ }
+
+ // We will select by default the thread that has the maximum activity
+ if (thread.CPUMSec > maxCpuTime)
+ {
+ maxCpuTime = thread.CPUMSec;
+ threadIndexWithMaxCpuTime = profileThreadIndex;
+ }
+
+ profileThreadIndex++;
+ }
+
+ if (gcHeapStatsEvents.Count > 0)
+ {
+ gcHeapStatsEvents.Sort((a, b) => a.Item1.CompareTo(b.Item1));
+
+ var gcHeapStatsCounter = new FirefoxProfiler.Counter()
+ {
+ Name = "GCHeapStats",
+ Category = "Memory",
+ Description = "GC Heap Stats",
+ Color = FirefoxProfiler.ProfileColor.Orange, // Doesn't look like it is used
+ Pid = $"{process.ProcessID}",
+ MainThreadIndex = threadIndexWithMaxCpuTime,
+ };
+
+ //gcHeapStatsCounter.Samples.Number = new();
+ gcHeapStatsCounter.Samples.Time = new();
+
+ profile.Counters ??= new();
+ profile.Counters.Add(gcHeapStatsCounter);
+
+ long previousTotalHeapSize = 0;
+
+ foreach (var evt in gcHeapStatsEvents)
+ {
+ gcHeapStatsCounter.Samples.Time!.Add(evt.Item1);
+ // The memory track is special and is assuming a delta
+ var deltaMemory = evt.Item2.TotalHeapSize - previousTotalHeapSize;
+ gcHeapStatsCounter.Samples.Count.Add(deltaMemory);
+ gcHeapStatsCounter.Samples.Length++;
+ previousTotalHeapSize = evt.Item2.TotalHeapSize;
+ }
+ }
+
+ if (profile.Threads.Count > 0)
+ {
+ profile.Meta.InitialSelectedThreads.Add(threadIndexWithMaxCpuTime >= 0 ? threadIndexWithMaxCpuTime : 0);
+ }
+ }
+
+ return profile;
+ }
+
+ private int ProcessCallStack(CallStackIndex callStackIndex, TraceLog log, FirefoxProfiler.Thread profileThread)
+ {
+ if (callStackIndex == CallStackIndex.Invalid) return -1;
+
+ var parentCallStackIndex = log.CallStacks.Caller(callStackIndex);
+ var fireFoxParentCallStackIndex = ProcessCallStack(parentCallStackIndex, log, profileThread);
+
+ return GetFirefoxCallStackIndex(callStackIndex, fireFoxParentCallStackIndex, log, profileThread);
+ }
+
+ private int GetFirefoxString(string text, FirefoxProfiler.Thread profileThread)
+ {
+ if (_mapStringToFirefox.TryGetValue(text, out var index))
+ {
+ return index;
+ }
+ var firefoxStringIndex = profileThread.StringArray.Count;
+ _mapStringToFirefox.Add(text, firefoxStringIndex);
+
+ profileThread.StringArray.Add(text);
+ return firefoxStringIndex;
+ }
+
+ private int GetFirefoxMethodIndex(CodeAddressIndex codeAddressIndex, MethodIndex methodIndex, TraceLog log, FirefoxProfiler.Thread profileThread)
+ {
+ var funcTable = profileThread.FuncTable;
+ int firefoxMethodIndex;
+ if (methodIndex == MethodIndex.Invalid)
+ {
+ if (_mapCodeAddressIndexToMethodIndexFirefox.TryGetValue(codeAddressIndex, out var index))
+ {
+ return index;
+ }
+ firefoxMethodIndex = funcTable.Length;
+ _mapCodeAddressIndexToMethodIndexFirefox[codeAddressIndex] = firefoxMethodIndex;
+ }
+ else if (_mapMethodIndexToFirefox.TryGetValue(methodIndex, out var index))
+ {
+ return index;
+ }
+ else
+ {
+ firefoxMethodIndex = funcTable.Length;
+ _mapMethodIndexToFirefox.Add(methodIndex, firefoxMethodIndex);
+ }
+
+ //public List Name { get; }
+ //public List IsJS { get; }
+ //public List RelevantForJS { get; }
+ //public List Resource { get; }
+ //public List FileName { get; }
+ //public List LineNumber { get; }
+ //public List ColumnNumber { get; }
+
+ if (methodIndex == MethodIndex.Invalid)
+ {
+ funcTable.Name.Add(GetFirefoxString($"0x{log.CodeAddresses.Address(codeAddressIndex):X16}", profileThread));
+ funcTable.IsJS.Add(false);
+ funcTable.RelevantForJS.Add(false);
+ funcTable.Resource.Add(-1);
+ funcTable.FileName.Add(null);
+ funcTable.LineNumber.Add(null);
+ funcTable.ColumnNumber.Add(null);
+ }
+ else
+ {
+ var fullMethodName = log.CodeAddresses.Methods.FullMethodName(methodIndex) ?? $"0x{log.CodeAddresses.Address(codeAddressIndex):X16}";
+
+ var firefoxMethodNameIndex = GetFirefoxString(fullMethodName, profileThread);
+ funcTable.Name.Add(firefoxMethodNameIndex);
+ funcTable.IsJS.Add(false);
+ funcTable.RelevantForJS.Add(false);
+ funcTable.FileName.Add(null); // TODO
+ funcTable.LineNumber.Add(null);
+ funcTable.ColumnNumber.Add(null);
+
+ var moduleIndex = log.CodeAddresses.ModuleFileIndex(codeAddressIndex);
+ if (moduleIndex != ModuleFileIndex.Invalid)
+ {
+ funcTable.Resource.Add(profileThread.ResourceTable.Length); // TODO
+ var moduleName = Path.GetFileName(log.ModuleFiles[moduleIndex].FilePath);
+ profileThread.ResourceTable.Name.Add(GetFirefoxString(moduleName, profileThread));
+ profileThread.ResourceTable.Lib.Add(_mapModuleFileIndexToFirefox[moduleIndex]);
+ profileThread.ResourceTable.Length++;
+ }
+ else
+ {
+ funcTable.Resource.Add(-1);
+ }
+ }
+
+ funcTable.Length++;
+
+ return firefoxMethodIndex;
+ }
+
+ private int GetFirefoxCallStackIndex(CallStackIndex callStackIndex, int firefoxParentCallStackIndex, TraceLog log, FirefoxProfiler.Thread profileThread)
+ {
+ if (_mapCallStackIndexToFirefox.TryGetValue(callStackIndex, out var index))
+ {
+ return index;
+ }
+ var stackTable = profileThread.StackTable;
+
+ var firefoxCallStackIndex = stackTable.Length;
+ _mapCallStackIndexToFirefox.Add(callStackIndex, firefoxCallStackIndex);
+
+ var codeAddressIndex = log.CallStacks.CodeAddressIndex(callStackIndex);
+ var frameTableIndex = GetFirefoxFrameTableIndex(codeAddressIndex, log, profileThread, out var category, out var subCategory);
+
+ stackTable.Frame.Add(frameTableIndex);
+ stackTable.Category.Add(category);
+ stackTable.Subcategory.Add(subCategory);
+ stackTable.Prefix.Add(firefoxParentCallStackIndex < 0 ? null : (int)firefoxParentCallStackIndex);
+ stackTable.Length++;
+
+ return firefoxCallStackIndex;
+ }
+
+ private int GetFirefoxFrameTableIndex(CodeAddressIndex codeAddressIndex, TraceLog log, FirefoxProfiler.Thread profileThread, out int category, out int subCategory)
+ {
+ var frameTable = profileThread.FrameTable;
+
+ if (_mapCodeAddressIndexToFirefox.TryGetValue(codeAddressIndex, out var firefoxFrameTableIndex))
+ {
+ category = frameTable.Category[firefoxFrameTableIndex]!.Value;
+ subCategory = frameTable.Subcategory[firefoxFrameTableIndex]!.Value;
+ return firefoxFrameTableIndex;
+ }
+
+ firefoxFrameTableIndex = frameTable.Length;
+ _mapCodeAddressIndexToFirefox.Add(codeAddressIndex, firefoxFrameTableIndex);
+
+ var module = log.CodeAddresses.ModuleFile(codeAddressIndex);
+ var absoluteAddress = log.CodeAddresses.Address(codeAddressIndex);
+ var offsetIntoModule = module is not null ? (int)(absoluteAddress - module.ImageBase) : 0;
+
+ // Address
+ // InlineDepth
+ // Category
+ // Subcategory
+ // Func
+ // NativeSymbol
+ // InnerWindowID
+ // Implementation
+ // Line
+ // Column
+
+ frameTable.Address.Add(offsetIntoModule);
+ frameTable.InlineDepth.Add(0);
+
+ bool isManaged = false;
+ if (module is not null)
+ {
+ isManaged = _setManagedModules.Contains(module.ModuleFileIndex);
+ }
+
+ subCategory = 0;
+
+ if (isManaged)
+ {
+ category = CategoryManaged;
+ }
+ else
+ {
+ bool isKernel = (absoluteAddress >> 56) == 0xFF;
+ category = isKernel ? CategoryKernel : CategoryNative;
+
+ if (module != null)
+ {
+ if (module.ModuleFileIndex == _clrJitModuleIndex)
+ {
+ category = CategoryJit;
+ }
+ else if (module.ModuleFileIndex == _coreClrModuleIndex)
+ {
+ category = CategoryClr;
+ }
+ }
+ }
+
+ var methodIndex = log.CodeAddresses.MethodIndex(codeAddressIndex);
+ var firefoxMethodIndex = GetFirefoxMethodIndex(codeAddressIndex, methodIndex, log, profileThread);
+
+ if (methodIndex != MethodIndex.Invalid)
+ {
+ var nameIndex = profileThread.FuncTable.Name[firefoxMethodIndex];
+ var fullMethodName = profileThread.StringArray[nameIndex];
+ // Hack to distinguish GC methods
+ var isGC = fullMethodName.StartsWith("WKS::gc", StringComparison.OrdinalIgnoreCase) || fullMethodName.StartsWith("SVR::gc", StringComparison.OrdinalIgnoreCase);
+ if (isGC)
+ {
+ category = CategoryGC;
+ }
+ }
+
+ frameTable.Category.Add(category);
+ frameTable.Subcategory.Add(subCategory);
+
+ frameTable.Func.Add(firefoxMethodIndex);
+
+ // Set other fields to null
+ frameTable.NativeSymbol.Add(null);
+ frameTable.InnerWindowID.Add(null);
+ frameTable.Implementation.Add(null);
+
+ //var sourceLine = log.CodeAddresses.GetSourceLine(_symbolReader, codeAddressIndex);
+ //if (sourceLine != null)
+ //{
+ // ft.Line.Add(sourceLine.LineNumber);
+ // ft.Column.Add(sourceLine.ColumnNumber);
+ //}
+ //else
+ {
+ frameTable.Line.Add(null);
+ frameTable.Column.Add(null);
+ }
+ frameTable.Length++;
+
+ return firefoxFrameTableIndex;
+ }
+
+ public void Dispose()
+ {
+ _symbolReader?.Dispose();
+ _etl.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/EtwUltraProfiler.cs b/src/Ultra.Core/EtwUltraProfiler.cs
new file mode 100644
index 0000000..5b72be1
--- /dev/null
+++ b/src/Ultra.Core/EtwUltraProfiler.cs
@@ -0,0 +1,403 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Diagnostics;
+using System.IO.Compression;
+using System.Text.Json;
+using ByteSizeLib;
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Parsers;
+using Microsoft.Diagnostics.Tracing.Parsers.Clr;
+using Microsoft.Diagnostics.Tracing.Session;
+
+namespace Ultra.Core;
+
+public class EtwUltraProfiler : IDisposable
+{
+ private TraceEventSession? _userSession;
+ private TraceEventSession? _kernelSession;
+ private bool _cancelRequested;
+ private ManualResetEvent? _cleanCancel;
+ private bool _stopRequested;
+
+ public bool Cancel()
+ {
+ if (!_cancelRequested)
+ {
+ _cleanCancel = new ManualResetEvent(false);
+ _cancelRequested = true;
+ return false;
+ }
+ else
+ {
+ _stopRequested = true;
+
+ // Before really canceling, wait for the clean cancel to be done
+ WaitForCleanCancel();
+ return true;
+ }
+ }
+
+ private void WaitForCleanCancel()
+ {
+ if (_cleanCancel is not null)
+ {
+ _cleanCancel.WaitOne();
+ _cleanCancel.Dispose();
+ _cleanCancel = null;
+ }
+ }
+
+ public static bool IsElevated()
+ {
+ var isElevated = TraceEventSession.IsElevated();
+ return isElevated.HasValue && isElevated.Value;
+ }
+
+ public async Task Run(EtwUltraProfilerOptions ultraProfilerOptions)
+ {
+ List processList = new List();
+ if (ultraProfilerOptions.ProcessIds.Count > 0)
+ {
+ foreach (var pidToAttach in ultraProfilerOptions.ProcessIds)
+ {
+ try
+ {
+ var process = System.Diagnostics.Process.GetProcessById(pidToAttach);
+ processList.Add(process);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new ArgumentException($"Unable to find Process with pid {pidToAttach}");
+ }
+ }
+ }
+
+ if (processList.Count == 0 && ultraProfilerOptions.ProgramPath is null)
+ {
+ throw new ArgumentException("pid is required or an executable with optional arguments");
+ }
+
+ string? processName = null;
+
+ System.Diagnostics.Process? singleProcess = null;
+
+ if (processList.Count == 1 && ultraProfilerOptions.ProgramPath is null)
+ {
+ singleProcess = processList[0];
+ processName = singleProcess.ProcessName;
+ }
+ else if (ultraProfilerOptions.ProgramPath != null)
+ {
+ if (!ultraProfilerOptions.ProgramPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"Executable path {ultraProfilerOptions.ProgramPath} must end with .exe");
+ }
+
+ processName = Path.GetFileNameWithoutExtension(ultraProfilerOptions.ProgramPath);
+ }
+
+ var currentTime = DateTime.Now;
+ var baseName = processName != null ? $"ultra_{processName}_{currentTime:yyyy-MM-dd_HH_mm_ss}" : $"ultra_{currentTime:yyyy-MM-dd_HH_mm_ss}";
+
+ // Append the pid for a single process that we are attaching to
+ if (singleProcess is not null)
+ {
+ baseName = $"{baseName}_{singleProcess.Id}";
+ }
+
+ var options = new TraceEventProviderOptions()
+ {
+ StacksEnabled = true,
+ };
+
+ // Filter the requested process ids
+ if (processList.Count > 0)
+ {
+ options.ProcessIDFilter = new List();
+ foreach (var process in processList)
+ {
+ options.ProcessIDFilter.Add(process.Id);
+ }
+ }
+
+ // Make sure to filter the process name if we have a single process
+ if (ultraProfilerOptions.ProgramPath != null)
+ {
+ options.ProcessNameFilter = [Path.GetFileName(ultraProfilerOptions.ProgramPath)];
+ }
+
+ var kernelFileName = $"{baseName}.kernel.etl";
+ var userFileName = $"{baseName}.user.etl";
+
+ var clock = Stopwatch.StartNew();
+ var lastTime = clock.Elapsed;
+
+ _userSession = new TraceEventSession($"{baseName}-user", userFileName);
+ _kernelSession = new TraceEventSession($"{baseName}-kernel", kernelFileName);
+
+ try
+ {
+ using (_userSession)
+ using (_kernelSession)
+ {
+ _kernelSession.StopOnDispose = true;
+ _kernelSession.CircularBufferMB = 0;
+ _kernelSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs;
+ _kernelSession.StackCompression = false;
+
+ _userSession.StopOnDispose = true;
+ _userSession.CircularBufferMB = 0;
+ _userSession.CpuSampleIntervalMSec = ultraProfilerOptions.CpuSamplingIntervalInMs;
+ _userSession.StackCompression = false;
+
+ var kernelEvents = KernelTraceEventParser.Keywords.Profile
+ | KernelTraceEventParser.Keywords.ContextSwitch
+ | KernelTraceEventParser.Keywords.ImageLoad
+ | KernelTraceEventParser.Keywords.Process
+ | KernelTraceEventParser.Keywords.Thread;
+ _kernelSession.EnableKernelProvider(kernelEvents, KernelTraceEventParser.Keywords.Profile);
+
+ var jitEvents = ClrTraceEventParser.Keywords.JITSymbols |
+ ClrTraceEventParser.Keywords.Exception |
+ ClrTraceEventParser.Keywords.GC |
+ ClrTraceEventParser.Keywords.GCHeapAndTypeNames |
+ ClrTraceEventParser.Keywords.Interop |
+ ClrTraceEventParser.Keywords.JITSymbols |
+ ClrTraceEventParser.Keywords.Jit |
+ ClrTraceEventParser.Keywords.JittedMethodILToNativeMap |
+ ClrTraceEventParser.Keywords.Loader |
+ ClrTraceEventParser.Keywords.Stack |
+ ClrTraceEventParser.Keywords.StartEnumeration;
+
+ _userSession.EnableProvider(
+ ClrTraceEventParser.ProviderGuid,
+ TraceEventLevel.Verbose, // For call stacks.
+ (ulong)jitEvents, options);
+
+ HashSet exitedProcessList = new();
+
+ // Start a command line process if needed
+ if (ultraProfilerOptions.ProgramPath is not null)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = ultraProfilerOptions.ProgramPath,
+ UseShellExecute = true,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+
+ foreach (var arg in ultraProfilerOptions.Arguments)
+ {
+ startInfo.ArgumentList.Add(arg);
+ }
+
+ ultraProfilerOptions.LogProgress?.Invoke($"Starting Process {startInfo.FileName} {string.Join(" ", startInfo.ArgumentList)}");
+ var process = System.Diagnostics.Process.Start(startInfo)!;
+ processList.Add(process);
+ singleProcess ??= process;
+ }
+
+ foreach (var process in processList)
+ {
+ ultraProfilerOptions.LogProgress?.Invoke($"Start Profiling Process {process.ProcessName} ({process.Id})");
+ }
+
+ // Collect the data until all processes have exited or there is a cancel request
+ while (!_cancelRequested)
+ {
+ if (clock.Elapsed.TotalMilliseconds - lastTime.TotalMilliseconds > ultraProfilerOptions.UpdateLogAfterInMs)
+ {
+ var userFileNameLength = new FileInfo(userFileName).Length;
+ var kernelFileNameLength = new FileInfo(kernelFileName).Length;
+ var totalFileNameLength = userFileNameLength + kernelFileNameLength;
+
+ ultraProfilerOptions.LogStepProgress?.Invoke(singleProcess is not null
+ ? $"Profiling Process {singleProcess.ProcessName} ({singleProcess.Id}) - {(int)clock.Elapsed.TotalSeconds}s - {ByteSize.FromBytes(totalFileNameLength)}"
+ : $"Profiling {processList.Count} Processes - {(int)clock.Elapsed.TotalSeconds}s - {ByteSize.FromBytes(totalFileNameLength)}");
+ lastTime = clock.Elapsed;
+ }
+
+ await Task.Delay(ultraProfilerOptions.CheckDeltaTimeInMs);
+
+ foreach (var process in processList)
+ {
+ if (process.HasExited && exitedProcessList.Add(process))
+ {
+ ultraProfilerOptions.LogProgress?.Invoke($"Process {process.ProcessName} ({process.Id}) has exited");
+ }
+ }
+
+ if (exitedProcessList.Count == processList.Count)
+ {
+ break;
+ }
+
+ } // Needed for JIT Compile code that was already compiled.
+
+ _kernelSession.Stop();
+ _userSession.Stop();
+
+ ultraProfilerOptions.LogProgress?.Invoke(singleProcess is not null ? $"End Profiling Process" : $"End Profiling {processList.Count} Processes");
+
+ await WaitForStaleFile(userFileName, ultraProfilerOptions);
+ await WaitForStaleFile(kernelFileName, ultraProfilerOptions);
+ }
+ }
+ catch
+ {
+ // Delete intermediate files if we have an exception
+ File.Delete(kernelFileName);
+ File.Delete(userFileName);
+ throw;
+ }
+ finally
+ {
+ _userSession = null;
+ _kernelSession = null;
+ _cleanCancel?.Set();
+ }
+
+ if (_stopRequested)
+ {
+ throw new InvalidOperationException("CTRL+C requested");
+ }
+
+ var rundownSession = $"{baseName}.rundown.etl";
+ using (TraceEventSession clrRundownSession = new TraceEventSession($"{baseName}-rundown", rundownSession))
+ {
+ clrRundownSession.StopOnDispose = true;
+ clrRundownSession.CircularBufferMB = 0;
+
+ ultraProfilerOptions.LogProgress?.Invoke($"Running CLR Rundown");
+
+ // The runtime does method rundown first then the module rundown. This means if you have a large
+ // number of methods and method rundown does not complete you don't get ANYTHING. To avoid this
+ // we first trigger all module (loader) rundown and then trigger the method rundown
+ clrRundownSession.EnableProvider(
+ ClrRundownTraceEventParser.ProviderGuid,
+ TraceEventLevel.Verbose,
+ (ulong)(ClrRundownTraceEventParser.Keywords.Loader | ClrRundownTraceEventParser.Keywords.ForceEndRundown), options);
+
+ await Task.Delay(500);
+
+ clrRundownSession.EnableProvider(
+ ClrRundownTraceEventParser.ProviderGuid,
+ TraceEventLevel.Verbose,
+ (ulong)(ClrRundownTraceEventParser.Keywords.Default & ~ClrRundownTraceEventParser.Keywords.Loader), options);
+
+ await Task.Delay(500);
+
+ await WaitForStaleFile(rundownSession, ultraProfilerOptions);
+ }
+
+ if (_stopRequested)
+ {
+ throw new InvalidOperationException("CTRL+C requested");
+ }
+
+ ultraProfilerOptions.LogProgress?.Invoke($"Merging ETL Files");
+ // Merge file (and to force Volume mapping)
+ var etlFinalFile = $"{baseName}.etl";
+ TraceEventSession.Merge([kernelFileName, userFileName, rundownSession], etlFinalFile);
+ //TraceEventSession.Merge([kernelFileName, userFileName], $"{baseName}.etl");
+
+ if (_stopRequested)
+ {
+ throw new InvalidOperationException("CTRL+C requested");
+ }
+
+ if (!ultraProfilerOptions.KeepEtlIntermediateFiles)
+ {
+ File.Delete(kernelFileName);
+ File.Delete(userFileName);
+ File.Delete(rundownSession);
+ }
+
+ if (_stopRequested)
+ {
+ throw new InvalidOperationException("CTRL+C requested");
+ }
+
+ var jsonFinalFile = await Convert(etlFinalFile, processList.Select(x => x.Id).ToList(), ultraProfilerOptions);
+
+ if (!ultraProfilerOptions.KeepMergedEtl)
+ {
+ File.Delete(etlFinalFile);
+ File.Delete($"{baseName}.etlx");
+ }
+
+ return jsonFinalFile;
+ }
+
+ public async Task Convert(string etlFile, List pIds, EtwUltraProfilerOptions ultraProfilerOptions)
+ {
+ var etlProcessor = new EtwConverterToFirefox();
+ var profile = etlProcessor.Convert(etlFile, pIds, ultraProfilerOptions);
+
+ if (_stopRequested)
+ {
+ throw new InvalidOperationException("CTRL+C requested");
+ }
+
+ var directory = Path.GetDirectoryName(etlFile);
+ var etlFileNameWithoutExtension = Path.GetFileNameWithoutExtension(etlFile);
+ var jsonFinalFile = $"{etlFileNameWithoutExtension}.json.gz";
+ ultraProfilerOptions.LogProgress?.Invoke($"Converting to Firefox Profiler JSON");
+ await using var stream = File.Create(jsonFinalFile);
+ await using var gzipStream = new GZipStream(stream, CompressionLevel.Optimal);
+ await JsonSerializer.SerializeAsync(gzipStream, profile, FirefoxProfiler.JsonProfilerContext.Default.Profile);
+ gzipStream.Flush();
+
+ return jsonFinalFile;
+ }
+
+ private async Task WaitForStaleFile(string file, EtwUltraProfilerOptions options)
+ {
+ var clock = Stopwatch.StartNew();
+ var startTime = clock.ElapsedMilliseconds;
+ var fileInfo = new FileInfo(file);
+ if (!fileInfo.Exists) return;
+ var length = fileInfo.Length;
+ long lastTimeLogInMs = -1;
+ while (true)
+ {
+ fileInfo.Refresh();
+ var newLength = fileInfo.Length;
+ if (newLength != length)
+ {
+ length = newLength;
+ }
+ else
+ {
+ break;
+ }
+
+ if (lastTimeLogInMs < 0 || (clock.ElapsedMilliseconds - lastTimeLogInMs) > options.UpdateLogAfterInMs)
+ {
+ options.WaitingFileToComplete?.Invoke(file);
+ lastTimeLogInMs = clock.ElapsedMilliseconds;
+ }
+
+ if (clock.ElapsedMilliseconds - startTime > options.TimeOutAfterInMs)
+ {
+ options.WaitingFileToCompleteTimeOut?.Invoke(file);
+ break;
+ }
+
+ await Task.Delay(options.CheckDeltaTimeInMs);
+ }
+ }
+
+ public void Dispose()
+ {
+ _userSession?.Dispose();
+ _userSession = null;
+ _kernelSession?.Dispose();
+ _kernelSession = null;
+ _cleanCancel?.Dispose();
+ _cleanCancel = null;
+ }
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/EtwUltraProfilerOptions.cs b/src/Ultra.Core/EtwUltraProfilerOptions.cs
new file mode 100644
index 0000000..acd5383
--- /dev/null
+++ b/src/Ultra.Core/EtwUltraProfilerOptions.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using Microsoft.Diagnostics.Symbols;
+
+namespace Ultra.Core;
+
+public class EtwUltraProfilerOptions
+{
+ public EtwUltraProfilerOptions()
+ {
+ CheckDeltaTimeInMs = 100;
+ UpdateLogAfterInMs = 1000;
+ TimeOutAfterInMs = 30 * 1000; // 30s
+ CpuSamplingIntervalInMs = 1000.0f / 8190.0f;
+ KeepMergedEtl = false;
+ KeepEtlIntermediateFiles = false;
+ }
+
+ public List ProcessIds { get; } = new();
+
+ public string? ProgramPath { get; set; }
+
+ public List Arguments { get; } = new();
+
+ public int CheckDeltaTimeInMs { get; set; }
+
+ public int UpdateLogAfterInMs { get; set; }
+
+ public int TimeOutAfterInMs { get; set; }
+
+ public Action? LogProgress;
+
+ public Action? LogStepProgress;
+
+ public Action? WaitingFileToComplete;
+
+ public Action? WaitingFileToCompleteTimeOut;
+
+ public bool KeepEtlIntermediateFiles { get; set; }
+
+ public bool KeepMergedEtl { get; set; }
+
+ public float CpuSamplingIntervalInMs { get; set; }
+
+ public string? SymbolPathText { get; set; }
+
+ public SymbolPath GetCachedSymbolPath()
+ {
+ var symbolPath = new SymbolPath();
+ if (SymbolPathText != null)
+ {
+ symbolPath.Add(SymbolPathText);
+ }
+ else
+ {
+ var symbolPathText = SymbolPath.SymbolPathFromEnvironment;
+ if (string.IsNullOrEmpty(symbolPathText))
+ {
+ symbolPathText = $"{SymbolPath.MicrosoftSymbolServerPath};SRV*https://symbols.nuget.org/download/symbols";
+ }
+
+ symbolPath.Add(symbolPathText);
+ }
+
+ return symbolPath.InsureHasCache(symbolPath.DefaultSymbolCache()).CacheFirst();
+ }
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/FirefoxProfiler.cs b/src/Ultra.Core/FirefoxProfiler.cs
new file mode 100644
index 0000000..eeec3e4
--- /dev/null
+++ b/src/Ultra.Core/FirefoxProfiler.cs
@@ -0,0 +1,1297 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+// ReSharper disable InconsistentNaming
+
+namespace Ultra.Core;
+
+// https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js
+
+// Ideas: https://github.com/parttimenerd/jfrtofp/blob/main/src/main/kotlin/me/bechberger/jfrtofp/types/Marker.kt
+
+// Profile code source loading:
+// https://github.com/firefox-devtools/profiler/blob/e51f64485f85091e5c3f5fc692e69068b3324fbd/src/utils/special-paths.js#L52-L90
+
+public static partial class FirefoxProfiler
+{
+ [JsonSourceGenerationOptions(
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ //Converters = [
+ // //typeof(KebabCaseEnumConverter),
+ // //typeof(KebabCaseEnumConverter),
+ // //typeof(MarkerPayloadConverter),
+ // //typeof(ProfileColorEnumConverter)
+ // ]
+ )
+ ]
+ [JsonSerializable(typeof(Profile))]
+ [JsonSerializable(typeof(MarkerTableFormatType))]
+ //[JsonSerializable(typeof(MarkerDisplayLocation))]
+ //[JsonSerializable(typeof(MarkerGraphType))]
+ //[JsonSerializable(typeof(ProfileColor))]
+ public partial class JsonProfilerContext : JsonSerializerContext
+ {
+ }
+
+ public class StackTable
+ {
+ public StackTable()
+ {
+ Frame = new List();
+ Category = new List();
+ Subcategory = new List();
+ Prefix = new List();
+ }
+
+ public List Frame { get; }
+
+ public List Category { get; }
+
+ public List Subcategory { get; }
+
+ public List Prefix { get; }
+
+ public int Length { get; set; }
+ }
+
+ public abstract class SamplesLikeTable
+ {
+ public SamplesLikeTable()
+ {
+ Stack = new List();
+ }
+
+ public List Stack { get; }
+
+ public List? Time { get; set; }
+
+ public List? TimeDeltas { get; set; }
+
+ public List? Weight { get; set; }
+
+ public string WeightType { get; set; } = string.Empty;
+
+ public int Length { get; set; }
+ }
+
+ public class SamplesTable : SamplesLikeTable
+ {
+ public SamplesTable()
+ {
+ }
+
+ public List? Responsiveness { get; set; }
+
+ public List? EventDelay { get; set; }
+
+ public List? ThreadCPUDelta { get; set; }
+
+ public List? ThreadId { get; set; }
+ }
+
+ public class JsAllocationsTable : SamplesLikeTable
+ {
+ public JsAllocationsTable()
+ {
+ ClassName = new List();
+ TypeName = new List();
+ CoarseType = new List();
+ InNursery = new List();
+ }
+
+ public List ClassName { get; }
+
+ public List TypeName { get; }
+
+ public List CoarseType { get; }
+
+ public List InNursery { get; }
+ }
+
+ public class RawMarkerTable
+ {
+ public RawMarkerTable()
+ {
+ Data = new List();
+ Name = new List();
+ StartTime = new List();
+ EndTime = new List();
+ Phase = new List();
+ Category = new List();
+ ThreadId = new List();
+ }
+
+ public List Data { get; }
+
+ public List Name { get; }
+
+ public List StartTime { get; }
+
+ public List EndTime { get; }
+
+ public List Phase { get; }
+
+ public List Category { get; }
+
+ public List ThreadId { get; }
+
+ public int Length { get; set; }
+ }
+
+ public enum MarkerPhase
+ {
+ Instance = 0,
+ Interval = 1,
+ IntervalStart = 2,
+ IntervalEnd = 3,
+ }
+
+ public class FrameTable
+ {
+ public FrameTable()
+ {
+ Address = new List();
+ InlineDepth = new List();
+ Category = new List();
+ Subcategory = new List();
+ Func = new List();
+ NativeSymbol = new List();
+ InnerWindowID = new List();
+ Implementation = new List();
+ Line = new List();
+ Column = new List();
+ }
+
+ public List Address { get; }
+
+ public List InlineDepth { get; }
+
+ public List Category { get; }
+
+ public List Subcategory { get; }
+
+ public List Func { get; }
+
+ public List NativeSymbol { get; }
+
+ public List InnerWindowID { get; }
+
+ public List Implementation { get; }
+
+ public List Line { get; }
+
+ public List Column { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class FuncTable
+ {
+ public FuncTable()
+ {
+ Name = new List();
+ IsJS = new List();
+ RelevantForJS = new List();
+ Resource = new List();
+ FileName = new List();
+ LineNumber = new List();
+ ColumnNumber = new List();
+ }
+
+ public List Name { get; }
+
+ public List IsJS { get; }
+
+ public List RelevantForJS { get; }
+
+ public List Resource { get; }
+
+ public List FileName { get; }
+
+ public List LineNumber { get; }
+
+ public List ColumnNumber { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class NativeSymbolTable
+ {
+ public NativeSymbolTable()
+ {
+ LibIndex = new List();
+ Address = new List();
+ Name = new List();
+ FunctionSize = new List();
+ }
+
+ public List LibIndex { get; }
+
+ public List Address { get; }
+
+ public List Name { get; }
+
+ public List FunctionSize { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class ResourceTable
+ {
+ public ResourceTable()
+ {
+ Lib = new List();
+ Name = new List();
+ Host = new List();
+ Type = new List();
+ }
+
+ public List Lib { get; }
+
+ public List Name { get; }
+
+ public List Host { get; }
+
+ public List Type { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class Lib
+ {
+ public Lib()
+ {
+ Arch = string.Empty;
+ Name = string.Empty;
+ Path = string.Empty;
+ DebugName = string.Empty;
+ DebugPath = string.Empty;
+ BreakpadId = string.Empty;
+ }
+
+ public ulong? AddressStart { get; set; }
+
+ public ulong? AddressEnd { get; set; }
+
+ public ulong? AddressOffset { get; set; }
+
+ public string Arch { get; set; }
+
+ public string Name { get; set; }
+
+ public string Path { get; set; }
+
+ public string DebugName { get; set; }
+
+ public string DebugPath { get; set; }
+
+ public string BreakpadId { get; set; }
+
+ public string? CodeId { get; set; }
+ }
+
+ public class Category
+ {
+ public Category()
+ {
+ Name = string.Empty;
+ Color = ProfileColor.Grey;
+ Subcategories = new List();
+ }
+
+ public string Name { get; set; }
+
+ public ProfileColor Color { get; set; }
+
+ public List Subcategories { get; }
+ }
+
+ public class Page
+ {
+ public Page()
+ {
+ Url = string.Empty;
+ }
+
+ public int TabID { get; set; }
+
+ public int InnerWindowID { get; set; }
+
+ public string Url { get; set; }
+
+ public int EmbedderInnerWindowID { get; set; }
+
+ public bool? IsPrivateBrowsing { get; set; }
+
+ public string? Favicon { get; set; }
+ }
+
+ public class PausedRange
+ {
+ public double? StartTime { get; set; }
+
+ public double? EndTime { get; set; }
+
+ public string Reason { get; set; } = string.Empty;
+ }
+
+ public class ProfilerConfiguration
+ {
+ public ProfilerConfiguration()
+ {
+ Threads = new List();
+ Features = new List();
+ }
+
+ public List Threads { get; }
+
+ public List Features { get; }
+
+ public int Capacity { get; set; }
+
+ public int? Duration { get; set; }
+
+ public int? ActiveTabID { get; set; }
+ }
+
+ public class VisualMetrics
+ {
+ public VisualMetrics()
+ {
+ VisualProgress = new List();
+ ContentfulSpeedIndexProgress = new List();
+ PerceptualSpeedIndexProgress = new List();
+ }
+
+ [JsonPropertyName("FirstVisualChange")]
+ public int FirstVisualChange { get; set; }
+
+ [JsonPropertyName("LastVisualChange")]
+ public int LastVisualChange { get; set; }
+
+ [JsonPropertyName("SpeedIndex")]
+ public int SpeedIndex { get; set; }
+
+ [JsonPropertyName("VisualProgress")]
+ public List VisualProgress { get; }
+
+ [JsonPropertyName("ContentfulSpeedIndex")]
+ public int? ContentfulSpeedIndex { get; set; }
+
+ [JsonPropertyName("ContentfulSpeedIndexProgress")]
+ public List ContentfulSpeedIndexProgress { get; }
+
+ [JsonPropertyName("PerceptualSpeedIndex")]
+ public int? PerceptualSpeedIndex { get; set; }
+
+ [JsonPropertyName("PerceptualSpeedIndexProgress")]
+ public List PerceptualSpeedIndexProgress { get; }
+
+ [JsonPropertyName("VisualReadiness")]
+ public int VisualReadiness { get; set; }
+
+ [JsonPropertyName("VisualComplete85")]
+ public int VisualComplete85 { get; set; }
+
+ [JsonPropertyName("VisualComplete95")]
+ public int VisualComplete95 { get; set; }
+
+ [JsonPropertyName("VisualComplete99")]
+ public int VisualComplete99 { get; set; }
+ }
+
+ public class ProgressGraphData
+ {
+ public int Percent { get; set; }
+
+ public double? Timestamp { get; set; }
+ }
+
+ public class ProfilerOverheadStats
+ {
+ public int MaxCleaning { get; set; }
+
+ public int MaxCounter { get; set; }
+
+ public int MaxInterval { get; set; }
+
+ public int MaxLockings { get; set; }
+
+ public int MaxOverhead { get; set; }
+
+ public int MaxThread { get; set; }
+
+ public int MeanCleaning { get; set; }
+
+ public int MeanCounter { get; set; }
+
+ public int MeanInterval { get; set; }
+
+ public int MeanLockings { get; set; }
+
+ public int MeanOverhead { get; set; }
+
+ public int MeanThread { get; set; }
+
+ public int MinCleaning { get; set; }
+
+ public int MinCounter { get; set; }
+
+ public int MinInterval { get; set; }
+
+ public int MinLockings { get; set; }
+
+ public int MinOverhead { get; set; }
+
+ public int MinThread { get; set; }
+
+ public int OverheadDurations { get; set; }
+
+ public int OverheadPercentage { get; set; }
+
+ public int ProfiledDuration { get; set; }
+
+ public int SamplingCount { get; set; }
+ }
+
+ public class ProfilerOverheadSamplesTable
+ {
+ public ProfilerOverheadSamplesTable()
+ {
+ Counters = new List();
+ ExpiredMarkerCleaning = new List();
+ Locking = new List();
+ Threads = new List();
+ Time = new List();
+ }
+
+ public List Counters { get; }
+
+ public List ExpiredMarkerCleaning { get; }
+
+ public List Locking { get; }
+
+ public List Threads { get; }
+
+ public List Time { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class ProfilerOverhead
+ {
+ public ProfilerOverhead()
+ {
+ Samples = new ProfilerOverheadSamplesTable();
+ Pid = string.Empty;
+ }
+
+ public ProfilerOverheadSamplesTable Samples { get; }
+
+ public ProfilerOverheadStats? Statistics { get; set; }
+
+ public string Pid { get; set; }
+
+ public int MainThreadIndex { get; set; }
+ }
+
+ public class Thread
+ {
+ public Thread()
+ {
+ PausedRanges = new List();
+ Name = string.Empty;
+ Samples = new SamplesTable();
+ Markers = new RawMarkerTable();
+ StackTable = new StackTable();
+ FrameTable = new FrameTable();
+ StringArray = new List();
+ FuncTable = new FuncTable();
+ ResourceTable = new ResourceTable();
+ NativeSymbols = new NativeSymbolTable();
+ Pid = string.Empty;
+ Tid = string.Empty;
+ }
+
+ public string ProcessType { get; set; } = string.Empty;
+
+ public double ProcessStartupTime { get; set; }
+
+ public double? ProcessShutdownTime { get; set; }
+
+ public double RegisterTime { get; set; }
+
+ public double? UnregisterTime { get; set; }
+
+ public List PausedRanges { get; }
+
+ public bool? ShowMarkersInTimeline { get; set; }
+
+ public string Name { get; set; }
+
+ public bool IsMainThread { get; set; }
+
+ [JsonPropertyName("eTLD+1")]
+ public string? ETLDPlus1 { get; set; }
+
+ public string? ProcessName { get; set; }
+
+ public bool? IsJsTracer { get; set; }
+
+ public string Pid { get; set; }
+
+ public string Tid { get; set; }
+
+ public SamplesTable Samples { get; }
+
+ //[JsonPropertyName("jsAllocations")]
+ //public JsAllocationsTable? JsAllocations { get; set; }
+
+ //[JsonPropertyName("nativeAllocations")]
+ //public NativeAllocationsTable? NativeAllocations { get; set; }
+
+ public RawMarkerTable Markers { get; }
+
+ public StackTable StackTable { get; }
+
+ public FrameTable FrameTable { get; }
+
+ public List StringArray { get; }
+
+ public FuncTable FuncTable { get; }
+
+ public ResourceTable ResourceTable { get; }
+
+ public NativeSymbolTable NativeSymbols { get; }
+
+ public JsTracerTable? JsTracer { get; set; }
+
+ public bool? IsPrivateBrowsing { get; set; }
+
+ public int? UserContextId { get; set; }
+ }
+
+
+ public abstract class NativeAllocationsTable : SamplesLikeTable
+ {
+ }
+
+ public class BalancedNativeAllocationsTable : NativeAllocationsTable
+ {
+ public BalancedNativeAllocationsTable()
+ {
+ MemoryAddress = new();
+ ThreadId = new();
+ }
+
+ public List MemoryAddress { get; }
+
+ public List ThreadId { get; }
+ }
+
+ public class UnbalancedNativeAllocationsTable : NativeAllocationsTable
+ {
+ }
+
+
+ public class ProfileMeta
+ {
+ public ProfileMeta()
+ {
+ MarkerSchema = new List();
+ Interval = 0;
+ StartTime = 0;
+ Product = string.Empty;
+ }
+
+ public double Interval { get; set; }
+
+ public double StartTime { get; set; }
+
+ public double? EndTime { get; set; }
+
+ public double? ProfilingStartTime { get; set; }
+
+ public double? ProfilingEndTime { get; set; }
+
+ public int ProcessType { get; set; }
+
+ public ExtensionTable? Extensions { get; set; }
+
+ public List? Categories { get; set; }
+
+ public string Product { get; set; }
+
+ public int Stackwalk { get; set; }
+
+ public bool? Debug { get; set; }
+
+ public int Version { get; set; }
+
+ public int PreprocessedProfileVersion { get; set; }
+
+ public string? Abi { get; set; }
+
+ public string? Misc { get; set; }
+
+ public string? Oscpu { get; set; }
+
+ public int? MainMemory { get; set; }
+
+ public string? Platform { get; set; }
+
+ public string? Toolkit { get; set; }
+
+ public string? AppBuildID { get; set; }
+
+ public string? Arguments { get; set; }
+
+ public string? SourceURL { get; set; }
+
+ public int? PhysicalCPUs { get; set; }
+
+ public int? LogicalCPUs { get; set; }
+
+ public string? CPUName { get; set; }
+
+ public bool? Symbolicated { get; set; }
+
+ public bool? SymbolicationNotSupported { get; set; }
+
+ public string? UpdateChannel { get; set; }
+
+ public VisualMetrics? VisualMetrics { get; set; }
+
+ public ProfilerConfiguration? Configuration { get; set; }
+
+ public List MarkerSchema { get; }
+
+ public SampleUnits? SampleUnits { get; set; }
+
+ public string? Device { get; set; }
+
+ public string? ImportedFrom { get; set; }
+
+ public bool? UsesOnlyOneStackType { get; set; }
+
+ public bool? DoesNotUseFrameImplementation { get; set; }
+
+ public bool? SourceCodeIsNotOnSearchfox { get; set; }
+
+ public List? Extra { get; set; }
+
+ public List? InitialVisibleThreads { get; set; }
+
+ public List? InitialSelectedThreads { get; set; }
+
+ public bool? KeepProfileThreadOrder { get; set; }
+
+ public double? GramsOfCO2ePerKWh { get; set; }
+ }
+
+ public class Profile
+ {
+ public Profile()
+ {
+ Meta = new ProfileMeta();
+ Libs = new List();
+ Threads = new List();
+ }
+
+ public ProfileMeta Meta { get; }
+
+ public List Libs { get; }
+
+ public List? Pages { get; set; }
+
+ public List? Counters { get; set; }
+
+ public List? ProfilerOverhead { get; set; }
+
+ public List Threads { get; }
+ }
+
+ public class ExtensionTable
+ {
+ public ExtensionTable()
+ {
+ BaseURL = new List();
+ Id = new List();
+ Name = new List();
+ }
+
+ public List BaseURL { get; }
+
+ public List Id { get; }
+
+ public List Name { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class CounterSamplesTable
+ {
+ public CounterSamplesTable()
+ {
+ Count = new List();
+ }
+
+ public List? Time { get; set; }
+
+ public List? TimeDeltas { get; set; }
+
+ public List? Number { get; set; }
+
+ public List Count { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class Counter
+ {
+ public Counter()
+ {
+ Name = string.Empty;
+ Category = string.Empty;
+ Description = string.Empty;
+ Pid = string.Empty;
+ Samples = new CounterSamplesTable();
+ }
+
+ public string Name { get; set; }
+
+ public string Category { get; set; } // 'Memory', 'power', 'Bandwidth'
+
+ public string Description { get; set; }
+
+ public ProfileColor? Color { get; set; }
+
+ public string Pid { get; set; }
+
+ public int MainThreadIndex { get; set; }
+
+ public CounterSamplesTable Samples { get; }
+ }
+
+ public class JsTracerTable
+ {
+ public JsTracerTable()
+ {
+ Events = new List();
+ Timestamps = new List();
+ Durations = new List();
+ Line = new List();
+ Column = new List();
+ }
+
+ public List Events { get; }
+
+ public List Timestamps { get; }
+
+ public List Durations { get; }
+
+ public List Line { get; }
+
+ public List Column { get; }
+
+ public int Length { get; set; }
+ }
+
+ public class SampleUnits
+ {
+ public SampleUnits()
+ {
+ Time = string.Empty;
+ EventDelay = string.Empty;
+ ThreadCPUDelta = string.Empty;
+ }
+
+ public string Time { get; set; }
+
+ public string EventDelay { get; set; }
+
+ public string ThreadCPUDelta { get; set; }
+ }
+
+ public class ExtraProfileInfoSection
+ {
+ public ExtraProfileInfoSection()
+ {
+ Label = string.Empty;
+ Entries = new List();
+ }
+
+ public string Label { get; set; }
+
+ public List Entries { get; }
+ }
+
+ public class ProfileInfoEntry
+ {
+ public ProfileInfoEntry()
+ {
+ Label = string.Empty;
+ Format = string.Empty;
+ Value = string.Empty;
+ }
+
+ public string Label { get; set; }
+
+ public string Format { get; set; }
+
+ public string Value { get; set; }
+ }
+
+ public abstract class MarkerFormatType
+ {
+ protected MarkerFormatType(string type)
+ {
+ Type = type;
+ }
+
+ [JsonIgnore]
+ public string Type { get; set; }
+
+ // ----------------------------------------------------
+ // String types.
+
+ ///
+ /// Show the URL, and handle PII sanitization
+ ///
+ public static readonly MarkerSimpleFormatType Url = new("url");
+
+ ///
+ /// Show the file path, and handle PII sanitization.
+ ///
+ public static readonly MarkerSimpleFormatType FilePath = new("file-path");
+
+ ///
+ /// Show regular string, and handle PII sanitization.
+ ///
+ public static readonly MarkerSimpleFormatType SanitizedString = new("sanitized-string");
+
+ ///
+ /// Important: do not put URL or file path information here, as it will not be
+ /// sanitized. Please be careful with including other types of PII here as well.
+ /// e.g. "Label: Some String"
+ ///
+ public static readonly MarkerSimpleFormatType String = new("string");
+
+ ///
+ /// An index into a (currently) thread-local string table, aka UniqueStringArray.
+ /// This is effectively an integer, so wherever we need to display this value, we
+ /// must first perform a lookup into the appropriate string table.
+ ///
+ public static readonly MarkerSimpleFormatType UniqueString = new("unique-string");
+
+ // ----------------------------------------------------
+ // Flow types.
+
+ ///
+ /// A flow ID is a u64 identifier that's unique across processes. In the current
+ /// implementation, we represent them as hex strings, as string table indexes.
+ ///
+ public static readonly MarkerSimpleFormatType FlowId = new("flow-id");
+
+ ///
+ /// A terminating flow ID is a flow ID that, when used in a marker with timestamp T,
+ /// makes it so that if the same flow ID is used in a marker whose timestamp is
+ /// after T, that flow ID is considered to refer to a different flow.
+ ///
+ public static readonly MarkerSimpleFormatType TerminatingFlowId = new("terminating-flow-id");
+
+ // ----------------------------------------------------
+ // Numeric types
+
+ // Note: All time and durations are stored as milliseconds.
+
+ ///
+ /// For time data that represents a duration of time.
+ /// e.g. "Label: 5s, 5ms, 5μs"
+ ///
+ public static readonly MarkerSimpleFormatType Duration = new("duration");
+
+ ///
+ /// Data that happened at a specific time, relative to the start of
+ /// the profile. e.g. "Label: 15.5s, 20.5ms, 30.5μs"
+ ///
+ public static readonly MarkerSimpleFormatType Time = new("time");
+
+ // The following are alternatives to display a time only in a specific unit of time.
+
+ /// "Label: 5s"
+ public static readonly MarkerSimpleFormatType Seconds = new("seconds");
+
+ /// "Label: 5ms"
+ public static readonly MarkerSimpleFormatType Milliseconds = new("milliseconds");
+
+ /// "Label: 5μs"
+ public static readonly MarkerSimpleFormatType Microseconds = new("microseconds");
+
+ /// "Label: 5ns"
+ public static readonly MarkerSimpleFormatType Nanoseconds = new("nanoseconds");
+
+ ///
+ /// e.g. "Label: 5.55mb, 5 bytes, 312.5kb"
+ ///
+ public static readonly MarkerSimpleFormatType Bytes = new("bytes");
+
+ ///
+ /// This should be a value between 0 and 1.
+ /// "Label: 50%"
+ ///
+ public static readonly MarkerSimpleFormatType Percentage = new("percentage");
+
+ ///
+ /// The integer should be used for generic representations of numbers. Do not
+ /// use it for time information.
+ /// "Label: 52, 5,323, 1,234,567"
+ ///
+ public static readonly MarkerSimpleFormatType Integer = new("integer");
+
+ ///
+ /// The decimal should be used for generic representations of numbers. Do not
+ /// use it for time information.
+ /// "Label: 52.23, 0.0054, 123,456.78"
+ ///
+ public static readonly MarkerSimpleFormatType Decimal = new("decimal");
+
+ public static readonly MarkerSimpleFormatType Pid = new("pid");
+ public static readonly MarkerSimpleFormatType Tid = new("tid");
+ public static readonly MarkerSimpleFormatType List = new("list");
+
+ ///
+ /// Represents a table format, with columns of type `TableColumnFormat[]`.
+ ///
+ public static MarkerTableFormatType Table(IEnumerable columns)
+ {
+ var table = new MarkerTableFormatType();
+ table.Columns.AddRange(columns);
+ return table;
+ }
+ }
+
+ public class MarkerSimpleFormatType(string type) : MarkerFormatType(type);
+
+ private sealed class MarkerFormatTypeConverter : JsonConverter
+ {
+ public override MarkerFormatType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ string propertyName = reader.GetString() ?? string.Empty;
+
+ MarkerFormatType? markerFormatType = propertyName switch
+ {
+ "url" => MarkerFormatType.Url,
+ "file-path" => MarkerFormatType.FilePath,
+ "sanitized-string" => MarkerFormatType.SanitizedString,
+ "string" => MarkerFormatType.String,
+ "unique-string" => MarkerFormatType.UniqueString,
+ "flow-id" => MarkerFormatType.FlowId,
+ "terminating-flow-id" => MarkerFormatType.TerminatingFlowId,
+ "duration" => MarkerFormatType.Duration,
+ "time" => MarkerFormatType.Time,
+ "seconds" => MarkerFormatType.Seconds,
+ "milliseconds" => MarkerFormatType.Milliseconds,
+ "microseconds" => MarkerFormatType.Microseconds,
+ "nanoseconds" => MarkerFormatType.Nanoseconds,
+ "bytes" => MarkerFormatType.Bytes,
+ "percentage" => MarkerFormatType.Percentage,
+ "integer" => MarkerFormatType.Integer,
+ "decimal" => MarkerFormatType.Decimal,
+ "pid" => MarkerFormatType.Pid,
+ "tid" => MarkerFormatType.Tid,
+ "list" => MarkerFormatType.List,
+ _ => new MarkerSimpleFormatType(propertyName)
+ };
+
+ return markerFormatType;
+ }
+
+ return JsonSerializer.Deserialize(ref reader, JsonProfilerContext.Default.MarkerTableFormatType)!;
+ }
+
+ public override void Write(Utf8JsonWriter writer, MarkerFormatType value, JsonSerializerOptions options)
+ {
+ if (value is MarkerSimpleFormatType simpleFormatType)
+ {
+ writer.WriteStringValue(simpleFormatType.Type);
+ }
+ else
+ {
+ JsonSerializer.Serialize(writer, (object?)value, (JsonTypeInfo)JsonProfilerContext.Default.MarkerTableFormatType);
+ }
+ }
+ }
+
+
+ public class MarkerTableFormatType : MarkerFormatType
+ {
+ public MarkerTableFormatType() : base("table")
+ {
+ Columns = new();
+ }
+
+ public List Columns { get; }
+ }
+
+ public class TableColumnFormat
+ {
+ public TableColumnFormat()
+ {
+ Label = string.Empty;
+ }
+
+ [JsonConverter(typeof(MarkerFormatTypeConverter))]
+ public MarkerFormatType? Type { get; set; }
+
+ public string Label { get; set; }
+ }
+
+ [JsonConverter(typeof(KebabCaseEnumConverter))]
+ public enum MarkerDisplayLocation
+ {
+ MarkerChart,
+ MarkerTable,
+ TimelineOverview,
+ TimelineMemory,
+ TimelineIpc,
+ TimelineFileio,
+ StackChart
+ }
+
+ private class KebabCaseEnumConverter() :
+ JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower) where TEnum : struct, Enum;
+
+ [JsonConverter(typeof(KebabCaseEnumConverter))]
+ public enum MarkerGraphType
+ {
+ Bar,
+ Line,
+ LineFilled
+ }
+
+ public class MarkerGraph
+ {
+ public MarkerGraph()
+ {
+ Key = string.Empty;
+ }
+
+ public string Key { get; set; }
+
+ public MarkerGraphType Type { get; set; }
+
+ public ProfileColor? Color { get; set; }
+ }
+
+ [JsonConverter(typeof(ProfileColorEnumConverter))]
+ public enum ProfileColor
+ {
+ Blue,
+ Green,
+ Grey,
+ Ink,
+ Magenta,
+ Orange,
+ Purple,
+ Red,
+ Teal,
+ Yellow,
+ }
+
+ private sealed class ProfileColorEnumConverter() : JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false);
+
+
+ public class MarkerSchema
+ {
+ public MarkerSchema()
+ {
+ Name = string.Empty;
+ Display = new List();
+ Data = new List();
+ }
+
+ public string Name { get; set; }
+
+ public string? TooltipLabel { get; set; }
+
+ public string? TableLabel { get; set; }
+
+ public string? ChartLabel { get; set; }
+
+ public List Display { get; }
+
+ public List Data { get; }
+
+ public List? Graphs { get; set; }
+
+ public bool? IsStackBased { get; set; }
+ }
+
+ public class MarkerDataItem
+ {
+ public string? Key { get; set; }
+
+ public string? Label { get; set; }
+
+ [JsonConverter(typeof(MarkerFormatTypeConverter))]
+ public MarkerFormatType? Format { get; set; }
+
+ public bool? Searchable { get; set; }
+
+ public string? Value { get; set; }
+ }
+
+ [JsonConverter(typeof(MarkerPayloadConverter))]
+ public class MarkerPayload
+ {
+ public string? Type { get; set; }
+
+ public Dictionary? ExtensionData { get; set; }
+
+ protected internal virtual void WriteJson(Utf8JsonWriter writer, MarkerPayload payload, JsonSerializerOptions options)
+ {
+ }
+ }
+
+ private class MarkerPayloadConverter : JsonConverter
+ {
+ public override MarkerPayload? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException("Expected object start { for MarkerPayload");
+ }
+
+ var payload = new MarkerPayload();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ return payload;
+ }
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ throw new JsonException("Expected property name for MarkerPayload");
+ }
+ string propertyName = reader.GetString() ?? string.Empty;
+
+ if (propertyName == "type")
+ {
+ reader.Read();
+ payload.Type = reader.GetString();
+ }
+ else
+ {
+ if (payload.ExtensionData == null)
+ {
+ payload.ExtensionData = new Dictionary();
+ }
+
+ reader.Read();
+
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.String:
+ payload.ExtensionData[propertyName] = reader.GetString();
+ break;
+ case JsonTokenType.Number:
+ payload.ExtensionData[propertyName] = reader.GetDouble();
+ break;
+ case JsonTokenType.True:
+ payload.ExtensionData[propertyName] = true;
+ break;
+ case JsonTokenType.False:
+ payload.ExtensionData[propertyName] = false;
+ break;
+ case JsonTokenType.Null:
+ payload.ExtensionData[propertyName] = null;
+ break;
+ default:
+ throw new JsonException($"Unexpected token type {reader.TokenType} for MarkerPayload");
+
+ }
+ }
+ }
+
+ return payload;
+ }
+
+ public override void Write(Utf8JsonWriter writer, MarkerPayload payload, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+
+ if (payload.Type != null)
+ {
+ writer.WriteString("type", payload.Type);
+ }
+
+ if (payload.ExtensionData != null)
+ {
+ foreach (var (key, value) in payload.ExtensionData)
+ {
+ writer.WritePropertyName(key);
+ switch (value)
+ {
+ case string stringValue:
+ writer.WriteStringValue(stringValue);
+ break;
+ case long longValue:
+ writer.WriteNumberValue(longValue);
+ break;
+ case int intValue:
+ writer.WriteNumberValue(intValue);
+ break;
+ case short intValue:
+ writer.WriteNumberValue(intValue);
+ break;
+ case sbyte intValue:
+ writer.WriteNumberValue(intValue);
+ break;
+ case ulong longValue:
+ writer.WriteNumberValue(longValue);
+ break;
+ case uint intValue:
+ writer.WriteNumberValue(intValue);
+ break;
+ case ushort intValue:
+ writer.WriteNumberValue(intValue);
+ break;
+ case byte intValue:
+ writer.WriteNumberValue(intValue);
+ break;
+ case float floatValue:
+ writer.WriteNumberValue(floatValue);
+ break;
+ case double doubleValue:
+ writer.WriteNumberValue(doubleValue);
+ break;
+ case bool boolValue:
+ writer.WriteBooleanValue(boolValue);
+ break;
+ case null:
+ writer.WriteNullValue();
+ break;
+ default:
+ throw new JsonException($"Unexpected value type {value.GetType()} for MarkerPayload");
+ }
+ }
+ }
+
+ payload.WriteJson(writer, payload, options);
+
+ writer.WriteEndObject();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/Markers/GCAllocationTickEvent.cs b/src/Ultra.Core/Markers/GCAllocationTickEvent.cs
new file mode 100644
index 0000000..6426b48
--- /dev/null
+++ b/src/Ultra.Core/Markers/GCAllocationTickEvent.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Text.Json;
+
+namespace Ultra.Core.Markers;
+
+public class GCAllocationTickEvent : FirefoxProfiler.MarkerPayload
+{
+ public const string TypeId = "GCMinor"; // dotnet.gc.allocation_tick
+
+ public GCAllocationTickEvent()
+ {
+ Type = TypeId;
+ }
+
+ public long AllocationAmount { get; set; }
+
+ public string AllocationKind { get; set; }
+
+ public string TypeName { get; set; }
+
+ public int HeapIndex { get; set; }
+
+ protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options)
+ {
+ writer.WriteNumber("allocationAmount", AllocationAmount);
+ writer.WriteString("allocationKind", AllocationKind);
+ writer.WriteString("typeName", TypeName);
+ writer.WriteNumber("heapIndex", HeapIndex);
+ }
+
+ public static FirefoxProfiler.MarkerSchema Schema()
+ => new()
+ {
+ Name = TypeId,
+ ChartLabel = "GC Allocation: {marker.data.typeName}, Amount: {marker.data.allocationAmount}",
+ TableLabel = "GC Allocation: {marker.data.typeName}, Amount: {marker.data.allocationAmount}",
+ Display =
+ {
+ FirefoxProfiler.MarkerDisplayLocation.TimelineOverview,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerChart,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerTable
+ },
+ Graphs = [
+ new()
+ {
+ Key = "allocationAmount",
+ Color = FirefoxProfiler.ProfileColor.Red,
+ Type = FirefoxProfiler.MarkerGraphType.Bar,
+ }
+ ],
+ Data =
+ {
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.Bytes,
+ Key = "allocationAmount",
+ Label = "Allocation Amount",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.String,
+ Key = "allocationKind",
+ Label = "Allocation Kind",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.String,
+ Key = "typeName",
+ Label = "Type Name",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.Integer,
+ Key = "heapIndex",
+ Label = "Heap Index",
+ },
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/Markers/GCEvent.cs b/src/Ultra.Core/Markers/GCEvent.cs
new file mode 100644
index 0000000..639b458
--- /dev/null
+++ b/src/Ultra.Core/Markers/GCEvent.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Text.Json;
+
+namespace Ultra.Core.Markers;
+
+public class GCEvent : FirefoxProfiler.MarkerPayload
+{
+ public const string TypeId = "GCMajor"; // Use a predefined type to have a different marker style in the timeline
+
+ public GCEvent()
+ {
+ Type = TypeId;
+ }
+
+ public string Reason { get; set; }
+
+ public int Count { get; set; }
+
+ public int Depth { get; set; }
+
+ public string GCType { get; set; }
+
+ protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options)
+ {
+ writer.WriteString("reason", Reason);
+ writer.WriteNumber("count", Count);
+ writer.WriteNumber("depth", Depth);
+ writer.WriteString("gcType", GCType);
+ }
+
+ public static FirefoxProfiler.MarkerSchema Schema()
+ => new()
+ {
+ Name = TypeId,
+ ChartLabel = "GC: {marker.data.reason}, Type: {marker.data.gcType}, Count: {marker.data.count}",
+ TableLabel = "GC: {marker.data.reason}, Type: {marker.data.gcType}, Count: {marker.data.count}",
+ Display =
+ {
+ FirefoxProfiler.MarkerDisplayLocation.TimelineMemory,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerChart,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerTable
+ },
+ Data =
+ {
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.String,
+ Key = "reason",
+ Label = "Reason",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.Integer,
+ Key = "count",
+ Label = "Count",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.Integer,
+ Key = "depth",
+ Label = "Depth",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.String,
+ Key = "gcType",
+ Label = "GC Type",
+ },
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/Markers/GCHeapStatsEvent.cs b/src/Ultra.Core/Markers/GCHeapStatsEvent.cs
new file mode 100644
index 0000000..9b7b0d3
--- /dev/null
+++ b/src/Ultra.Core/Markers/GCHeapStatsEvent.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Text.Json;
+using static Ultra.Core.FirefoxProfiler;
+
+namespace Ultra.Core.Markers;
+
+public class GCHeapStatsEvent : FirefoxProfiler.MarkerPayload
+{
+ public const string TypeId = "dotnet.gc.heap_stats";
+
+ public GCHeapStatsEvent()
+ {
+ Type = TypeId;
+ }
+
+ public long TotalHeapSize { get; set; }
+
+ public long TotalPromoted { get; set; }
+
+ public long GenerationSize0 { get; set; }
+
+ public long TotalPromotedSize0 { get; set; }
+
+ public long GenerationSize1 { get; set; }
+
+ public long TotalPromotedSize1 { get; set; }
+
+ public long GenerationSize2 { get; set; }
+
+ public long TotalPromotedSize2 { get; set; }
+
+ public long GenerationSize3 { get; set; }
+
+ public long TotalPromotedSize3 { get; set; }
+
+ public long GenerationSize4 { get; set; }
+
+ public long TotalPromotedSize4 { get; set; }
+
+ public long FinalizationPromotedSize { get; set; }
+
+ public long FinalizationPromotedCount { get; set; }
+
+ public int PinnedObjectCount { get; set; }
+
+ public int SinkBlockCount { get; set; }
+
+ public int GCHandleCount { get; set; }
+
+ protected internal override void WriteJson(Utf8JsonWriter writer, MarkerPayload payload, JsonSerializerOptions options)
+ {
+ writer.WriteNumber("totalHeapSize", TotalHeapSize);
+ writer.WriteNumber("totalPromoted", TotalPromoted);
+ writer.WriteNumber("generationSize0", GenerationSize0);
+ writer.WriteNumber("totalPromotedSize0", TotalPromotedSize0);
+ writer.WriteNumber("generationSize1", GenerationSize1);
+ writer.WriteNumber("totalPromotedSize1", TotalPromotedSize1);
+ writer.WriteNumber("generationSize2", GenerationSize2);
+ writer.WriteNumber("totalPromotedSize2", TotalPromotedSize2);
+ writer.WriteNumber("generationSize3", GenerationSize3);
+ writer.WriteNumber("totalPromotedSize3", TotalPromotedSize3);
+ writer.WriteNumber("generationSize4", GenerationSize4);
+ writer.WriteNumber("totalPromotedSize4", TotalPromotedSize4);
+ writer.WriteNumber("finalizationPromotedSize", FinalizationPromotedSize);
+ writer.WriteNumber("finalizationPromotedCount", FinalizationPromotedCount);
+ writer.WriteNumber("pinnedObjectCount", PinnedObjectCount);
+ writer.WriteNumber("sinkBlockCount", SinkBlockCount);
+ writer.WriteNumber("gcHandleCount", GCHandleCount);
+ }
+
+ public static MarkerSchema Schema()
+ => new()
+ {
+ Name = TypeId,
+ ChartLabel = "GC Heap Stats: {marker.data.totalHeapSize}, Promoted: {marker.data.totalPromoted}",
+ TableLabel = "GC Heap Stats: {marker.data.totalHeapSize}, Promoted: {marker.data.totalPromoted}",
+ Display =
+ {
+ MarkerDisplayLocation.MarkerChart,
+ MarkerDisplayLocation.MarkerTable,
+ MarkerDisplayLocation.TimelineMemory,
+ },
+ Data =
+ {
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalHeapSize",
+ Label = "Total Heap Size",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalPromoted",
+ Label = "Total Promoted",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "generationSize0",
+ Label = "Generation Size 0",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalPromotedSize0",
+ Label = "Total Promoted Size 0",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "generationSize1",
+ Label = "Generation Size 1",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalPromotedSize1",
+ Label = "Total Promoted Size 1",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "generationSize2",
+ Label = "Generation Size 2",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalPromotedSize2",
+ Label = "Total Promoted Size 2",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "generationSize3",
+ Label = "Generation Size 3",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalPromotedSize3",
+ Label = "Total Promoted Size 3",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "generationSize4",
+ Label = "Generation Size 4",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "totalPromotedSize4",
+ Label = "Total Promoted Size 4",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Bytes,
+ Key = "finalizationPromotedSize",
+ Label = "Finalization Promoted Size",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Integer,
+ Key = "finalizationPromotedCount",
+ Label = "Finalization Promoted Count",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Integer,
+ Key = "pinnedObjectCount",
+ Label = "Pinned Object Count",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Integer,
+ Key = "sinkBlockCount",
+ Label = "Sink Block Count",
+ },
+ new MarkerDataItem()
+ {
+ Format = MarkerFormatType.Integer,
+ Key = "gcHandleCount",
+ Label = "GCHandle Count",
+ },
+ }
+ };
+}
diff --git a/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs b/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs
new file mode 100644
index 0000000..b94cbae
--- /dev/null
+++ b/src/Ultra.Core/Markers/GCRestartExecutionEngineEvent.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+namespace Ultra.Core.Markers;
+
+public class GCRestartExecutionEngineEvent : FirefoxProfiler.MarkerPayload
+{
+ public const string TypeId = "dotnet.gc.restart_execution_engine";
+
+ public GCRestartExecutionEngineEvent()
+ {
+ Type = TypeId;
+ }
+
+ public static FirefoxProfiler.MarkerSchema Schema()
+ => new()
+ {
+ Name = TypeId,
+ ChartLabel = "GC Restart Execution Engine",
+ TableLabel = "GC Restart Execution Engine",
+ Display =
+ {
+ FirefoxProfiler.MarkerDisplayLocation.TimelineOverview,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerChart,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerTable
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs b/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs
new file mode 100644
index 0000000..d29b300
--- /dev/null
+++ b/src/Ultra.Core/Markers/GCSuspendExecutionEngineEvent.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Text.Json;
+
+namespace Ultra.Core.Markers;
+
+public class GCSuspendExecutionEngineEvent : FirefoxProfiler.MarkerPayload
+{
+ public const string TypeId = "dotnet.gc.suspend_execution_engine";
+
+ public GCSuspendExecutionEngineEvent()
+ {
+ Type = TypeId;
+ }
+
+ public string Reason { get; set; }
+
+ public int Count { get; set; }
+
+ protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options)
+ {
+ writer.WriteString("reason", Reason);
+ writer.WriteNumber("count", Count);
+ }
+
+ public static FirefoxProfiler.MarkerSchema Schema()
+ => new()
+ {
+ Name = TypeId,
+ ChartLabel = "GC Suspend Execution Engine: {marker.data.reason}, Count: {marker.data.count}",
+ TableLabel = "GC Suspend Execution Engine: {marker.data.reason}, Count: {marker.data.count}",
+ Display =
+ {
+ FirefoxProfiler.MarkerDisplayLocation.TimelineOverview,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerChart,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerTable
+ },
+ Data =
+ {
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.String,
+ Key = "reason",
+ Label = "Reason",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.Integer,
+ Key = "count",
+ Label = "Count",
+ },
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/Markers/JitCompileEvent.cs b/src/Ultra.Core/Markers/JitCompileEvent.cs
new file mode 100644
index 0000000..c5278df
--- /dev/null
+++ b/src/Ultra.Core/Markers/JitCompileEvent.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Text.Json;
+
+namespace Ultra.Core.Markers;
+
+public class JitCompileEvent : FirefoxProfiler.MarkerPayload
+{
+ // Use CC instead of dotnet.jit.compile because Firefox Profiler is hardcoding styles based on names :( See https://github.com/firefox-devtools/profiler/blob/main/src/profile-logic/marker-styles.js
+ public const string TypeId = "CC";
+
+ public JitCompileEvent()
+ {
+ Type = TypeId;
+ FullName = string.Empty;
+ }
+
+ public string FullName { get; set; }
+
+ public int MethodILSize { get; set; }
+
+ protected internal override void WriteJson(Utf8JsonWriter writer, FirefoxProfiler.MarkerPayload payload, JsonSerializerOptions options)
+ {
+ writer.WriteString("fullName", FullName);
+ writer.WriteNumber("methodILSize", MethodILSize);
+ }
+
+ public static FirefoxProfiler.MarkerSchema Schema()
+ => new()
+ {
+ Name = TypeId,
+
+ ChartLabel = "JIT Compile: {marker.data.fullName}, ILSize: {marker.data.methodILSize}",
+ TableLabel = "JIT Compile: {marker.data.fullName}, ILSize: {marker.data.methodILSize}",
+
+ Display =
+ {
+ FirefoxProfiler.MarkerDisplayLocation.TimelineOverview,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerChart,
+ FirefoxProfiler.MarkerDisplayLocation.MarkerTable
+ },
+
+ Data =
+ {
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.String,
+ Key = "fullName",
+ Label = "Full Name",
+ },
+ new FirefoxProfiler.MarkerDataItem()
+ {
+ Format = FirefoxProfiler.MarkerFormatType.Integer,
+ Key = "methodILSize",
+ Label = "Method IL Size",
+ },
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/Ultra.Core/Ultra.Core.csproj b/src/Ultra.Core/Ultra.Core.csproj
new file mode 100644
index 0000000..005e41f
--- /dev/null
+++ b/src/Ultra.Core/Ultra.Core.csproj
@@ -0,0 +1,41 @@
+
+
+ Library
+ net8.0
+ enable
+ true
+
+
+ true
+
+
+
+ This is a default project description
+ Alexandre Mutel
+ en-US
+ Alexandre Mutel
+ tag1;tag2;tag3
+ readme.md
+ ultra.png
+ https://github.com/xoofx/ultra
+ BSD-2-Clause
+
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/src/Ultra.Tests/FirefoxProfilerTests.cs b/src/Ultra.Tests/FirefoxProfilerTests.cs
new file mode 100644
index 0000000..ea94667
--- /dev/null
+++ b/src/Ultra.Tests/FirefoxProfilerTests.cs
@@ -0,0 +1,405 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// Licensed under the BSD-Clause 2 license.
+// See license.txt file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using Ultra.Core;
+using Ultra.Core.Markers;
+using static Ultra.Core.FirefoxProfiler;
+
+namespace Ultra.Tests;
+
+[TestClass]
+public class Class1Test
+{
+ [TestMethod]
+ public void TestMarker()
+ {
+ var markerText = """
+ { "type": "hello", "value": 1 }
+ """;
+ var marker = JsonSerializer.Deserialize(markerText, JsonProfilerContext.Default.MarkerPayload);
+ }
+
+ [TestMethod]
+ public void TestSimple()
+ {
+ var profile = new Profile();
+
+ profile.Meta.StartTime = 0;
+ profile.Meta.EndTime = 2000;
+ profile.Meta.Version = 29;
+ profile.Meta.PreprocessedProfileVersion = 50;
+
+ profile.Meta.Product = "myapp.exe";
+ profile.Meta.InitialSelectedThreads = new();
+ profile.Meta.InitialSelectedThreads.Add(0);
+
+ profile.Meta.Platform = "Windows";
+ profile.Meta.Oscpu = RuntimeInformation.ProcessArchitecture.ToString();
+ profile.Meta.LogicalCPUs = Environment.ProcessorCount;
+ // We don't have access to physical CPUs
+ //profile.Meta.PhysicalCPUs = Environment.ProcessorCount / 2;
+ //profile.Meta.CPUName = ""; // TBD
+
+
+ profile.Meta.InitialVisibleThreads = new();
+ profile.Meta.InitialVisibleThreads.Add(0);
+
+ profile.Meta.Stackwalk = 1;
+
+
+ profile.Meta.Interval = 1.0;
+ profile.Meta.Categories =
+ [
+ new Category()
+ {
+ Name = "Kernel",
+ Color = ProfileColor.Orange,
+ },
+ new Category()
+ {
+ Name = "Native User",
+ Color = ProfileColor.Blue,
+ },
+ new Category()
+ {
+ Name = ".NET",
+ Color = ProfileColor.Green,
+ },
+ new Category()
+ {
+ Name = "GC",
+ Color = ProfileColor.Yellow,
+ },
+ new Category()
+ {
+ Name = "JIT",
+ Color = ProfileColor.Purple,
+ },
+ ];
+
+ var thread = new FirefoxProfiler.Thread();
+ thread.Name = "Main";
+ thread.Tid = "125";
+ thread.Pid = "My Process";
+
+ // Frame table
+ var frameTable = thread.FrameTable;
+ frameTable.Category.Add(0);
+ frameTable.Address.Add(-1);
+ frameTable.Line.Add(null);
+ frameTable.Column.Add(null);
+ frameTable.InlineDepth.Add(0);
+ frameTable.Subcategory.Add(null);
+ frameTable.Func.Add(0);
+ frameTable.NativeSymbol.Add(null);
+ frameTable.Implementation.Add(null);
+ frameTable.InnerWindowID.Add(null);
+ frameTable.Length = 1;
+
+ // Function table
+ var funcTable = thread.FuncTable;
+ funcTable.Name.Add(0); // myfunction
+ funcTable.IsJS.Add(false);
+ funcTable.RelevantForJS.Add(false);
+ funcTable.Resource.Add(0);
+ funcTable.LineNumber.Add(null);
+ funcTable.ColumnNumber.Add(null);
+ funcTable.Length = 1;
+
+ // Stack and prefix
+ var stackTable = thread.StackTable;
+ stackTable.Frame.Add(0);
+ stackTable.Prefix.Add(null);
+ stackTable.Category.Add(0);
+ stackTable.Subcategory.Add(0);
+ stackTable.Length = 1;
+
+ // Samples
+ var samples = thread.Samples;
+
+ samples.TimeDeltas = new();
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ samples.WeightType = "samples";
+ samples.Length = 4;
+ //samples.Responsiveness.Add(0);
+
+ var strings = thread.StringArray;
+ strings.Add("myfunction");
+
+ // Resource table
+ thread.ResourceTable.Name.Add(0);
+ thread.ResourceTable.Lib.Add(0);
+ thread.ResourceTable.Host.Add(null);
+ //unknown: 0,
+ //library: 1,
+ //addon: 2,
+ //webhost: 3,
+ //otherhost: 4,
+ //url: 5,
+ thread.ResourceTable.Type.Add(1);
+ thread.ResourceTable.Length = 1;
+ thread.ProcessType = "default";
+
+
+ //thread.JsAllocations = null;
+
+ profile.Threads.Add(thread);
+
+ var lib = new Lib();
+ lib.Name = "mylib";
+ lib.AddressStart = 0x10000000;
+ lib.AddressEnd = 0x20000000;
+ lib.AddressOffset = 0x0;
+ lib.Path = "/path/to/mylib";
+ lib.DebugName = "mylib.pdb";
+ lib.DebugPath = "/path/to/mylib.pdb";
+ lib.BreakpadId = "1234567890";
+ profile.Libs.Add(lib);
+
+ var result = JsonSerializer.Serialize(profile, JsonProfilerContext.Default.Profile);
+
+ Console.WriteLine(result);
+ }
+
+ [TestMethod]
+ public void TestSimpleWithAddresses()
+ {
+ var profile = new Profile();
+
+ profile.Meta.StartTime = 0;
+ profile.Meta.EndTime = 2000;
+ profile.Meta.Version = 29;
+ profile.Meta.PreprocessedProfileVersion = 50;
+ profile.Meta.Symbolicated = true;
+
+ profile.Meta.SampleUnits = new SampleUnits();
+ profile.Meta.SampleUnits.Time = "ms";
+ profile.Meta.SampleUnits.EventDelay = "ms";
+ profile.Meta.SampleUnits.ThreadCPUDelta = "ns";
+
+ profile.Meta.Platform = Environment.OSVersion.ToString(); // "Windows";
+ profile.Meta.Oscpu = RuntimeInformation.ProcessArchitecture.ToString();
+ profile.Meta.LogicalCPUs = Environment.ProcessorCount;
+
+ // We don't have access to physical CPUs
+ //profile.Meta.PhysicalCPUs = Environment.ProcessorCount / 2;
+ //profile.Meta.CPUName = ""; // TBD
+
+ profile.Meta.Product = "myapp.exe";
+ profile.Meta.InitialSelectedThreads = new();
+ profile.Meta.InitialSelectedThreads.Add(0);
+
+ profile.Meta.InitialVisibleThreads = new();
+ profile.Meta.InitialVisibleThreads.Add(0);
+
+ profile.Meta.Stackwalk = 1;
+
+
+ profile.Meta.Interval = 1.0;
+ profile.Meta.Categories =
+ [
+ new Category()
+ {
+ Name = "Kernel",
+ Color = ProfileColor.Orange,
+ },
+ new Category()
+ {
+ Name = "Native User",
+ Color = ProfileColor.Blue,
+ },
+ new Category()
+ {
+ Name = ".NET",
+ Color = ProfileColor.Green,
+ },
+ new Category()
+ {
+ Name = "GC",
+ Color = ProfileColor.Yellow,
+ },
+ new Category()
+ {
+ Name = "JIT",
+ Color = ProfileColor.Purple,
+ },
+ ];
+
+ profile.Meta.MarkerSchema.Add(JitCompileEvent.Schema());
+
+ //profile.Meta.MarkerSchema.Add(new FirefoxProfiler.MarkerSchema()
+ //{
+ // Name = FirefoxProfiler.JitCompile.TypeId,
+
+ // ChartLabel = "memory size (chart): {marker.data.memorySize} bytes - Hello",
+ // TableLabel = "memory size (table): {marker.data.memorySize} bytes",
+
+ // Display =
+ // {
+ // FirefoxProfiler.MarkerDisplayLocation.TimelineOverview,
+ // FirefoxProfiler.MarkerDisplayLocation.TimelineMemory,
+ // FirefoxProfiler.MarkerDisplayLocation.StackChart,
+ // FirefoxProfiler.MarkerDisplayLocation.MarkerChart,
+ // FirefoxProfiler.MarkerDisplayLocation.MarkerTable
+ // },
+
+ // Data =
+ // {
+ // new FirefoxProfiler.MarkerDataItem()
+ // {
+ // Format = FirefoxProfiler.MarkerFormatType.Integer,
+ // Key = "memorySize",
+ // Label = "Memory Size",
+ // }
+ // },
+
+ // Graphs = new()
+ // {
+ // new FirefoxProfiler.MarkerGraph()
+ // {
+ // Key = "memorySize",
+ // Type = FirefoxProfiler.MarkerGraphType.LineFilled,
+ // Color = FirefoxProfiler.ProfileColor.Blue,
+ // }
+ // }
+ //});
+
+ var thread = new FirefoxProfiler.Thread();
+ thread.Name = "Main";
+ thread.Tid = "125";
+ thread.Pid = "My Process";
+
+ // Samples
+ var samples = thread.Samples;
+
+ samples.TimeDeltas = new();
+ samples.TimeDeltas.Add(0);
+ samples.Stack.Add(0);
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ samples.TimeDeltas.Add(10);
+ samples.Stack.Add(0);
+ // Needs to be set if profile.Meta.SampleUnits.ThreadCPUDelta is set
+ samples.ThreadCPUDelta = new List();
+ samples.ThreadCPUDelta.Add(10_000_000);
+ samples.ThreadCPUDelta.Add(10_000_000);
+ samples.ThreadCPUDelta.Add(10_000_000);
+ samples.ThreadCPUDelta.Add(10_000_000);
+
+ samples.WeightType = "samples";
+ samples.Length = 4;
+ //samples.Responsiveness.Add(0);
+
+ // Stack and prefix
+ var stackTable = thread.StackTable;
+ stackTable.Frame.Add(0);
+ stackTable.Prefix.Add(null);
+ stackTable.Category.Add(0);
+ stackTable.Subcategory.Add(0);
+ stackTable.Length = 1;
+
+ // Frame table
+ var frameTable = thread.FrameTable;
+ frameTable.Category.Add(0);
+ frameTable.Address.Add(0x0);
+ frameTable.Line.Add(null);
+ frameTable.Column.Add(null);
+ frameTable.InlineDepth.Add(0);
+ frameTable.Subcategory.Add(null);
+ frameTable.Func.Add(0);
+ frameTable.NativeSymbol.Add(0);
+ frameTable.Implementation.Add(null);
+ frameTable.InnerWindowID.Add(null);
+ frameTable.Length = 1;
+
+ // Function table
+ var funcTable = thread.FuncTable;
+ funcTable.Name.Add(0); // myfunction
+ funcTable.IsJS.Add(false);
+ funcTable.RelevantForJS.Add(false);
+ funcTable.Resource.Add(0);
+ funcTable.LineNumber.Add(null);
+ funcTable.ColumnNumber.Add(null);
+ funcTable.Length = 1;
+
+ // NativeSymbols
+ var nativeSymbols = thread.NativeSymbols;
+ nativeSymbols.Name.Add(1);
+ nativeSymbols.Address.Add(0x16);
+ nativeSymbols.LibIndex.Add(0);
+ nativeSymbols.FunctionSize.Add(null);
+ nativeSymbols.Length = 1;
+
+ var strings = thread.StringArray;
+ strings.Add("myfunction");
+ strings.Add("myfunction (native symbols)");
+ strings.Add("myfunction (resource)");
+ strings.Add("Memory Size");
+
+ // Markers
+ var markers = thread.Markers;
+
+ for (int i = 0; i < 20; i++)
+ {
+ markers.StartTime.Add(i * 2);
+ markers.EndTime.Add(i * 2 + 1);
+ markers.Category.Add(3);
+ markers.Phase.Add(MarkerPhase.Instance);
+ markers.ThreadId.Add(0);
+ markers.Name.Add(3);
+ markers.Data.Add(new JitCompileEvent()
+ {
+ FullName = "World",
+ MethodILSize = 100,
+ });
+ }
+ markers.Length = markers.StartTime.Count;
+
+ // Resource table
+ thread.ResourceTable.Name.Add(2);
+ thread.ResourceTable.Lib.Add(0);
+ thread.ResourceTable.Host.Add(null);
+ // native functions -> library
+ //unknown: 0,
+ //library: 1,
+ //addon: 2,
+ //webhost: 3,
+ //otherhost: 4,
+ //url: 5,
+ thread.ResourceTable.Type.Add(1);
+ thread.ResourceTable.Length = 1;
+ thread.ProcessType = "default";
+
+ //thread.JsAllocations = null;
+
+ profile.Threads.Add(thread);
+
+ var lib = new Lib();
+ lib.Arch = "x86_64";
+ lib.Name = "mylib";
+ lib.AddressStart = 0x10000000;
+ lib.AddressEnd = 0x20000000;
+ lib.AddressOffset = 0x0;
+ lib.Path = "/path/to/mylib";
+ lib.DebugName = "mylib.pdb";
+ lib.DebugPath = "/path/to/mylib.pdb";
+ lib.BreakpadId = "1234567890";
+ profile.Libs.Add(lib);
+
+ var result = JsonSerializer.Serialize(profile, JsonProfilerContext.Default.Options);
+
+ Console.WriteLine(result);
+ }
+}
diff --git a/src/Ultra.Tests/Ultra.Tests.csproj b/src/Ultra.Tests/Ultra.Tests.csproj
new file mode 100644
index 0000000..58a0213
--- /dev/null
+++ b/src/Ultra.Tests/Ultra.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ Exe
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ultra/Program.cs b/src/Ultra/Program.cs
new file mode 100644
index 0000000..f7b1804
--- /dev/null
+++ b/src/Ultra/Program.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Diagnostics.Tracing;
+using System.Text;
+using ByteSizeLib;
+using Microsoft.Extensions.Options;
+using Spectre.Console;
+using Ultra.Core;
+using XenoAtom.CommandLine;
+
+namespace Ultra;
+
+internal class Program
+{
+ static async Task Main(string[] args)
+ {
+ Console.InputEncoding = Encoding.UTF8;
+ Console.OutputEncoding = Encoding.UTF8;
+
+ List pidList = new();
+
+ bool verbose = false;
+ var options = new EtwUltraProfilerOptions();
+
+ const string _ = "";
+
+ var commandApp = new CommandApp("ultra", "Profile an application")
+ {
+ new CommandUsage("Usage: {NAME} [Options] command "),
+ _,
+ new HelpOption(),
+ new VersionOption(),
+ { "verbose", "Display verbose progress", v => verbose = v is not null },
+ _,
+ "Available commands:",
+ new Command("profile", "Profile a new process or attach to an existing process")
+ {
+ new CommandUsage("Usage: {NAME} [Options] "),
+ _,
+ new HelpOption(),
+ { "pid=", "The {PID} of the process", (int pid) => { pidList.Add(pid); } },
+ { "keep-merged-etl-file", "Keep the merged ETL file.", v => options.KeepMergedEtl = v is not null },
+ { "keep-intermediate-etl-files", "Keep the intermediate ETL files before merging.", v => options.KeepEtlIntermediateFiles = v is not null },
+ { "sampling-interval=", $"The {{VALUE}} of the sample interval in ms. Default is 8190Hz = {options.CpuSamplingIntervalInMs:0.000}ms.", (float v) => options.CpuSamplingIntervalInMs = v },
+ { "symbol-path=", $"The {{VALUE}} of symbol path. The default value is `{options.GetCachedSymbolPath()}`.", v => options.SymbolPathText = v },
+ // Action for the commit commandd
+ async (ctx, arguments) =>
+ {
+ if (arguments.Length == 0 && pidList.Count == 0)
+ {
+ AnsiConsole.MarkupLine("[red]Missing pid or executable name[/]");
+ return 1;
+ }
+
+ if (!EtwUltraProfiler.IsElevated())
+ {
+ AnsiConsole.MarkupLine("[darkorange]This command requires to run with administrator rights[/]");
+ return 1;
+ }
+
+ string? fileOutput = null;
+
+ await AnsiConsole.Status()
+ .Spinner(Spinner.Known.Default)
+ .SpinnerStyle(Style.Parse("red"))
+ .StartAsync("Profiling", async statusCtx =>
+ {
+ string? previousText = null;
+
+ options.LogStepProgress = (text) =>
+ {
+ if (verbose && previousText != text)
+ {
+ AnsiConsole.MarkupLine($"{previousText} [green]\u2713[/]");
+ previousText = text;
+ }
+
+ statusCtx.Status($"{text}");
+ };
+ options.LogProgress = (text) =>
+ {
+ if (verbose && previousText != null && previousText != text)
+ {
+ AnsiConsole.MarkupLine($"{previousText} [green]\u2713[/]");
+ }
+
+ statusCtx.Status(text);
+ previousText = text;
+ };
+ options.WaitingFileToComplete = (file) => { statusCtx.Status($"Waiting for {file} to complete"); };
+ options.WaitingFileToCompleteTimeOut = (file) => { statusCtx.Status($"Timeout waiting for {file} to complete"); };
+
+ // Add the pid passed as options
+ options.ProcessIds.AddRange(pidList);
+
+ if (arguments.Length == 1 && int.TryParse(arguments[0], out var pid))
+ {
+ options.ProcessIds.Add(pid);
+ }
+ else if (arguments.Length > 0)
+ {
+ options.ProgramPath = arguments[0];
+ options.Arguments.AddRange(arguments.AsSpan().Slice(1));
+ }
+
+ var etwProfiler = new EtwUltraProfiler();
+ try
+ {
+ Console.CancelKeyPress += (sender, eventArgs) =>
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine("[darkorange]Cancelled via CTRL+C[/]");
+ eventArgs.Cancel = true;
+ if (etwProfiler.Cancel())
+ {
+ AnsiConsole.MarkupLine("[red]Stopped via CTRL+C[/]");
+ }
+ };
+
+ fileOutput = await etwProfiler.Run(options);
+ }
+ finally
+ {
+ etwProfiler.Dispose();
+ }
+
+ if (verbose)
+ {
+ options.LogProgress.Invoke("Profiling Done");
+ }
+ }
+ );
+
+ if (fileOutput != null)
+ {
+ AnsiConsole.MarkupLine($"Generated Firefox Profiler JSON file -> [green]{fileOutput}[/] - {ByteSize.FromBytes(new FileInfo(fileOutput).Length)}");
+ AnsiConsole.MarkupLine($"Go to [blue]https://profiler.firefox.com/ [/]");
+ }
+
+ return 0;
+ }
+ },
+ new Command("convert", "Convert an existing ETL file to a Firefox Profiler json file")
+ {
+ new CommandUsage("Usage: {NAME} --pid xxx "),
+ _,
+ new HelpOption(),
+ { "pid=", "The {PID} of the process", (int pid) => { pidList.Add(pid); } },
+ { "symbol-path=", $"The {{VALUE}} of symbol path. The default value is `{options.GetCachedSymbolPath()}`.", v => options.SymbolPathText = v },
+ async (ctx, arguments) =>
+ {
+ var maxWidth = Console.IsOutputRedirected ? 80 : Console.WindowWidth;
+
+ if (arguments.Length == 0)
+ {
+ AnsiConsole.MarkupLine("[red]Missing ETL file name[/]");
+ return 1;
+ }
+
+ if (pidList.Count == 0)
+ {
+ AnsiConsole.MarkupLine("[red]Missing --pid option[/]");
+ return 1;
+ }
+
+ var etlFile = arguments[0];
+
+ string? fileOutput = null;
+
+ await AnsiConsole.Status()
+ .Spinner(Spinner.Known.Default)
+ .SpinnerStyle(Style.Parse("red"))
+ .StartAsync("Converting", async statusCtx =>
+ {
+ string? previousText = null;
+
+ options.LogStepProgress = (text) =>
+ {
+ if (verbose && previousText != text)
+ {
+ AnsiConsole.MarkupLine($"{previousText} [green]\u2713[/]");
+ previousText = text;
+ }
+
+ statusCtx.Status($"{text}");
+ };
+
+ options.LogProgress = (text) =>
+ {
+ if (verbose && previousText != null && previousText != text)
+ {
+ AnsiConsole.MarkupLine($"{previousText} [green]\u2713[/]");
+ }
+
+ statusCtx.Status(text);
+ previousText = text;
+ };
+
+ var etwProfiler = new EtwUltraProfiler();
+ try
+ {
+ Console.CancelKeyPress += (sender, eventArgs) =>
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine("[darkorange]Cancelled via CTRL+C[/]");
+
+ eventArgs.Cancel = true;
+ if (etwProfiler.Cancel())
+ {
+ AnsiConsole.MarkupLine("[red]Stopped via CTRL+C[/]");
+ }
+ };
+
+ fileOutput = await etwProfiler.Convert(etlFile, pidList, options);
+ }
+ finally
+ {
+ etwProfiler.Dispose();
+ }
+
+ if (verbose)
+ {
+ options.LogProgress.Invoke("Converting Done");
+ }
+ }
+ );
+
+
+ if (fileOutput != null)
+ {
+ AnsiConsole.MarkupLine($"Generated Firefox Profiler JSON file -> [green]{fileOutput}[/] - {ByteSize.FromBytes(new FileInfo(fileOutput).Length)}");
+ AnsiConsole.MarkupLine($"Go to [blue]https://profiler.firefox.com/ [/]");
+ }
+
+ return 0;
+ }
+ }
+ };
+
+ var width = Console.IsOutputRedirected ? 80 : Math.Max(80, Console.WindowWidth);
+ var optionWidth = Console.IsOutputRedirected || width == 80 ? 29 : 36;
+
+ try
+ {
+ return await commandApp.RunAsync(args, new CommandRunConfig(width, optionWidth));
+ }
+ catch (Exception ex)
+ {
+ AnsiConsole.Foreground = Color.Red;
+ AnsiConsole.WriteLine($"Unexpected error: {ex.Message}");
+ AnsiConsole.ResetColors();
+ if (verbose)
+ {
+ AnsiConsole.WriteLine(ex.ToString());
+ }
+ return 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ultra/Ultra.csproj b/src/Ultra/Ultra.csproj
new file mode 100644
index 0000000..3ddeb14
--- /dev/null
+++ b/src/Ultra/Ultra.csproj
@@ -0,0 +1,49 @@
+
+
+ Exe
+ net8.0
+ enable
+ true
+ true
+
+ ultra
+ ultra
+
+
+ true
+
+
+
+ This is a default project description
+ Alexandre Mutel
+ en-US
+ Alexandre Mutel
+ tag1;tag2;tag3
+ readme.md
+ ultra.png
+ https://github.com/xoofx/ultra
+ BSD-2-Clause
+
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/src/dotnet-releaser.toml b/src/dotnet-releaser.toml
new file mode 100644
index 0000000..52358fe
--- /dev/null
+++ b/src/dotnet-releaser.toml
@@ -0,0 +1,11 @@
+# configuration file for dotnet-releaser
+# Disable default packs - It will only publish NuGet and the Changelog
+profile = "custom"
+[msbuild]
+project = "ultra.sln"
+[github]
+user = "xoofx"
+repo = "ultra"
+#[[pack]]
+#rid = ["win-x64", "win-arm64"]
+#kinds = ["zip"]
\ No newline at end of file
diff --git a/src/global.json b/src/global.json
new file mode 100644
index 0000000..2aa85a4
--- /dev/null
+++ b/src/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "8.0.100",
+ "rollForward": "latestMinor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file
diff --git a/src/ultra.sln b/src/ultra.sln
new file mode 100644
index 0000000..67d0124
--- /dev/null
+++ b/src/ultra.sln
@@ -0,0 +1,51 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ultra", "Ultra\Ultra.csproj", "{FC67D552-9C9A-4CA2-8065-3D5C67116939}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ultra.Tests", "Ultra.Tests\Ultra.Tests.csproj", "{5AC3B614-66DB-4092-BEBC-D5415A7D0361}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BBC4B16D-083F-47E6-BFAE-9064E600F230}"
+ ProjectSection(SolutionItems) = preProject
+ ..\.editorconfig = ..\.editorconfig
+ ..\.gitattributes = ..\.gitattributes
+ ..\.gitignore = ..\.gitignore
+ ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml
+ Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
+ dotnet-releaser.toml = dotnet-releaser.toml
+ global.json = global.json
+ ..\license.txt = ..\license.txt
+ ..\readme.md = ..\readme.md
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ultra.Core", "Ultra.Core\Ultra.Core.csproj", "{CF642542-CAD0-47E3-A76A-218EEB92661D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FC67D552-9C9A-4CA2-8065-3D5C67116939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC67D552-9C9A-4CA2-8065-3D5C67116939}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC67D552-9C9A-4CA2-8065-3D5C67116939}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC67D552-9C9A-4CA2-8065-3D5C67116939}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AC3B614-66DB-4092-BEBC-D5415A7D0361}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AC3B614-66DB-4092-BEBC-D5415A7D0361}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AC3B614-66DB-4092-BEBC-D5415A7D0361}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AC3B614-66DB-4092-BEBC-D5415A7D0361}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CF642542-CAD0-47E3-A76A-218EEB92661D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CF642542-CAD0-47E3-A76A-218EEB92661D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CF642542-CAD0-47E3-A76A-218EEB92661D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CF642542-CAD0-47E3-A76A-218EEB92661D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {95E8F3B3-49E8-400E-97A1-38D8B946DFCA}
+ EndGlobalSection
+EndGlobal