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 @@ + + + + + ultra + + + + + + + + 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