├── .ado.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── PerfViewJS.sln ├── README.md └── src ├── .editorconfig ├── CacheExpirationTimeProvider.cs ├── CallTreeData.cs ├── CallTreeDataCache.cs ├── CallTreeDataEventSource.cs ├── Controllers ├── EventViewerController.cs └── StackViewerController.cs ├── DeserializedData.cs ├── DeserializedDataCache.cs ├── DetailedProcessInfo.cs ├── HttpApplication.cs ├── ICacheExpirationTimeProvider.cs ├── ICallTreeData.cs ├── IDeserializedData.cs ├── IDeserializedDataCache.cs ├── KestrelServerOptionsConfig.cs ├── Logging └── DefaultEventSourceLoggerFactory.cs ├── MemoryCacheOptionsConfig.cs ├── Models ├── EventData.cs ├── EventViewerModel.cs ├── LineInformation.cs ├── SourceInformation.cs ├── StackViewerModel.cs └── TreeNode.cs ├── ModuleInfo.cs ├── PerfViewJS.csproj ├── PerfViewJS.nuspec ├── ProcessInfo.cs ├── Program.cs ├── Properties └── launchSettings.json ├── SocketTransportOptionsConfig.cs ├── StackEventTypeInfo.cs ├── Startup.cs ├── ThreadInfo.cs ├── ThrowHelper.cs ├── TraceEventStackSourceExtensions.cs ├── TraceInfo.cs ├── TraceLogDeserializer.cs ├── msdia140.dll ├── spa ├── .gitignore ├── compress.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── scss │ └── custom.scss ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── components │ │ ├── Callers.tsx │ │ ├── EventList.tsx │ │ ├── EventViewer.tsx │ │ ├── Home.tsx │ │ ├── Hotspots.tsx │ │ ├── Layout.css │ │ ├── Layout.tsx │ │ ├── ModuleList.tsx │ │ ├── NavMenu.css │ │ ├── NavMenu.tsx │ │ ├── ProcessChooser.tsx │ │ ├── ProcessInfo.tsx │ │ ├── ProcessList.tsx │ │ ├── SourceViewer.css │ │ ├── SourceViewer.tsx │ │ ├── StackViewerFilter.tsx │ │ ├── TNode.tsx │ │ ├── TraceInfo.tsx │ │ └── TreeNode.tsx │ └── index.tsx └── tsconfig.json └── stylecop.json /.ado.yml: -------------------------------------------------------------------------------- 1 | # https://aka.ms/yaml 2 | 3 | name: "$(date:yyyyMMdd)$(rev:.r)" 4 | 5 | trigger: 6 | - main 7 | 8 | jobs: 9 | - job: PerfViewJS 10 | pool: 11 | vmImage: 'windows-2019' 12 | name: Azure Pipelines 13 | demands: msbuild 14 | 15 | steps: 16 | - task: MSBuild@1 17 | displayName: 'Build src/PerfViewJS.csproj' 18 | inputs: 19 | solution: src/PerfViewJS.csproj 20 | msbuildArguments: /restore 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Private files 5 | OSExtentions.cs 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.sln.docstates 11 | *.csproj.metaproj 12 | *.sln.metaproj 13 | !Global.csproj.user 14 | 15 | # VS 2015 stuff 16 | .vs/ 17 | *.VC.opendb 18 | *.VC.db 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | 32 | # Roslyn cache directories 33 | *.ide/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | #NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | *.pdb 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding addin-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | 113 | # MightyMoose 114 | *.mm.* 115 | AutoTest.Net/ 116 | 117 | # Web workbench (sass) 118 | .sass-cache/ 119 | 120 | # Installshield output folder 121 | [Ee]xpress/ 122 | 123 | # DocProject is a documentation generator add-in 124 | DocProject/buildhelp/ 125 | DocProject/Help/*.HxT 126 | DocProject/Help/*.HxC 127 | DocProject/Help/*.hhc 128 | DocProject/Help/*.hhk 129 | DocProject/Help/*.hhp 130 | DocProject/Help/Html2 131 | DocProject/Help/html 132 | 133 | # Click-Once directory 134 | publish/ 135 | 136 | # Publish Web Output 137 | *.[Pp]ublish.xml 138 | *.azurePubxml 139 | # TODO: Comment the next line if you want to checkin your web deploy settings 140 | # but database connection strings (with potential passwords) will be unencrypted 141 | *.pubxml 142 | *.publishproj 143 | 144 | # NuGet Packages 145 | *.nupkg 146 | **/project.lock.json 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 152 | #!**/packages/repositories.config 153 | 154 | # Windows Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Windows Store app package directory 159 | AppPackages/ 160 | 161 | # Others 162 | sql/ 163 | *.Cache 164 | ClientBin/ 165 | [Ss]tyle[Cc]op.* 166 | ~$* 167 | *~ 168 | *.dbmdl 169 | *.dbproj.schemaview 170 | *.pfx 171 | *.publishsettings 172 | node_modules/ 173 | 174 | # RIA/Silverlight projects 175 | Generated_Code/ 176 | 177 | # Backup & report files from converting an old project file 178 | # to a newer Visual Studio version. Backup files are not needed, 179 | # because we have git ;-) 180 | _UpgradeReport_Files/ 181 | Backup*/ 182 | UpgradeLog*.XML 183 | UpgradeLog*.htm 184 | 185 | # SQL Server files 186 | *.mdf 187 | *.ldf 188 | 189 | # Business Intelligence projects 190 | *.rdl.data 191 | *.bim.layout 192 | *.bim_*.settings 193 | 194 | # Microsoft Fakes 195 | FakesAssemblies/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/src/bin/Debug/netcoreapp3.1/PerfViewJS.dll", 13 | "args": ["${input:port}", "${input:dataDirectory}"], 14 | "cwd": "${workspaceFolder}", 15 | "stopAtEntry": false, 16 | "console": "internalConsole" 17 | }, 18 | { 19 | "name": "Single page application", 20 | "type": "node", 21 | "cwd": "${workspaceFolder}/src/spa", 22 | "runtimeExecutable": "npm", 23 | "runtimeArgs": ["run", "start"], 24 | "request": "launch" 25 | } 26 | ], 27 | "compounds": [ 28 | { 29 | "name": "Run & debug everything", 30 | "configurations": [".NET Core Launch (console)", "Single page application"] 31 | } 32 | ], 33 | "inputs": [ 34 | { 35 | "id": "port", 36 | "type": "promptString", 37 | "description": "Enter the port for the .NET server.", 38 | "default": "5000" 39 | }, 40 | { 41 | "id": "dataDirectory", 42 | "type": "promptString", 43 | "description": "Enter the data directory to search for trace files.", 44 | "default": "." 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "args": [ 8 | "build", 9 | "-c", "Debug", 10 | "-v", "m", 11 | "-m", 12 | "${workspaceRoot}/src/PerfViewJS.csproj" 13 | ], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "problemMatcher": "$msCompile" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /PerfViewJS.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29613.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerfViewJS", "src\PerfViewJS.csproj", "{A2AECB8A-B4F5-4006-A5D2-D10B25B10FF8}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A2AECB8A-B4F5-4006-A5D2-D10B25B10FF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A2AECB8A-B4F5-4006-A5D2-D10B25B10FF8}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A2AECB8A-B4F5-4006-A5D2-D10B25B10FF8}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A2AECB8A-B4F5-4006-A5D2-D10B25B10FF8}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {69F81695-89EF-4B75-A687-232DE132A3B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {69F81695-89EF-4B75-A687-232DE132A3B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {69F81695-89EF-4B75-A687-232DE132A3B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {69F81695-89EF-4B75-A687-232DE132A3B2}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ExtensibilityGlobals) = postSolution 27 | SolutionGuid = {AB447D21-8260-4F5F-9410-293E8C801802} 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo has been archived. See [this issue](https://github.com/microsoft/perfviewjs/issues/15) for more details. 2 | 3 | # PerfViewJS 4 | 5 | PerfViewJS is a webviewer for ETL, NetPerf and NetTrace data. 6 | 7 | ## Usage 8 | 9 | * On terminal: `dotnet run -p src/PerfViewJS.csproj 5000 .` 10 | * Open another terminal and run: 11 | * * `cd src/spa` 12 | * * `npm run start` 13 | * Browse to http://localhost:5000 14 | * Place your nettrace, etl, netperf, or btl file in repositories root directory 15 | 16 | ## Debugging 17 | 18 | * Press F5 in Visual Studio 19 | * cd src/spa 20 | * npm run start 21 | * Browse to http://localhost:3000 22 | 23 | In VSCode: 24 | * Select the "Run & debug everything" launch task on Run & Debug menu 25 | * This will launch port 5000 by default for the web server and port 3000 for SPA 26 | 27 | ## Todo 28 | 29 | * Wrap PerfViewJS as a dotnet global tool 30 | * Use Chromium Embedded Framework to make a client-side application 31 | 32 | ## Components 33 | 34 | PerfViewJS is an ASP.NET Core application. React is used for rendering and GUI state. 35 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | indent_style = space 3 | indent_size = 4 4 | 5 | # SA1600: Elements should be documented 6 | dotnet_diagnostic.SA1600.severity = none 7 | -------------------------------------------------------------------------------- /src/CacheExpirationTimeProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | 9 | public sealed class CacheExpirationTimeProvider : ICacheExpirationTimeProvider 10 | { 11 | public TimeSpan Expiration => TimeSpan.FromMinutes(120); // TODO: make this configurable 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/CallTreeData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text.RegularExpressions; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using global::Diagnostics.Tracing.StackSources; 15 | using Microsoft.Diagnostics.Symbols; 16 | using Microsoft.Diagnostics.Tracing.Stacks; 17 | 18 | public sealed class CallTreeData : ICallTreeData 19 | { 20 | private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); 21 | 22 | private readonly object lockObj = new object(); 23 | 24 | private readonly StackSource stackSource; 25 | 26 | private readonly StackViewerModel model; 27 | 28 | private int initialized; 29 | 30 | private Tuple tuple; 31 | 32 | public CallTreeData(StackSource stackSource, StackViewerModel model) 33 | { 34 | this.stackSource = stackSource; 35 | this.model = model; 36 | } 37 | 38 | public async ValueTask GetNode(string name) 39 | { 40 | await this.EnsureInitialized(); 41 | 42 | return this.GetNodeInner(name, this.tuple); 43 | } 44 | 45 | public async ValueTask GetCalleeTreeNode(string name, string path = "") 46 | { 47 | if (name == null) 48 | { 49 | ThrowHelper.ThrowArgumentNullException(nameof(name)); 50 | } 51 | 52 | await this.EnsureInitialized(); 53 | 54 | var t = this.tuple; 55 | 56 | var node = this.GetNodeInner(name, t); 57 | 58 | lock (this.lockObj) 59 | { 60 | CallTreeNodeBase backingNode = node.BackingNode; 61 | TreeNode calleeTreeNode; 62 | 63 | var c = t.CalleeTreeCache; 64 | 65 | if (c.ContainsKey(backingNode)) 66 | { 67 | calleeTreeNode = c[backingNode]; 68 | } 69 | else 70 | { 71 | calleeTreeNode = new TreeNode(AggregateCallTreeNode.CalleeTree(backingNode)); 72 | c.Add(backingNode, calleeTreeNode); 73 | } 74 | 75 | if (string.IsNullOrEmpty(path)) 76 | { 77 | return calleeTreeNode; 78 | } 79 | 80 | var pathArr = path.Split('/'); 81 | var pathNodeRoot = calleeTreeNode.Children[int.Parse(pathArr[0])]; 82 | 83 | for (int i = 1; i < pathArr.Length; ++i) 84 | { 85 | pathNodeRoot = pathNodeRoot.Children[int.Parse(pathArr[i])]; 86 | } 87 | 88 | return pathNodeRoot; 89 | } 90 | } 91 | 92 | public async ValueTask GetCallerTree(string name, char sep) 93 | { 94 | var node = await this.GetCallerTreeNode(name, sep); 95 | return node.Children; 96 | } 97 | 98 | public async ValueTask GetCallerTree(string name, char sep, string path) 99 | { 100 | var node = await this.GetCallerTreeNode(name, sep, path); 101 | return node.Children; 102 | } 103 | 104 | public async ValueTask GetCalleeTree(string name) 105 | { 106 | var node = await this.GetCalleeTreeNode(name); 107 | return node.Children; 108 | } 109 | 110 | public async ValueTask GetCalleeTree(string name, string path) 111 | { 112 | var node = await this.GetCalleeTreeNode(name, path); 113 | return node.Children; 114 | } 115 | 116 | public async ValueTask> GetSummaryTree(int numNodes) 117 | { 118 | await this.EnsureInitialized(); 119 | 120 | var nodes = this.tuple.CallTree.ByIDSortedExclusiveMetric().Take(numNodes); 121 | 122 | var summaryNodes = new List(); 123 | foreach (var node in nodes) 124 | { 125 | summaryNodes.Add(new TreeNode(node)); 126 | } 127 | 128 | return summaryNodes; 129 | } 130 | 131 | public async ValueTask GetDrillIntoStackSource(bool exclusive, string name, char sep, string path = "") 132 | { 133 | var node = await this.GetCallerTreeNode(name, sep, path); 134 | var callTreeNode = node.BackingNode; 135 | 136 | var originalStackSource = callTreeNode.CallTree.StackSource; 137 | var drillIntoStackSource = new CopyStackSource(originalStackSource); 138 | 139 | callTreeNode.GetSamples(exclusive, index => 140 | { 141 | drillIntoStackSource.AddSample(originalStackSource.GetSampleByIndex(index)); 142 | return true; 143 | }); 144 | 145 | return drillIntoStackSource; 146 | } 147 | 148 | public async ValueTask Source(string authorizationHeader, string name, char sep, string path = "") 149 | { 150 | var node = await this.GetCallerTreeNode(name, sep, path); 151 | 152 | var asCallTreeNodeBase = node.BackingNode; 153 | string cellText = node.Name; 154 | 155 | var m = Regex.Match(cellText, "<<(.*!.*)>>"); 156 | 157 | if (m.Success) 158 | { 159 | cellText = m.Groups[1].Value; 160 | } 161 | 162 | var ss = this.tuple.CallTree.StackSource; 163 | 164 | var frameIndexCounts = new Dictionary(); 165 | asCallTreeNodeBase.GetSamples(false, sampleIdx => 166 | { 167 | var matchingFrameIndex = StackSourceFrameIndex.Invalid; 168 | var sample = ss.GetSampleByIndex(sampleIdx); 169 | var callStackIdx = sample.StackIndex; 170 | 171 | while (callStackIdx != StackSourceCallStackIndex.Invalid) 172 | { 173 | var frameIndex = ss.GetFrameIndex(callStackIdx); 174 | var frameName = ss.GetFrameName(frameIndex, false); 175 | 176 | if (frameName == cellText) 177 | { 178 | matchingFrameIndex = frameIndex; 179 | } 180 | 181 | callStackIdx = ss.GetCallerIndex(callStackIdx); 182 | } 183 | 184 | if (matchingFrameIndex != StackSourceFrameIndex.Invalid) 185 | { 186 | frameIndexCounts.TryGetValue(matchingFrameIndex, out float count); 187 | frameIndexCounts[matchingFrameIndex] = count + sample.Metric; 188 | } 189 | 190 | return true; 191 | }); 192 | 193 | StackSourceFrameIndex maxFrameIdx = StackSourceFrameIndex.Invalid; 194 | float maxFrameIdxCount = -1; 195 | foreach (var keyValue in frameIndexCounts) 196 | { 197 | if (keyValue.Value >= maxFrameIdxCount) 198 | { 199 | maxFrameIdxCount = keyValue.Value; 200 | maxFrameIdx = keyValue.Key; 201 | } 202 | } 203 | 204 | if (maxFrameIdx == StackSourceFrameIndex.Invalid) 205 | { 206 | // TODO: Error handling ("Could not find " + cellText + " in call stack!") 207 | return null; 208 | } 209 | 210 | var asTraceEventStackSource = GetTraceEventStackSource(ss); 211 | 212 | if (asTraceEventStackSource == null) 213 | { 214 | // TODO: Error handling ("Source does not support symbolic lookup.") 215 | return null; 216 | } 217 | 218 | var log = new StringWriter(); 219 | using var reader = new SymbolReader(log) { AuthorizationHeaderForSourceLink = authorizationHeader }; 220 | var sourceLocation = asTraceEventStackSource.GetSourceLine(maxFrameIdx, reader); 221 | 222 | if (sourceLocation == null) 223 | { 224 | // TODO: Error handling ("Source could not find a source location for the given Frame.") 225 | return null; 226 | } 227 | 228 | var sourceFile = sourceLocation.SourceFile; 229 | 230 | var filePathForMax = sourceFile.BuildTimeFilePath; 231 | var metricOnLine = new SortedDictionary(Comparer.Create((x, y) => y.CompareTo(x))); 232 | 233 | foreach (StackSourceFrameIndex frameIdx in frameIndexCounts.Keys) 234 | { 235 | var loc = asTraceEventStackSource.GetSourceLine(frameIdx, reader); 236 | if (loc != null && loc.SourceFile.BuildTimeFilePath == filePathForMax) 237 | { 238 | metricOnLine.TryGetValue(loc.LineNumber, out var metric); 239 | metric += frameIndexCounts[frameIdx]; 240 | metricOnLine[loc.LineNumber] = metric; 241 | } 242 | } 243 | 244 | var data = File.ReadAllText(sourceFile.GetSourceFile()); 245 | 246 | var list = new LineInformation[metricOnLine.Count]; 247 | 248 | int i = 0; 249 | foreach (var lineMetric in metricOnLine) 250 | { 251 | list[i++] = new LineInformation { LineNumber = lineMetric.Key, Metric = lineMetric.Value }; 252 | } 253 | 254 | var si = new SourceInformation 255 | { 256 | Url = sourceFile.Url, 257 | Log = log.ToString(), 258 | BuildTimeFilePath = filePathForMax, 259 | Summary = list, 260 | Data = data, 261 | }; 262 | 263 | return si; 264 | } 265 | 266 | public void UnInitialize() 267 | { 268 | this.initialized = 0; 269 | } 270 | 271 | public string LookupWarmSymbols(int minCount) 272 | { 273 | var traceEventStackSource = GetTraceEventStackSource(this.stackSource); 274 | if (traceEventStackSource != null) 275 | { 276 | var writer = new StringWriter(); 277 | using (var symbolReader = new SymbolReader(writer)) 278 | { 279 | traceEventStackSource.LookupWarmSymbols(minCount, symbolReader); 280 | } 281 | 282 | this.UnInitialize(); 283 | return writer.ToString(); 284 | } 285 | 286 | return "Unable to find TraceEventStackSource. This a fatal error for symbol lookup"; 287 | } 288 | 289 | private static TraceEventStackSource GetTraceEventStackSource(StackSource source) 290 | { 291 | StackSourceStacks rawSource = source; 292 | while (true) 293 | { 294 | if (rawSource is TraceEventStackSource asTraceEventStackSource) 295 | { 296 | return asTraceEventStackSource; 297 | } 298 | 299 | if (rawSource is CopyStackSource asCopyStackSource) 300 | { 301 | rawSource = asCopyStackSource.SourceStacks; 302 | continue; 303 | } 304 | 305 | if (rawSource is StackSource asStackSource && asStackSource != asStackSource.BaseStackSource) 306 | { 307 | rawSource = asStackSource.BaseStackSource; 308 | continue; 309 | } 310 | 311 | return null; 312 | } 313 | } 314 | 315 | private async ValueTask GetCallerTreeNode(string name, char sep, string path = "") 316 | { 317 | if (name == null) 318 | { 319 | ThrowHelper.ThrowArgumentNullException(nameof(name)); 320 | } 321 | 322 | await this.EnsureInitialized(); 323 | 324 | var t = this.tuple; 325 | 326 | var node = this.GetNodeInner(name, t); 327 | 328 | lock (this.lockObj) 329 | { 330 | CallTreeNodeBase backingNode = node.BackingNode; 331 | TreeNode callerTreeNode; 332 | 333 | var c = t.CallerTreeCache; 334 | 335 | if (c.ContainsKey(backingNode)) 336 | { 337 | callerTreeNode = c[backingNode]; 338 | } 339 | else 340 | { 341 | callerTreeNode = new TreeNode(AggregateCallTreeNode.CallerTree(backingNode)); 342 | c.Add(backingNode, callerTreeNode); 343 | } 344 | 345 | if (string.IsNullOrEmpty(path)) 346 | { 347 | return callerTreeNode; 348 | } 349 | 350 | var pathArr = path.Split(sep); 351 | var pathNodeRoot = callerTreeNode.Children[int.Parse(pathArr[0])]; 352 | 353 | for (int i = 1; i < pathArr.Length; ++i) 354 | { 355 | pathNodeRoot = pathNodeRoot.Children[int.Parse(pathArr[i])]; 356 | } 357 | 358 | return pathNodeRoot; 359 | } 360 | } 361 | 362 | private TreeNode GetNodeInner(string name, Tuple t) 363 | { 364 | lock (this.lockObj) 365 | { 366 | var n = t.NodeNameCache; 367 | 368 | if (n.ContainsKey(name)) 369 | { 370 | CallTreeDataEventSource.Log.NodeCacheHit(name); 371 | return new TreeNode(n[name]); 372 | } 373 | else 374 | { 375 | var c = t.CallTree; 376 | foreach (var node in c.ByID) 377 | { 378 | if (node.Name == name) 379 | { 380 | n.Add(name, node); 381 | CallTreeDataEventSource.Log.NodeCacheMisss(name); 382 | return new TreeNode(node); 383 | } 384 | } 385 | 386 | CallTreeDataEventSource.Log.NodeCacheNotFound(name); 387 | return null; 388 | } 389 | } 390 | } 391 | 392 | private async Task EnsureInitialized() 393 | { 394 | if (Interlocked.CompareExchange(ref this.initialized, 1, comparand: -1) == 0) 395 | { 396 | await this.Initialize(); 397 | } 398 | } 399 | 400 | private async Task Initialize() 401 | { 402 | await this.semaphoreSlim.WaitAsync(); 403 | 404 | try 405 | { 406 | if (this.initialized == 1) 407 | { 408 | return; 409 | } 410 | 411 | var filterParams = new FilterParams 412 | { 413 | StartTimeRelativeMSec = this.model.Start, 414 | EndTimeRelativeMSec = this.model.End, 415 | ExcludeRegExs = this.model.ExcPats, 416 | IncludeRegExs = this.model.IncPats, 417 | FoldRegExs = this.model.FoldPats, 418 | GroupRegExs = this.model.GroupPats, 419 | MinInclusiveTimePercent = this.model.FoldPct, 420 | Name = "NoName", 421 | }; 422 | 423 | var ss = new FilterStackSource(filterParams, this.stackSource, ScalingPolicyKind.TimeMetric); 424 | 425 | double startTimeRelativeMsec = double.TryParse(filterParams.StartTimeRelativeMSec, out startTimeRelativeMsec) ? Math.Max(startTimeRelativeMsec, 0.0) : 0.0; 426 | double endTimeRelativeMsec = double.TryParse(filterParams.EndTimeRelativeMSec, out endTimeRelativeMsec) ? Math.Min(endTimeRelativeMsec, this.stackSource.SampleTimeRelativeMSecLimit) : this.stackSource.SampleTimeRelativeMSecLimit; 427 | 428 | var c = new CallTree(ScalingPolicyKind.TimeMetric); 429 | c.TimeHistogramController = new TimeHistogramController(c, startTimeRelativeMsec, endTimeRelativeMsec); 430 | c.StackSource = ss; 431 | 432 | if (float.TryParse(filterParams.MinInclusiveTimePercent, out float minIncusiveTimePercent) && minIncusiveTimePercent > 0) 433 | { 434 | c.FoldNodesUnder(minIncusiveTimePercent * c.Root.InclusiveMetric / 100, true); 435 | } 436 | 437 | var t = new Tuple(c); 438 | this.tuple = t; 439 | 440 | this.initialized = 1; 441 | } 442 | finally 443 | { 444 | this.semaphoreSlim.Release(); 445 | } 446 | } 447 | 448 | private sealed class Tuple 449 | { 450 | public Tuple(CallTree callTree) 451 | { 452 | this.CallTree = callTree; 453 | } 454 | 455 | public Dictionary NodeNameCache => new Dictionary(); 456 | 457 | public Dictionary CallerTreeCache => new Dictionary(); 458 | 459 | public Dictionary CalleeTreeCache => new Dictionary(); 460 | 461 | public CallTree CallTree { get; } 462 | } 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/CallTreeDataCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using Microsoft.Extensions.Caching.Memory; 8 | using Microsoft.Extensions.Options; 9 | 10 | public sealed class CallTreeDataCache : MemoryCache 11 | { 12 | public CallTreeDataCache(IOptions optionsAccessor) 13 | : base(optionsAccessor) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/CallTreeDataEventSource.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Diagnostics.Tracing; 8 | 9 | [EventSource(Guid = "240e729b-d191-59e3-cdd0-aa0a8abed0c3")] 10 | public sealed class CallTreeDataEventSource : EventSource 11 | { 12 | public static CallTreeDataEventSource Log { get; } = new CallTreeDataEventSource(); 13 | 14 | public void NodeCacheHit(string node) 15 | { 16 | this.WriteEvent(1, node); 17 | } 18 | 19 | public void NodeCacheMisss(string node) 20 | { 21 | this.WriteEvent(2, node); 22 | } 23 | 24 | public void NodeCacheNotFound(string node) 25 | { 26 | this.WriteEvent(3, node); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Controllers/EventViewerController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | 10 | public sealed class EventViewerController 11 | { 12 | private readonly IDeserializedDataCache dataCache; 13 | 14 | private readonly EventViewerModel eventViewerModel; 15 | 16 | public EventViewerController(IDeserializedDataCache dataCache, EventViewerModel eventViewerModel) 17 | { 18 | this.dataCache = dataCache; 19 | this.eventViewerModel = eventViewerModel; 20 | } 21 | 22 | public async ValueTask> EventsAPI() 23 | { 24 | return await this.dataCache.GetData(this.eventViewerModel.Filename).GetEvents(this.eventViewerModel); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controllers/StackViewerController.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.WebUtilities; 12 | 13 | public sealed class StackViewerController 14 | { 15 | private const int NumberOfDisplayedTableEntries = 100; 16 | 17 | private readonly IDeserializedDataCache dataCache; 18 | 19 | private readonly StackViewerModel model; 20 | 21 | public StackViewerController(IDeserializedDataCache dataCache, StackViewerModel model) 22 | { 23 | this.dataCache = dataCache; 24 | this.model = model; 25 | } 26 | 27 | public async ValueTask EventListAPIOrderedByName() 28 | { 29 | var deserializedData = this.dataCache.GetData(this.model.Filename); 30 | return await deserializedData.GetStackEventTypesAsyncOrderedByName(); 31 | } 32 | 33 | public async ValueTask EventListAPIOrderedByStackCount() 34 | { 35 | var deserializedData = this.dataCache.GetData(this.model.Filename); 36 | return await deserializedData.GetStackEventTypesAsyncOrderedByStackCount(); 37 | } 38 | 39 | public async ValueTask ProcessChooserAPI() 40 | { 41 | var deserializedData = this.dataCache.GetData(this.model.Filename); 42 | return await deserializedData.GetProcessChooserAsync(); 43 | } 44 | 45 | public async ValueTask DetailedProcessInfoAPI(int processIndex) 46 | { 47 | var deserializedData = this.dataCache.GetData(this.model.Filename); 48 | return await deserializedData.GetDetailedProcessInfoAsync(processIndex); 49 | } 50 | 51 | public async ValueTask> HotspotsAPI() 52 | { 53 | var data = await this.GetData(); 54 | return await data.GetSummaryTree(NumberOfDisplayedTableEntries); 55 | } 56 | 57 | public async ValueTask TreeNodeAPI(string name) 58 | { 59 | var data = await this.GetData(); 60 | return await data.GetNode(Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(name))); 61 | } 62 | 63 | public async ValueTask CallerChildrenAPI(string name, string path) 64 | { 65 | var data = await this.GetData(); 66 | return await data.GetCallerTree(Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(name)), '-', path); 67 | } 68 | 69 | public async ValueTask GetSourceAPI(string name, string path, string authorizationHeader) 70 | { 71 | var data = await this.GetData(); 72 | return await data.Source(authorizationHeader, Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(name)), '-', path); 73 | } 74 | 75 | public async ValueTask DrillIntoAPI(bool exclusive, string name, string path) 76 | { 77 | var data = await this.GetData(); 78 | var stackSource = await data.GetDrillIntoStackSource(exclusive, Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(name)), '-', path); 79 | string samplesKey = Guid.NewGuid().ToString(); 80 | this.model.SetDrillIntoKey(samplesKey); 81 | await this.dataCache.GetData(this.model.Filename).GetCallTreeAsync(this.model, stackSource); 82 | return samplesKey; 83 | } 84 | 85 | public async ValueTask LookupWarmSymbolsAPI(int minCount) 86 | { 87 | var data = await this.GetData(); 88 | var retVal = data.LookupWarmSymbols(minCount); 89 | this.dataCache.ClearAllCacheEntries(); 90 | return retVal; 91 | } 92 | 93 | public async ValueTask GetTraceInfoAPI() 94 | { 95 | return await this.dataCache.GetData(this.model.Filename).GetTraceInfoAsync(); 96 | } 97 | 98 | public async ValueTask GetModulesAPI() 99 | { 100 | return await this.dataCache.GetData(this.model.Filename).GetModulesAsync(); 101 | } 102 | 103 | public async ValueTask LookupSymbolAPI(int moduleIndex) 104 | { 105 | return await this.dataCache.GetData(this.model.Filename).LookupSymbolAsync(moduleIndex); 106 | } 107 | 108 | public async ValueTask LookupSymbolsAPI(int[] moduleIndices) 109 | { 110 | return await this.dataCache.GetData(this.model.Filename).LookupSymbolsAsync(moduleIndices); 111 | } 112 | 113 | private ValueTask GetData() 114 | { 115 | var deserializedData = this.dataCache.GetData(this.model.Filename); 116 | return deserializedData.GetCallTreeAsync(this.model); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/DeserializedData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.Diagnostics.Tracing.Etlx; 13 | using Microsoft.Diagnostics.Tracing.Stacks; 14 | 15 | public sealed class DeserializedData : IDeserializedData 16 | { 17 | private readonly string filename; 18 | 19 | private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); 20 | 21 | private readonly Dictionary callTreeDataCache = new Dictionary(); 22 | 23 | private readonly Dictionary stackSourceCache = new Dictionary(); 24 | 25 | private int initialized; 26 | 27 | private TraceLogDeserializer deserializer; 28 | 29 | private ProcessInfo[] processList; 30 | 31 | private ModuleInfo[] moduleInfoList; 32 | 33 | private StackEventTypeInfo[] stackEventTypesOrderedByName; 34 | 35 | private StackEventTypeInfo[] stackEventTypesOrderedByStackCount; 36 | 37 | private TraceInfo traceInfo; 38 | 39 | public DeserializedData(string filename) 40 | { 41 | this.filename = filename; 42 | } 43 | 44 | public async ValueTask GetStackEventTypesAsyncOrderedByName() 45 | { 46 | await this.EnsureInitialized(); 47 | return this.stackEventTypesOrderedByName; 48 | } 49 | 50 | public async ValueTask GetStackEventTypesAsyncOrderedByStackCount() 51 | { 52 | await this.EnsureInitialized(); 53 | return this.stackEventTypesOrderedByStackCount; 54 | } 55 | 56 | public async ValueTask GetCallTreeAsync(StackViewerModel model, StackSource stackSource = null) 57 | { 58 | await this.EnsureInitialized(); 59 | 60 | lock (this.callTreeDataCache) 61 | { 62 | if (!this.callTreeDataCache.TryGetValue(model, out var value)) 63 | { 64 | double start = string.IsNullOrEmpty(model.Start) ? 0.0 : double.Parse(model.Start); 65 | double end = string.IsNullOrEmpty(model.End) ? 0.0 : double.Parse(model.End); 66 | 67 | var key = new StackSourceCacheKey((ProcessIndex)int.Parse(model.Pid), int.Parse(model.StackType), start, end, model.DrillIntoKey); 68 | if (!this.stackSourceCache.TryGetValue(key, out var ss)) 69 | { 70 | ss = stackSource ?? this.deserializer.GetStackSource((ProcessIndex)int.Parse(model.Pid), int.Parse(model.StackType), start, end); 71 | this.stackSourceCache.Add(key, ss); 72 | } 73 | else 74 | { 75 | var drillIntoStackSource = new CopyStackSource(GetTraceEventStackSource(ss)); 76 | 77 | ss.ForEach(delegate(StackSourceSample sample) 78 | { 79 | drillIntoStackSource.AddSample(sample); 80 | }); 81 | 82 | ss = drillIntoStackSource; 83 | } 84 | 85 | value = new CallTreeData(stackSource ?? ss, model); 86 | this.callTreeDataCache.Add(model, value); 87 | } 88 | 89 | return value; 90 | } 91 | } 92 | 93 | public async ValueTask GetDetailedProcessInfoAsync(int processIndex) 94 | { 95 | await this.EnsureInitialized(); 96 | return this.deserializer.GetDetailedProcessInfo(processIndex); 97 | } 98 | 99 | public async ValueTask GetProcessChooserAsync() 100 | { 101 | await this.EnsureInitialized(); 102 | return this.processList; 103 | } 104 | 105 | public async ValueTask> GetEvents(EventViewerModel model) 106 | { 107 | await this.EnsureInitialized(); 108 | return this.deserializer.GetEvents(model.EventTypes, model.TextFilter, model.MaxEventCount, model.Start, model.End); 109 | } 110 | 111 | public async ValueTask GetTraceInfoAsync() 112 | { 113 | await this.EnsureInitialized(); 114 | return this.traceInfo; 115 | } 116 | 117 | public async ValueTask GetModulesAsync() 118 | { 119 | await this.EnsureInitialized(); 120 | return this.moduleInfoList; 121 | } 122 | 123 | public async ValueTask LookupSymbolAsync(int moduleIndex) 124 | { 125 | await this.EnsureInitialized(); 126 | var retVal = this.deserializer.LookupSymbol(moduleIndex); 127 | 128 | lock (this.callTreeDataCache) 129 | { 130 | this.callTreeDataCache.Clear(); 131 | } 132 | 133 | return retVal; 134 | } 135 | 136 | public async ValueTask LookupSymbolsAsync(int[] moduleIndices) 137 | { 138 | await this.EnsureInitialized(); 139 | var retVal = this.deserializer.LookupSymbols(moduleIndices); 140 | 141 | lock (this.callTreeDataCache) 142 | { 143 | this.callTreeDataCache.Clear(); 144 | } 145 | 146 | return retVal; 147 | } 148 | 149 | private static TraceEventStackSource GetTraceEventStackSource(StackSource source) 150 | { 151 | StackSourceStacks rawSource = source; 152 | while (true) 153 | { 154 | if (rawSource is TraceEventStackSource asTraceEventStackSource) 155 | { 156 | return asTraceEventStackSource; 157 | } 158 | 159 | if (rawSource is CopyStackSource asCopyStackSource) 160 | { 161 | rawSource = asCopyStackSource.SourceStacks; 162 | continue; 163 | } 164 | 165 | if (rawSource is StackSource asStackSource && asStackSource != asStackSource.BaseStackSource) 166 | { 167 | rawSource = asStackSource.BaseStackSource; 168 | continue; 169 | } 170 | 171 | return null; 172 | } 173 | } 174 | 175 | private async Task EnsureInitialized() 176 | { 177 | if (Interlocked.CompareExchange(ref this.initialized, 1, comparand: -1) == 0) 178 | { 179 | await this.Initialize(); 180 | } 181 | } 182 | 183 | private async Task Initialize() 184 | { 185 | await this.semaphoreSlim.WaitAsync(); 186 | 187 | try 188 | { 189 | if (this.initialized == 1) 190 | { 191 | return; 192 | } 193 | 194 | var d = new TraceLogDeserializer(this.filename); 195 | { 196 | var traceProcesses = d.TraceProcesses; 197 | var plist = new ProcessInfo[traceProcesses.Count + 1]; 198 | 199 | float totalmsec = 0; 200 | int i = 1; 201 | 202 | foreach (var traceProcess in traceProcesses.OrderByDescending(t => t.CPUMSec)) 203 | { 204 | totalmsec += traceProcess.CPUMSec; 205 | plist[i++] = new ProcessInfo(traceProcess.Name + $" ({traceProcess.ProcessID})", (int)traceProcess.ProcessIndex, traceProcess.CPUMSec, traceProcess.ProcessID, traceProcess.ParentID, traceProcess.CommandLine); 206 | } 207 | 208 | plist[0] = new ProcessInfo("All Processes", (int)ProcessIndex.Invalid, totalmsec, -1, -1, string.Empty); 209 | 210 | this.processList = plist; 211 | } 212 | 213 | { 214 | var eventStats = d.EventStats; 215 | 216 | var stackEventTypes = new StackEventTypeInfo[eventStats.Count]; 217 | 218 | int i = 0; 219 | foreach (var pair in d.EventStats.OrderBy(t => t.Value.FullName)) 220 | { 221 | stackEventTypes[i++] = new StackEventTypeInfo(pair.Key, pair.Value.FullName, pair.Value.Count, pair.Value.StackCount); 222 | } 223 | 224 | this.stackEventTypesOrderedByName = stackEventTypes; 225 | } 226 | 227 | { 228 | var eventStats = d.EventStats; 229 | 230 | var stackEventTypes = new StackEventTypeInfo[eventStats.Count + 1]; 231 | 232 | stackEventTypes[0] = new StackEventTypeInfo(0, "All Events", d.TotalEventCount, d.TotalStackCount); 233 | 234 | int i = 1; 235 | foreach (var pair in d.EventStats.OrderByDescending(t => t.Value.StackCount)) 236 | { 237 | stackEventTypes[i++] = new StackEventTypeInfo(pair.Key, pair.Value.FullName, pair.Value.Count, pair.Value.StackCount); 238 | } 239 | 240 | this.stackEventTypesOrderedByStackCount = stackEventTypes; 241 | } 242 | 243 | { 244 | var moduleFiles = d.TraceModuleFiles; 245 | var moduleInfos = new ModuleInfo[moduleFiles.Count]; 246 | 247 | int index = 0; 248 | foreach (var moduleFile in moduleFiles.OrderByDescending(t => t.CodeAddressesInModule)) 249 | { 250 | moduleInfos[index++] = new ModuleInfo((int)moduleFile.ModuleFileIndex, moduleFile.CodeAddressesInModule, moduleFile.FilePath); 251 | } 252 | 253 | this.moduleInfoList = moduleInfos; 254 | } 255 | 256 | this.traceInfo = d.TraceInfo; 257 | this.deserializer = d; 258 | this.initialized = 1; 259 | } 260 | finally 261 | { 262 | this.semaphoreSlim.Release(); 263 | } 264 | } 265 | 266 | private readonly struct StackSourceCacheKey : IEquatable 267 | { 268 | private const double TOLERANCE = 0.1; 269 | 270 | private readonly ProcessIndex processIndex; 271 | 272 | private readonly int stackType; 273 | 274 | private readonly double start; 275 | 276 | private readonly double end; 277 | 278 | private readonly string drillIntoKey; 279 | 280 | public StackSourceCacheKey(ProcessIndex processIndex, int stackType, double start, double end, string drillIntoKey) 281 | { 282 | this.processIndex = processIndex; 283 | this.stackType = stackType; 284 | this.start = start; 285 | this.end = end; 286 | this.drillIntoKey = drillIntoKey; 287 | } 288 | 289 | public bool Equals(StackSourceCacheKey other) 290 | { 291 | return this.processIndex == other.processIndex && this.stackType == other.stackType && this.drillIntoKey == other.drillIntoKey && Math.Abs(this.start - other.start) < TOLERANCE && Math.Abs(this.end - other.end) < TOLERANCE; 292 | } 293 | 294 | public override int GetHashCode() 295 | { 296 | return HashCode.Combine(this.processIndex, this.stackType, this.start, this.end, this.drillIntoKey); 297 | } 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/DeserializedDataCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Diagnostics.Tracing; 9 | using Microsoft.Extensions.Caching.Memory; 10 | 11 | public class DeserializedDataCache : IDeserializedDataCache 12 | { 13 | private readonly CallTreeDataCache cache; 14 | 15 | private readonly ICacheExpirationTimeProvider cacheExpirationTimeProvider; 16 | 17 | public DeserializedDataCache(CallTreeDataCache cache, ICacheExpirationTimeProvider cacheExpirationTimeProvider) 18 | { 19 | this.cache = cache; 20 | this.cacheExpirationTimeProvider = cacheExpirationTimeProvider; 21 | } 22 | 23 | public void ClearAllCacheEntries() 24 | { 25 | lock (this.cache) 26 | { 27 | this.cache.Compact(100); 28 | } 29 | } 30 | 31 | public IDeserializedData GetData(string cacheKey) 32 | { 33 | lock (this.cache) 34 | { 35 | if (!this.cache.TryGetValue(cacheKey, out IDeserializedData data)) 36 | { 37 | var cacheEntryOptions = new MemoryCacheEntryOptions().SetPriority(CacheItemPriority.NeverRemove).RegisterPostEvictionCallback(callback: EvictionCallback, state: this).SetSlidingExpiration(this.cacheExpirationTimeProvider.Expiration); 38 | data = new DeserializedData(cacheKey); 39 | this.cache.Set(cacheKey, data, cacheEntryOptions); 40 | CacheMonitorEventSource.Logger.CacheEntryAdded(Environment.MachineName, cacheKey); 41 | } 42 | 43 | return data; 44 | } 45 | } 46 | 47 | private static void EvictionCallback(object key, object value, EvictionReason reason, object state) 48 | { 49 | CacheMonitorEventSource.Logger.CacheEntryRemoved(Environment.MachineName, (string)key); 50 | } 51 | 52 | [EventSource(Guid = "203010e5-cae2-5761-b597-a757ae66787b")] 53 | private sealed class CacheMonitorEventSource : EventSource 54 | { 55 | public static CacheMonitorEventSource Logger { get; } = new CacheMonitorEventSource(); 56 | 57 | public void CacheEntryAdded(string source, string cacheKey) 58 | { 59 | this.WriteEvent(1, source, cacheKey); 60 | } 61 | 62 | public void CacheEntryRemoved(string source, string cacheKey) 63 | { 64 | this.WriteEvent(2, source, cacheKey); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/DetailedProcessInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | 9 | public sealed class DetailedProcessInfo 10 | { 11 | public DetailedProcessInfo(ProcessInfo processInfo, List threads, List modules) 12 | { 13 | this.ProcessInfo = processInfo; 14 | this.Threads = threads; 15 | this.Modules = modules; 16 | } 17 | 18 | public ProcessInfo ProcessInfo { get; } 19 | 20 | public List Threads { get; } 21 | 22 | public List Modules { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HttpApplication.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Hosting.Server; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Http.Features; 12 | 13 | internal sealed class HttpApplication : IHttpApplication 14 | { 15 | private readonly RequestDelegate application; 16 | 17 | public HttpApplication(RequestDelegate application) => this.application = application; 18 | 19 | public HttpContext CreateContext(IFeatureCollection contextFeatures) => new DefaultHttpContext(contextFeatures); 20 | 21 | public Task ProcessRequestAsync(HttpContext context) => this.application(context); 22 | 23 | public void DisposeContext(HttpContext context, Exception exception) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ICacheExpirationTimeProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | 9 | public interface ICacheExpirationTimeProvider 10 | { 11 | TimeSpan Expiration { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ICallTreeData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Diagnostics.Tracing.Stacks; 10 | 11 | public interface ICallTreeData 12 | { 13 | // gets a node without a context 14 | ValueTask GetNode(string name); 15 | 16 | // gets a node with a callee tree context and looks up the path 17 | ValueTask GetCalleeTreeNode(string name, string path = ""); 18 | 19 | // gets a list of nodes with no context 20 | ValueTask> GetSummaryTree(int numNodes); 21 | 22 | // returns a flat caller tree given a node 23 | ValueTask GetCallerTree(string name, char sep); 24 | 25 | // returns a flat caller tree given a node and its context 26 | ValueTask GetCallerTree(string name, char sep, string path); 27 | 28 | // returns a flat callee tree given a node and its context 29 | ValueTask GetCalleeTree(string name, string path); 30 | 31 | // returns samples for Drill Into 32 | ValueTask GetDrillIntoStackSource(bool exclusive, string name, char sep, string path); 33 | 34 | ValueTask Source(string authorizationHeader, string name, char sep, string path = ""); 35 | 36 | void UnInitialize(); 37 | 38 | string LookupWarmSymbols(int minCount); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/IDeserializedData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Diagnostics.Tracing.Stacks; 10 | 11 | public interface IDeserializedData 12 | { 13 | ValueTask GetStackEventTypesAsyncOrderedByName(); 14 | 15 | ValueTask GetStackEventTypesAsyncOrderedByStackCount(); 16 | 17 | ValueTask GetProcessChooserAsync(); 18 | 19 | ValueTask GetDetailedProcessInfoAsync(int processIndex); 20 | 21 | ValueTask GetCallTreeAsync(StackViewerModel model, StackSource stackSource = null); 22 | 23 | ValueTask> GetEvents(EventViewerModel model); 24 | 25 | ValueTask GetModulesAsync(); 26 | 27 | ValueTask GetTraceInfoAsync(); 28 | 29 | ValueTask LookupSymbolAsync(int moduleIndex); 30 | 31 | ValueTask LookupSymbolsAsync(int[] moduleIndices); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/IDeserializedDataCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | public interface IDeserializedDataCache 8 | { 9 | IDeserializedData GetData(string cacheKey); 10 | 11 | void ClearAllCacheEntries(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/KestrelServerOptionsConfig.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using Microsoft.AspNetCore.Server.Kestrel.Core; 8 | using Microsoft.Extensions.Options; 9 | 10 | internal sealed class KestrelServerOptionsConfig : IOptions 11 | { 12 | public KestrelServerOptionsConfig(int port) 13 | { 14 | this.Value = new KestrelServerOptions 15 | { 16 | AddServerHeader = false, 17 | AllowSynchronousIO = false, 18 | ApplicationServices = null, 19 | ConfigurationLoader = null, 20 | }; 21 | 22 | this.Value.ConfigureEndpointDefaults(options => 23 | { 24 | options.Protocols = HttpProtocols.Http1; 25 | }); 26 | 27 | this.Value.ListenAnyIP(port); 28 | } 29 | 30 | public KestrelServerOptions Value { get; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Logging/DefaultEventSourceLoggerFactory.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using Microsoft.Extensions.Logging; 9 | 10 | internal sealed class DefaultEventSourceLoggerFactory : ILoggerFactory 11 | { 12 | private readonly ILogger defaultLogger = new DefaultEventSourceLogger(); 13 | 14 | public void Dispose() 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | 19 | public ILogger CreateLogger(string categoryName) 20 | { 21 | return this.defaultLogger; 22 | } 23 | 24 | public void AddProvider(ILoggerProvider provider) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | private sealed class DefaultEventSourceLogger : ILogger 30 | { 31 | private readonly FakeDisposable fakeDisposable = new FakeDisposable(); 32 | 33 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 34 | { 35 | } 36 | 37 | public bool IsEnabled(LogLevel logLevel) 38 | { 39 | return false; 40 | } 41 | 42 | public IDisposable BeginScope(TState state) 43 | { 44 | return this.fakeDisposable; 45 | } 46 | 47 | private sealed class FakeDisposable : IDisposable 48 | { 49 | public void Dispose() 50 | { 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MemoryCacheOptionsConfig.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using Microsoft.Extensions.Internal; 10 | using Microsoft.Extensions.Options; 11 | 12 | internal sealed class MemoryCacheOptionsConfig : IOptions 13 | { 14 | public MemoryCacheOptionsConfig() 15 | { 16 | this.Value = new MemoryCacheOptions 17 | { 18 | CompactionPercentage = 0.9, 19 | ExpirationScanFrequency = TimeSpan.FromMinutes(60.0), 20 | Clock = new SystemClock(), 21 | }; 22 | } 23 | 24 | public MemoryCacheOptions Value { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/EventData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Runtime.Serialization; 8 | 9 | [DataContract] 10 | public sealed class EventData 11 | { 12 | [DataMember] 13 | public int EventIndex { get; set; } 14 | 15 | [DataMember] 16 | public bool HasStack { get; set; } 17 | 18 | [DataMember] 19 | public string Timestamp { get; set; } 20 | 21 | [DataMember] 22 | public string EventName { get; set; } 23 | 24 | [DataMember] 25 | public string ProcessName { get; set; } 26 | 27 | [DataMember] 28 | public string Rest { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/EventViewerModel.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Text; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.WebUtilities; 12 | 13 | public sealed class EventViewerModel 14 | { 15 | public EventViewerModel(string dataDirectoryListingRoot, IQueryCollection queryCollection) 16 | { 17 | var filename = (string)queryCollection["Filename"] ?? string.Empty; 18 | this.Filename = Path.Combine(dataDirectoryListingRoot, Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(filename))); 19 | 20 | var start = (string)queryCollection["Start"] ?? string.Empty; 21 | this.Start = string.IsNullOrEmpty(start) ? 0.0 : double.Parse(start); 22 | 23 | var end = (string)queryCollection["End"] ?? string.Empty; 24 | this.End = string.IsNullOrEmpty(end) ? 0.0 : double.Parse(end); 25 | 26 | var maxEventCount = (string)queryCollection["MaxEventCount"] ?? string.Empty; 27 | this.MaxEventCount = string.IsNullOrEmpty(maxEventCount) ? 10000 : int.Parse(maxEventCount); 28 | 29 | var textFilter = (string)queryCollection["Filter"] ?? string.Empty; 30 | this.TextFilter = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(textFilter)); 31 | 32 | var eventTypesString = (string)queryCollection["EventTypes"] ?? string.Empty; 33 | if (string.IsNullOrEmpty(eventTypesString)) 34 | { 35 | this.EventTypes = new HashSet(0); 36 | } 37 | else 38 | { 39 | var arr = ((string)queryCollection["EventTypes"] ?? string.Empty).Split(','); 40 | var eventTypes = new HashSet(arr.Length); 41 | foreach (var e in arr) 42 | { 43 | eventTypes.Add(int.Parse(e)); 44 | } 45 | 46 | this.EventTypes = eventTypes; 47 | } 48 | } 49 | 50 | public string Filename { get; } 51 | 52 | public HashSet EventTypes { get; } 53 | 54 | public double Start { get; } 55 | 56 | public double End { get; } 57 | 58 | public int MaxEventCount { get; } 59 | 60 | public string TextFilter { get; } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Models/LineInformation.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | public sealed class LineInformation 8 | { 9 | public int LineNumber { get; set; } 10 | 11 | public double Metric { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Models/SourceInformation.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | using System.Runtime.Serialization; 9 | 10 | [DataContract] 11 | public sealed class SourceInformation 12 | { 13 | [DataMember] 14 | public string Url { get; set; } 15 | 16 | [DataMember] 17 | public string Log { get; set; } 18 | 19 | [DataMember] 20 | public IEnumerable Summary { get; set; } 21 | 22 | [DataMember] 23 | public string Data { get; set; } 24 | 25 | [DataMember] 26 | public string BuildTimeFilePath { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Models/StackViewerModel.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.IO; 8 | using System.Text; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.WebUtilities; 11 | 12 | public sealed class StackViewerModel 13 | { 14 | public StackViewerModel(string dataDirectoryListingRoot, IQueryCollection queryCollection) 15 | { 16 | this.Filename = (string)queryCollection["Filename"] ?? string.Empty; 17 | this.StackType = (string)queryCollection["StackType"] ?? string.Empty; 18 | this.Pid = (string)queryCollection["Pid"] ?? string.Empty; 19 | this.Start = (string)queryCollection["Start"] ?? string.Empty; 20 | this.End = (string)queryCollection["End"] ?? string.Empty; 21 | this.GroupPats = (string)queryCollection["GroupPats"] ?? string.Empty; 22 | this.IncPats = (string)queryCollection["IncPats"] ?? string.Empty; 23 | this.ExcPats = (string)queryCollection["ExcPats"] ?? string.Empty; 24 | this.FoldPats = (string)queryCollection["FoldPats"] ?? string.Empty; 25 | this.FoldPct = (string)queryCollection["FoldPct"] ?? string.Empty; 26 | this.DrillIntoKey = (string)queryCollection["DrillIntoKey"] ?? string.Empty; 27 | 28 | this.Filename = Path.Combine(dataDirectoryListingRoot, Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.Filename))); 29 | this.Start = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.Start)); 30 | this.End = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.End)); 31 | this.GroupPats = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.GroupPats)); 32 | this.IncPats = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.IncPats)); 33 | this.FoldPats = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.FoldPats)); 34 | this.ExcPats = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(this.ExcPats)); 35 | } 36 | 37 | public string Filename { get; } 38 | 39 | public string Pid { get; } 40 | 41 | public string StackType { get; } 42 | 43 | public string Start { get; } 44 | 45 | public string End { get; } 46 | 47 | public string GroupPats { get; } 48 | 49 | public string IncPats { get; } 50 | 51 | public string ExcPats { get; } 52 | 53 | public string FoldPats { get; } 54 | 55 | public string FoldPct { get; } 56 | 57 | public string DrillIntoKey { get; private set; } 58 | 59 | public override string ToString() 60 | { 61 | return $"pid={this.Pid}&stacktype={this.StackType}&filename={this.Filename}&start={this.Start}&end={this.End}&grouppats={this.GroupPats}&incpats={this.IncPats}&excpats={this.ExcPats}&foldpats={this.FoldPats}&foldpct={this.FoldPct}&drillIntoKey={this.DrillIntoKey}"; 62 | } 63 | 64 | public override int GetHashCode() 65 | { 66 | unchecked 67 | { 68 | var hashCode = this.Filename != null ? this.Filename.GetHashCode() : 0; 69 | hashCode = (hashCode * 397) ^ (this.Pid != null ? this.Pid.GetHashCode() : 0); 70 | hashCode = (hashCode * 397) ^ (this.StackType != null ? this.StackType.GetHashCode() : 0); 71 | hashCode = (hashCode * 397) ^ (this.Start != null ? this.Start.GetHashCode() : 0); 72 | hashCode = (hashCode * 397) ^ (this.End != null ? this.End.GetHashCode() : 0); 73 | hashCode = (hashCode * 397) ^ (this.GroupPats != null ? this.GroupPats.GetHashCode() : 0); 74 | hashCode = (hashCode * 397) ^ (this.IncPats != null ? this.IncPats.GetHashCode() : 0); 75 | hashCode = (hashCode * 397) ^ (this.ExcPats != null ? this.ExcPats.GetHashCode() : 0); 76 | hashCode = (hashCode * 397) ^ (this.FoldPats != null ? this.FoldPats.GetHashCode() : 0); 77 | hashCode = (hashCode * 397) ^ (this.FoldPct != null ? this.FoldPct.GetHashCode() : 0); 78 | return hashCode; 79 | } 80 | } 81 | 82 | public override bool Equals(object obj) 83 | { 84 | if (obj is null) 85 | { 86 | return false; 87 | } 88 | 89 | if (ReferenceEquals(this, obj)) 90 | { 91 | return true; 92 | } 93 | 94 | if (obj.GetType() != this.GetType()) 95 | { 96 | return false; 97 | } 98 | 99 | return this.Equals((StackViewerModel)obj); 100 | } 101 | 102 | public void SetDrillIntoKey(string drillIntoKey) 103 | { 104 | this.DrillIntoKey = drillIntoKey; 105 | } 106 | 107 | private bool Equals(StackViewerModel other) 108 | { 109 | return string.Equals(this.Filename, other.Filename) && 110 | string.Equals(this.Pid, other.Pid) && 111 | string.Equals(this.StackType, other.StackType) && 112 | string.Equals(this.Start, other.Start) && 113 | string.Equals(this.End, other.End) && 114 | string.Equals(this.GroupPats, other.GroupPats) && 115 | string.Equals(this.IncPats, other.IncPats) && 116 | string.Equals(this.ExcPats, other.ExcPats) && 117 | string.Equals(this.FoldPats, other.FoldPats) && 118 | string.Equals(this.FoldPct, other.FoldPct) && 119 | string.Equals(this.DrillIntoKey, other.DrillIntoKey); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Models/TreeNode.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System.Collections.Generic; 8 | using System.Runtime.Serialization; 9 | using System.Text; 10 | using System.Text.Json.Serialization; 11 | using Microsoft.AspNetCore.WebUtilities; 12 | using Microsoft.Diagnostics.Tracing.Stacks; 13 | 14 | [DataContract] 15 | public class TreeNode 16 | { 17 | private readonly object lockObj = new object(); 18 | 19 | private readonly CallTreeNode backingNodeWithChildren; 20 | 21 | private TreeNode[] callees; 22 | 23 | public TreeNode(CallTreeNodeBase template) 24 | { 25 | if (template == null) 26 | { 27 | ThrowHelper.ThrowArgumentNullException(nameof(template)); 28 | } 29 | 30 | this.Base64EncodedId = Base64UrlTextEncoder.Encode(Encoding.UTF8.GetBytes(template.Name)); 31 | this.Path = string.Empty; 32 | this.Name = template.Name; 33 | this.InclusiveMetric = template.InclusiveMetric.ToString("N3"); 34 | this.InclusiveCount = template.InclusiveCount.ToString("N0"); 35 | this.ExclusiveMetric = template.ExclusiveMetric.ToString("N3"); 36 | this.ExclusiveCount = template.ExclusiveCount.ToString("N0"); 37 | this.ExclusiveFoldedMetric = template.ExclusiveFoldedMetric.ToString("N0"); 38 | this.ExclusiveFoldedCount = template.ExclusiveFoldedCount.ToString("N0"); 39 | this.InclusiveMetricByTimeString = template.InclusiveMetricByTimeString; 40 | this.FirstTimeRelativeMSec = template.FirstTimeRelativeMSec.ToString("N3"); 41 | this.LastTimeRelativeMSec = template.LastTimeRelativeMSec.ToString("N3"); 42 | this.InclusiveMetricPercent = (template.InclusiveMetric * 100 / template.CallTree.PercentageBasis).ToString("N2"); 43 | this.ExclusiveMetricPercent = (template.ExclusiveMetric * 100 / template.CallTree.PercentageBasis).ToString("N2"); 44 | this.ExclusiveFoldedMetricPercent = (template.ExclusiveFoldedMetric * 100 / template.CallTree.PercentageBasis).ToString("N2"); 45 | this.HasChildren = false; 46 | this.BackingNode = template; 47 | } 48 | 49 | public TreeNode(CallTreeNode template) 50 | { 51 | if (template == null) 52 | { 53 | ThrowHelper.ThrowArgumentNullException(nameof(template)); 54 | } 55 | 56 | this.Path = string.Empty; 57 | this.Base64EncodedId = Base64UrlTextEncoder.Encode(Encoding.UTF8.GetBytes(template.Name)); 58 | this.Name = template.Name; 59 | this.InclusiveMetric = template.InclusiveMetric.ToString("N3"); 60 | this.InclusiveCount = template.InclusiveCount.ToString("N0"); 61 | this.ExclusiveMetric = template.ExclusiveMetric.ToString("N3"); 62 | this.ExclusiveCount = template.ExclusiveCount.ToString("N0"); 63 | this.ExclusiveFoldedMetric = template.ExclusiveFoldedMetric.ToString("N0"); 64 | this.ExclusiveFoldedCount = template.ExclusiveFoldedCount.ToString("N0"); 65 | this.InclusiveMetricByTimeString = template.InclusiveMetricByTimeString; 66 | this.FirstTimeRelativeMSec = template.FirstTimeRelativeMSec.ToString("N3"); 67 | this.LastTimeRelativeMSec = template.LastTimeRelativeMSec.ToString("N3"); 68 | this.InclusiveMetricPercent = (template.InclusiveMetric * 100 / template.CallTree.PercentageBasis).ToString("N2"); 69 | this.ExclusiveMetricPercent = (template.ExclusiveMetric * 100 / template.CallTree.PercentageBasis).ToString("N2"); 70 | this.ExclusiveFoldedMetricPercent = (template.ExclusiveFoldedMetric * 100 / template.CallTree.PercentageBasis).ToString("N2"); 71 | this.HasChildren = template.HasChildren; 72 | this.BackingNode = template; 73 | this.backingNodeWithChildren = template; 74 | } 75 | 76 | internal TreeNode() 77 | { 78 | } 79 | 80 | [JsonIgnore] 81 | public CallTreeNodeBase BackingNode { get; } 82 | 83 | [DataMember] 84 | public string Path { get; set; } 85 | 86 | [DataMember] 87 | public string Base64EncodedId { get; set; } 88 | 89 | [DataMember] 90 | public string Name { get; set; } 91 | 92 | [DataMember] 93 | public string InclusiveMetric { get; set; } 94 | 95 | [DataMember] 96 | public string ExclusiveMetric { get; set; } 97 | 98 | [DataMember] 99 | public string ExclusiveFoldedMetric { get; set; } 100 | 101 | [DataMember] 102 | public string InclusiveCount { get; set; } 103 | 104 | [DataMember] 105 | public string ExclusiveCount { get; set; } 106 | 107 | [DataMember] 108 | public string ExclusiveFoldedCount { get; set; } 109 | 110 | [DataMember] 111 | public string InclusiveMetricPercent { get; set; } 112 | 113 | [DataMember] 114 | public string ExclusiveMetricPercent { get; set; } 115 | 116 | [DataMember] 117 | public string ExclusiveFoldedMetricPercent { get; set; } 118 | 119 | [DataMember] 120 | public string InclusiveMetricByTimeString { get; set; } 121 | 122 | [DataMember] 123 | public string FirstTimeRelativeMSec { get; set; } 124 | 125 | [DataMember] 126 | public string LastTimeRelativeMSec { get; set; } 127 | 128 | [DataMember] 129 | public double DurationMSec { get; set; } 130 | 131 | [DataMember] 132 | public bool HasChildren { get; set; } 133 | 134 | [JsonIgnore] 135 | public TreeNode[] Children 136 | { 137 | get 138 | { 139 | lock (this.lockObj) 140 | { 141 | if (this.callees == null && this.HasChildren) 142 | { 143 | IList backingNodeCallees = this.backingNodeWithChildren.Callees; 144 | int count = backingNodeCallees.Count; 145 | this.callees = new TreeNode[count]; 146 | 147 | for (int i = 0; i < count; ++i) 148 | { 149 | this.callees[i] = new TreeNode(backingNodeCallees[i]) 150 | { 151 | Path = string.IsNullOrEmpty(this.Path) ? i.ToString() : this.Path + '-' + i, 152 | }; // for example, 7105/0 .. 7105/N 153 | } 154 | } 155 | 156 | return this.callees; 157 | } 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/ModuleInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | 9 | public sealed class ModuleInfo : IComparable 10 | { 11 | public ModuleInfo(int moduleIndex, int addressesInModule, string modulePath) 12 | { 13 | this.Id = moduleIndex; 14 | this.AddrCount = addressesInModule; 15 | this.ModulePath = modulePath; 16 | } 17 | 18 | public int Id { get; } 19 | 20 | public int AddrCount { get; } 21 | 22 | public string ModulePath { get; } 23 | 24 | public int CompareTo(ModuleInfo other) 25 | { 26 | if (this.AddrCount > other.AddrCount) 27 | { 28 | return -1; 29 | } 30 | else if (this.AddrCount < other.AddrCount) 31 | { 32 | return 1; 33 | } 34 | else 35 | { 36 | return 0; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/PerfViewJS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 8.0 6 | true 7 | false 8 | true 9 | embedded 10 | true 11 | Latest 12 | false 13 | spa\ 14 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Always 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | %(DistFiles.Identity) 57 | PreserveNewest 58 | true 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/PerfViewJS.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | PerfViewJS 4 | 1.0.0 5 | PerfViewJS 6 | Microsoft 7 | true 8 | http://go.microsoft.com/fwlink/?LinkId=329770 9 | https://github.com/microsoft/perfview/tree/main/src/PerfViewJS 10 | PerfView SPA 11 | (c) Microsoft Corporation. All rights reserved. 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ProcessInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | public sealed class ProcessInfo 8 | { 9 | public ProcessInfo(string processName, int index, float cpumsec, int processId, int parentid, string commandline) 10 | { 11 | this.Name = processName; 12 | this.Id = index; 13 | this.ProcessId = processId; 14 | this.CPUMSec = cpumsec; 15 | this.ParentId = parentid; 16 | this.CommandLine = commandline; 17 | } 18 | 19 | public string Name { get; } 20 | 21 | public int Id { get; } 22 | 23 | public int ProcessId { get; } 24 | 25 | public int ParentId { get; } 26 | 27 | public string CommandLine { get; } 28 | 29 | public float CPUMSec { get; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.IO; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.Server.Kestrel.Core; 12 | using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; 13 | 14 | public static class Program 15 | { 16 | public static async Task Main(string[] args) 17 | { 18 | TaskScheduler.UnobservedTaskException += (sender, e) => 19 | { 20 | Console.WriteLine("Unobserved exception: {0}", e.Exception); 21 | }; 22 | 23 | if (args.Length != 2) 24 | { 25 | Console.WriteLine("Usage: PerfViewJS portNumber DataRoot"); 26 | return; 27 | } 28 | 29 | string defaultAuthorizationHeaderForSourceLink = Environment.GetEnvironmentVariable("PerfViewJS_DefaultAuthorizationHeaderForSourceLink"); 30 | 31 | var defaultEventSourceLoggerFactory = new DefaultEventSourceLoggerFactory(); 32 | var startup = new Startup(Directory.GetCurrentDirectory(), args[1], defaultAuthorizationHeaderForSourceLink); 33 | 34 | var server = new KestrelServer(new KestrelServerOptionsConfig(int.Parse(args[0])), new SocketTransportFactory(new SocketTransportOptionsConfig(), defaultEventSourceLoggerFactory), defaultEventSourceLoggerFactory); 35 | 36 | await server.StartAsync(new HttpApplication(startup.HandleRequest), CancellationToken.None); 37 | 38 | Thread.Sleep(Timeout.Infinite); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "PerfViewJS": { 4 | "commandName": "Project", 5 | "commandLineArgs": "5000 FixMePath", 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:5000" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/SocketTransportOptionsConfig.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; 8 | using Microsoft.Extensions.Options; 9 | 10 | internal sealed class SocketTransportOptionsConfig : IOptions 11 | { 12 | public SocketTransportOptionsConfig() 13 | { 14 | this.Value = new SocketTransportOptions(); 15 | } 16 | 17 | public SocketTransportOptions Value { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/StackEventTypeInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | public sealed class StackEventTypeInfo 8 | { 9 | public StackEventTypeInfo(int eventId, string eventName, int eventCount, int stackEventCount) 10 | { 11 | this.EventId = eventId; 12 | this.EventName = eventName; 13 | this.EventCount = eventCount; 14 | this.StackEventCount = stackEventCount; 15 | } 16 | 17 | public int EventId { get; } 18 | 19 | public string EventName { get; } 20 | 21 | public int EventCount { get; } 22 | 23 | public int StackEventCount { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Startup.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Buffers; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.IO.Compression; 12 | using System.Linq; 13 | using System.Runtime.CompilerServices; 14 | using System.Text; 15 | using System.Text.Json; 16 | using System.Threading; 17 | using System.Threading.Tasks; 18 | using Microsoft.AspNetCore.Http; 19 | 20 | public sealed class Startup 21 | { 22 | private const string JsonContentType = "application/json; charset=UTF-8"; 23 | 24 | private const string JavaScriptContentType = "application/javascript; charset=UTF-8"; 25 | 26 | private const string CSSContentType = "text/css; charset=UTF-8"; 27 | 28 | private const string HTMLContentType = "text/html; charset=UTF-8"; 29 | 30 | private const string AcceptEncoding = "Accept-Encoding"; 31 | 32 | private const string ContentEncoding = "Content-Encoding"; 33 | 34 | private const string Brotli = "br"; 35 | 36 | private const string GZip = "gzip"; 37 | 38 | private const string JSExtension = ".js"; 39 | 40 | private const string CSSExtension = ".css"; 41 | 42 | private const string HTMLExtension = ".html"; 43 | 44 | private readonly HashSet perfviewJSSupportedFileExtensions = new HashSet { "*.etl", "*.btl", "*.netperf", "*.nettrace" }; 45 | 46 | private readonly JsonSerializerOptions jsonSerializerSettings = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; 47 | 48 | private readonly DeserializedDataCache deserializedDataCache = new DeserializedDataCache(new CallTreeDataCache(new MemoryCacheOptionsConfig()), new CacheExpirationTimeProvider()); 49 | 50 | private readonly string contentRoot; 51 | 52 | private readonly string indexFile; 53 | 54 | private readonly string dataDirectoryListingRoot; 55 | 56 | private readonly string defaultAuthorizationHeader; 57 | 58 | public Startup(string contentRootPath, string dataDirectoryListingRoot, string defaultAuthorizationHeader) 59 | { 60 | this.contentRoot = Path.Combine(contentRootPath, "spa", "build"); 61 | this.indexFile = Path.GetFullPath(Path.Join(this.contentRoot, "index.html")); 62 | this.dataDirectoryListingRoot = Path.GetFullPath(dataDirectoryListingRoot); 63 | this.defaultAuthorizationHeader = defaultAuthorizationHeader; 64 | } 65 | 66 | internal enum Compression 67 | { 68 | /// 69 | /// Brotli compression 70 | /// 71 | Brotli, 72 | 73 | /// 74 | /// GZip compression 75 | /// 76 | GZip, 77 | 78 | /// 79 | /// No compression 80 | /// 81 | None, 82 | } 83 | 84 | public Task HandleRequest(HttpContext context) 85 | { 86 | var request = context.Request; 87 | var acceptEncoding = request.Headers[AcceptEncoding].ToString() ?? string.Empty; 88 | 89 | return this.HandleRequestInner(request.Path.Value, request.Query, context.Response, context.RequestAborted, acceptEncoding.Contains(Brotli) ? Compression.Brotli : acceptEncoding.Contains(GZip) ? Compression.GZip : Compression.None); 90 | } 91 | 92 | internal async Task HandleRequestInner(string requestPath, IQueryCollection queryCollection, HttpResponse response, CancellationToken requestAborted, Compression compression) 93 | { 94 | if (requestPath.StartsWith("/api")) 95 | { 96 | if (requestPath.StartsWith(@"/api/eventdata")) 97 | { 98 | var controller = new EventViewerController(this.deserializedDataCache, new EventViewerModel(this.dataDirectoryListingRoot, queryCollection)); 99 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.EventsAPI(), compression); 100 | } 101 | else 102 | { 103 | var controller = new StackViewerController(this.deserializedDataCache, new StackViewerModel(this.dataDirectoryListingRoot, queryCollection)); 104 | response.ContentType = JsonContentType; 105 | 106 | if (requestPath.StartsWith(@"/api/callerchildren")) 107 | { 108 | var name = (string)queryCollection["name"] ?? string.Empty; 109 | var path = (string)queryCollection["path"] ?? string.Empty; 110 | 111 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.CallerChildrenAPI(name, path), compression); 112 | } 113 | else if (requestPath.StartsWith(@"/api/treenode")) 114 | { 115 | var name = (string)queryCollection["name"] ?? string.Empty; 116 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.TreeNodeAPI(name), compression); 117 | } 118 | else if (requestPath.StartsWith(@"/api/hotspots")) 119 | { 120 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.HotspotsAPI(), compression); 121 | } 122 | else if (requestPath.StartsWith(@"/api/eventliston")) 123 | { 124 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.EventListAPIOrderedByName(), compression); 125 | } 126 | else if (requestPath.StartsWith(@"/api/eventlistos")) 127 | { 128 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.EventListAPIOrderedByStackCount(), compression); 129 | } 130 | else if (requestPath.StartsWith(@"/api/processchooser")) 131 | { 132 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.ProcessChooserAPI(), compression); 133 | } 134 | else if (requestPath.StartsWith(@"/api/modulelist")) 135 | { 136 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.GetModulesAPI(), compression); 137 | } 138 | else if (requestPath.StartsWith(@"/api/traceinfo")) 139 | { 140 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.GetTraceInfoAPI(), compression); 141 | } 142 | else if (requestPath.StartsWith(@"/api/drillinto")) 143 | { 144 | bool exclusive = requestPath.StartsWith(@"/api/drillinto/exclusive"); 145 | 146 | var name = (string)queryCollection["name"] ?? string.Empty; 147 | var path = (string)queryCollection["path"] ?? string.Empty; 148 | 149 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.DrillIntoAPI(exclusive, name, path), compression); 150 | } 151 | else if (requestPath.StartsWith(@"/api/processinfo")) 152 | { 153 | var processIndexString = (string)queryCollection["processIndex"] ?? string.Empty; 154 | var processIndex = -1; 155 | if (!string.IsNullOrEmpty(processIndexString)) 156 | { 157 | int.TryParse(processIndexString, out processIndex); 158 | } 159 | 160 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.DetailedProcessInfoAPI(processIndex), compression); 161 | } 162 | else if (requestPath.StartsWith(@"/api/lookupwarmsymbols")) 163 | { 164 | var minCountString = (string)queryCollection["minCount"] ?? string.Empty; 165 | var minCount = 50; 166 | if (!string.IsNullOrEmpty(minCountString)) 167 | { 168 | int.TryParse(minCountString, out minCount); 169 | } 170 | 171 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.LookupWarmSymbolsAPI(minCount), compression); 172 | } 173 | else if (requestPath.StartsWith(@"/api/lookupsymbol")) 174 | { 175 | var moduleIndexString = (string)queryCollection["moduleIndex"] ?? string.Empty; 176 | var moduleIndex = -1; 177 | if (!string.IsNullOrEmpty(moduleIndexString)) 178 | { 179 | int.TryParse(moduleIndexString, out moduleIndex); 180 | } 181 | 182 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.LookupSymbolAPI(moduleIndex), compression); 183 | } 184 | else if (requestPath.StartsWith(@"/api/lookupymbols")) 185 | { 186 | var moduleIndicesString = (string)queryCollection["moduleIndices"] ?? string.Empty; 187 | int[] moduleIndices = null; 188 | if (!string.IsNullOrEmpty(moduleIndicesString)) 189 | { 190 | var split = moduleIndicesString.Split(','); 191 | moduleIndices = new int[split.Length]; 192 | for (int i = 0; i < split.Length; ++i) 193 | { 194 | moduleIndices[i] = int.Parse(split[i]); 195 | } 196 | } 197 | 198 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.LookupSymbolsAPI(moduleIndices), compression); 199 | } 200 | else if (requestPath.StartsWith("/api/datadirectorylisting")) 201 | { 202 | if (string.IsNullOrEmpty(this.dataDirectoryListingRoot)) 203 | { 204 | await WriteJsonResponse(response, this.jsonSerializerSettings, new[] { "PerfViewJS_DataRoot not set" }, compression); 205 | } 206 | else 207 | { 208 | var list = new List(); 209 | foreach (var item in this.perfviewJSSupportedFileExtensions) 210 | { 211 | var files = Directory.EnumerateFiles(this.dataDirectoryListingRoot, item).OrderByDescending(t => t); 212 | foreach (var file in files) 213 | { 214 | list.Add(Path.GetFileName(file)); 215 | } 216 | } 217 | 218 | await WriteJsonResponse(response, this.jsonSerializerSettings, list, compression); 219 | } 220 | } 221 | else if (requestPath.StartsWith("/api/getsource")) 222 | { 223 | var name = (string)queryCollection["name"] ?? string.Empty; 224 | var path = (string)queryCollection["path"] ?? string.Empty; 225 | var authorizationHeader = (string)queryCollection["authorizationHeader"] ?? this.defaultAuthorizationHeader; 226 | 227 | await WriteJsonResponse(response, this.jsonSerializerSettings, await controller.GetSourceAPI(name, path, authorizationHeader), compression); 228 | } 229 | } 230 | } 231 | else if (requestPath.StartsWith("/ui")) 232 | { 233 | await SendIndexFile(response, requestAborted, this.indexFile, compression); 234 | } 235 | else 236 | { 237 | var fullPath = Path.GetFullPath(Path.Join(this.contentRoot.AsSpan(), requestPath.AsSpan(1))); 238 | if (fullPath.StartsWith(this.contentRoot) && File.Exists(fullPath)) 239 | { 240 | response.Headers["Cache-Control"] = "public, max-age=31536000"; 241 | 242 | var ext = Path.GetExtension(fullPath); 243 | if (ext.EndsWith(JSExtension)) 244 | { 245 | response.ContentType = JavaScriptContentType; 246 | } 247 | else if (ext.EndsWith(CSSExtension)) 248 | { 249 | response.ContentType = CSSContentType; 250 | } 251 | else if (ext.EndsWith(HTMLExtension)) 252 | { 253 | response.ContentType = HTMLContentType; 254 | } 255 | 256 | await SendPotentiallyCompressedFileAsync(response, requestAborted, fullPath, compression); 257 | } 258 | else 259 | { 260 | if (requestPath.Equals("/")) 261 | { 262 | await SendIndexFile(response, requestAborted, this.indexFile, compression); 263 | } 264 | else 265 | { 266 | response.StatusCode = 404; 267 | await response.WriteAsync("404 Not Found", requestAborted); 268 | } 269 | } 270 | } 271 | } 272 | 273 | private static async Task SendPotentiallyCompressedFileAsync(HttpResponse response, CancellationToken requestAborted, string file, Compression compression) 274 | { 275 | switch (compression) 276 | { 277 | case Compression.Brotli: 278 | await SendCompressedOrUncompressedFileAsync(file, Brotli, response, requestAborted); 279 | break; 280 | case Compression.GZip: 281 | await SendCompressedOrUncompressedFileAsync(file, GZip, response, requestAborted); 282 | break; 283 | default: 284 | await SendFileAsync(file, response, requestAborted); 285 | break; 286 | } 287 | } 288 | 289 | private static async Task SendIndexFile(HttpResponse response, CancellationToken requestAborted, string indexFile, Compression compression) 290 | { 291 | response.ContentType = HTMLContentType; 292 | await SendPotentiallyCompressedFileAsync(response, requestAborted, indexFile, compression); 293 | } 294 | 295 | private static async Task WriteJsonResponse(HttpResponse response, JsonSerializerOptions settings, T data, Compression compression) 296 | { 297 | var jsonUtf8Bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data, settings)); 298 | 299 | switch (compression) 300 | { 301 | case Compression.Brotli: 302 | await WriteBrotliCompressedDynamicResponse(jsonUtf8Bytes, response); 303 | break; 304 | case Compression.GZip: 305 | await WriteGZipCompressedDynamicResponse(jsonUtf8Bytes, response); 306 | break; 307 | default: 308 | response.ContentLength = jsonUtf8Bytes.Length; 309 | await response.Body.WriteAsync(jsonUtf8Bytes); 310 | break; 311 | } 312 | } 313 | 314 | private static async Task WriteGZipCompressedDynamicResponse(byte[] input, HttpResponse response) 315 | { 316 | response.ContentLength = input.Length; 317 | response.Headers[ContentEncoding] = GZip; 318 | await using var gz = new GZipStream(response.Body, CompressionLevel.Fastest); 319 | await gz.WriteAsync(input, 0, input.Length); 320 | } 321 | 322 | private static Task WriteBrotliCompressedDynamicResponse(ReadOnlySpan input, HttpResponse response) 323 | { 324 | byte[] output = null; 325 | var arrayPool = ArrayPool.Shared; 326 | 327 | try 328 | { 329 | output = arrayPool.Rent(BrotliEncoder.GetMaxCompressedLength(input.Length)); 330 | if (BrotliEncoder.TryCompress(input, output, out var bytesWritten, 4, 22)) 331 | { 332 | response.ContentLength = bytesWritten; 333 | response.Headers[ContentEncoding] = Brotli; 334 | return response.Body.WriteAsync(output, 0, bytesWritten); 335 | } 336 | else 337 | { 338 | return TryCompressFalse(); 339 | } 340 | } 341 | finally 342 | { 343 | if (output != null) 344 | { 345 | arrayPool.Return(output); 346 | } 347 | } 348 | } 349 | 350 | private static async Task SendFileAsync(string file, HttpResponse response, CancellationToken cancellationToken) 351 | { 352 | await using var fs = new FileStream(file, FileMode.Open, FileAccess.ReadWrite); 353 | long remainingBytes = fs.Length; 354 | response.ContentLength = remainingBytes; 355 | 356 | byte[] buffer = null; 357 | var arrayPool = ArrayPool.Shared; 358 | 359 | try 360 | { 361 | buffer = arrayPool.Rent(81920); 362 | 363 | int bytesRead; 364 | 365 | while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) 366 | { 367 | int min = (int)Math.Min(remainingBytes, bytesRead); 368 | await response.Body.WriteAsync(buffer, 0, min, cancellationToken).ConfigureAwait(false); 369 | remainingBytes -= min; 370 | 371 | if (remainingBytes == 0) 372 | { 373 | break; 374 | } 375 | } 376 | } 377 | finally 378 | { 379 | if (buffer != null) 380 | { 381 | arrayPool.Return(buffer); 382 | } 383 | } 384 | } 385 | 386 | private static async Task SendCompressedOrUncompressedFileAsync(string file, string compressionExtension, HttpResponse response, CancellationToken requestAborted) 387 | { 388 | var compressedFile = file + "." + compressionExtension; 389 | if (File.Exists(compressedFile)) 390 | { 391 | response.Headers[ContentEncoding] = compressionExtension; 392 | await SendFileAsync(compressedFile, response, requestAborted); 393 | } 394 | else 395 | { 396 | await SendFileAsync(file, response, requestAborted); 397 | } 398 | } 399 | 400 | [MethodImpl(MethodImplOptions.NoInlining)] 401 | private static Task TryCompressFalse() 402 | { 403 | throw new Exception("TryCompress returned false."); 404 | } 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/ThreadInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | 9 | public sealed class ThreadInfo 10 | { 11 | public ThreadInfo(int threadId, int threadIndex, double cpumsec, DateTime startTime, double startTimeRelativeMSec, DateTime endTime, double endTimeRelativeMSec) 12 | { 13 | this.ThreadId = threadId; 14 | this.ThreadIndex = threadIndex; 15 | this.CPUMsec = cpumsec; 16 | this.StartTime = startTime; 17 | this.StartTimeRelativeMSec = startTimeRelativeMSec; 18 | this.EndTime = endTime; 19 | this.EndTimeRelativeMSec = endTimeRelativeMSec; 20 | } 21 | 22 | public int ThreadId { get; } 23 | 24 | public int ThreadIndex { get; } 25 | 26 | public DateTime StartTime { get; } 27 | 28 | public double StartTimeRelativeMSec { get; } 29 | 30 | public DateTime EndTime { get; } 31 | 32 | public double EndTimeRelativeMSec { get; } 33 | 34 | public double CPUMsec { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Runtime.CompilerServices; 9 | 10 | internal static class ThrowHelper 11 | { 12 | [MethodImpl(MethodImplOptions.NoInlining)] 13 | internal static void ThrowArgumentNullException(string argument) 14 | { 15 | throw new ArgumentNullException(argument); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/TraceEventStackSourceExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using Microsoft.Diagnostics.Tracing; 9 | using Microsoft.Diagnostics.Tracing.Etlx; 10 | using Microsoft.Diagnostics.Tracing.Parsers.Kernel; 11 | using Microsoft.Diagnostics.Tracing.Stacks; 12 | 13 | internal static class TraceEventStackSourceExtensions 14 | { 15 | public static StackSource CPUStacks(this TraceEvents events, TraceProcess process = null, Predicate predicate = null) 16 | { 17 | // optimization only 18 | if (process != null) 19 | { 20 | var start = Math.Max(events.StartTimeRelativeMSec, process.StartTimeRelativeMsec); 21 | var end = Math.Min(events.EndTimeRelativeMSec, process.EndTimeRelativeMsec); 22 | events = events.FilterByTime(start, end); 23 | events = events.Filter(x => (predicate == null || predicate(x)) && x is SampledProfileTraceData && x.ProcessID == process.ProcessID); 24 | } 25 | else 26 | { 27 | events = events.Filter(x => (predicate == null || predicate(x)) && x is SampledProfileTraceData && x.ProcessID != 0); // TODO: Is it really correc that x.ProcessID != 0 should be there? What if we want see these? 28 | } 29 | 30 | var traceStackSource = new TraceEventStackSource(events) { ShowUnknownAddresses = true }; 31 | 32 | return CopyStackSource.Clone(traceStackSource); 33 | } 34 | 35 | public static StackSource SingleEventTypeStack(this TraceEvents events, TraceProcess process = null, Predicate predicate = null) 36 | { 37 | // optimization only 38 | if (process != null) 39 | { 40 | var start = Math.Max(events.StartTimeRelativeMSec, process.StartTimeRelativeMsec); 41 | var end = Math.Min(events.EndTimeRelativeMSec, process.EndTimeRelativeMsec); 42 | events = events.FilterByTime(start, end); 43 | events = events.Filter(x => (predicate == null || predicate(x)) && x.ProcessID == process.ProcessID); 44 | } 45 | else 46 | { 47 | events = events.Filter(x => (predicate == null || predicate(x)) && x.ProcessID != 0); // TODO: Is it really correc that x.ProcessID != 0 should be there? What if we want see these? 48 | } 49 | 50 | var traceStackSource = new TraceEventStackSource(events) { ShowUnknownAddresses = true }; 51 | 52 | return CopyStackSource.Clone(traceStackSource); 53 | } 54 | 55 | public static StackSource AnyStacks(this TraceEvents events, TraceProcess process = null, Predicate predicate = null) 56 | { 57 | // optimization only 58 | if (process != null) 59 | { 60 | var start = Math.Max(events.StartTimeRelativeMSec, process.StartTimeRelativeMsec); 61 | var end = Math.Min(events.EndTimeRelativeMSec, process.EndTimeRelativeMsec); 62 | events = events.FilterByTime(start, end); 63 | events = events.Filter(x => (predicate == null || predicate(x)) && x.ProcessID == process.ProcessID); 64 | } 65 | else 66 | { 67 | events = events.Filter(x => (predicate == null || predicate(x)) && x.ProcessID != 0); // TODO: Is it really correc that x.ProcessID != 0 should be there? What if we want see these? 68 | } 69 | 70 | var stackSource = new MutableTraceEventStackSource(events.Log) { ShowUnknownAddresses = true }; 71 | var sample = new StackSourceSample(stackSource); 72 | var eventSource = events.GetSource(); 73 | 74 | eventSource.AllEvents += data => 75 | { 76 | var callStackIdx = data.CallStackIndex(); 77 | StackSourceCallStackIndex stackIndex = callStackIdx != CallStackIndex.Invalid ? stackSource.GetCallStack(callStackIdx, data) : StackSourceCallStackIndex.Invalid; 78 | 79 | var eventNodeName = "Event " + data.ProviderName + "/" + data.EventName; 80 | stackIndex = stackSource.Interner.CallStackIntern(stackSource.Interner.FrameIntern(eventNodeName), stackIndex); 81 | sample.StackIndex = stackIndex; 82 | sample.TimeRelativeMSec = data.TimeStampRelativeMSec; 83 | sample.Metric = 1; 84 | stackSource.AddSample(sample); 85 | }; 86 | 87 | eventSource.Process(); 88 | stackSource.DoneAddingSamples(); 89 | 90 | return stackSource; 91 | } 92 | 93 | public static StackSource Exceptions(this TraceEvents events, TraceProcess process = null, Predicate predicate = null) 94 | { 95 | // optimization only 96 | if (process != null) 97 | { 98 | var start = Math.Max(events.StartTimeRelativeMSec, process.StartTimeRelativeMsec); 99 | var end = Math.Min(events.EndTimeRelativeMSec, process.EndTimeRelativeMsec); 100 | events = events.FilterByTime(start, end); 101 | events = events.Filter(x => (predicate == null || predicate(x)) && x.ProcessID == process.ProcessID); 102 | } 103 | else 104 | { 105 | events = events.Filter(x => (predicate == null || predicate(x)) && x.ProcessID != 0); // TODO: Is it really correc that x.ProcessID != 0 should be there? What if we want see these? 106 | } 107 | 108 | var eventSource = events.GetSource(); 109 | var stackSource = new MutableTraceEventStackSource(events.Log) { ShowUnknownAddresses = true }; 110 | var sample = new StackSourceSample(stackSource); 111 | 112 | eventSource.Clr.ExceptionStart += data => 113 | { 114 | sample.Metric = 1; 115 | sample.TimeRelativeMSec = data.TimeStampRelativeMSec; 116 | 117 | // Create a call stack that ends with the 'throw' 118 | var nodeName = "Throw(" + data.ExceptionType + ") " + data.ExceptionMessage; 119 | var nodeIndex = stackSource.Interner.FrameIntern(nodeName); 120 | sample.StackIndex = stackSource.Interner.CallStackIntern(nodeIndex, stackSource.GetCallStack(data.CallStackIndex(), data)); 121 | stackSource.AddSample(sample); 122 | }; 123 | 124 | eventSource.Kernel.MemoryAccessViolation += data => 125 | { 126 | sample.Metric = 1; 127 | sample.TimeRelativeMSec = data.TimeStampRelativeMSec; 128 | 129 | // Create a call stack that ends with the 'throw' 130 | var nodeName = "AccessViolation(ADDR=" + data.VirtualAddress.ToString("x") + ")"; 131 | var nodeIndex = stackSource.Interner.FrameIntern(nodeName); 132 | sample.StackIndex = stackSource.Interner.CallStackIntern(nodeIndex, stackSource.GetCallStack(data.CallStackIndex(), data)); 133 | stackSource.AddSample(sample); 134 | }; 135 | 136 | eventSource.Process(); 137 | 138 | stackSource.DoneAddingSamples(); 139 | 140 | return stackSource; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/TraceInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | 9 | public sealed class TraceInfo 10 | { 11 | public TraceInfo(string machineName, string osname, string osbuild, int? utoffset, TimeSpan currentOffset, DateTime bootTime, DateTime startTime, DateTime endTime, double endTimeRelativeMSec, TimeSpan duration, int cpuspeed, int numberOfProcs, int memorySize, int pointerSize, TimeSpan profileInt, int eventCount, int lostEvents, long filesize) 12 | { 13 | this.MachineName = machineName; 14 | this.OperatingSystemName = osname; 15 | this.OperatingSystemBuildNumber = osbuild; 16 | this.UTCDiff = utoffset; 17 | this.UTCOffsetCurrentProcess = currentOffset.TotalMinutes; 18 | this.BootTime = bootTime; 19 | this.StartTime = startTime; 20 | this.EndTime = endTime; 21 | this.EndTimeRelativeMSec = endTimeRelativeMSec.ToString("F3"); 22 | this.Duration = duration.TotalSeconds; 23 | this.ProcessorSpeed = cpuspeed; 24 | this.NumberOfProcessors = numberOfProcs; 25 | this.MemorySize = memorySize; 26 | this.PointerSize = pointerSize; 27 | this.SampleProfileInterval = profileInt.TotalMilliseconds; 28 | this.TotalEvents = eventCount; 29 | this.LostEvents = lostEvents; 30 | this.FileSize = filesize / 1024.0 / 1024.0; 31 | } 32 | 33 | public string MachineName { get; } 34 | 35 | public string OperatingSystemName { get; } 36 | 37 | public string OperatingSystemBuildNumber { get; } 38 | 39 | public int? UTCDiff { get; } 40 | 41 | public double UTCOffsetCurrentProcess { get; } 42 | 43 | public DateTime BootTime { get; } 44 | 45 | public DateTime StartTime { get; } 46 | 47 | public DateTime EndTime { get; } 48 | 49 | public string EndTimeRelativeMSec { get; } 50 | 51 | public double Duration { get; } 52 | 53 | public int ProcessorSpeed { get; } 54 | 55 | public int NumberOfProcessors { get; } 56 | 57 | public int MemorySize { get; } 58 | 59 | public int PointerSize { get; } 60 | 61 | public double SampleProfileInterval { get; } 62 | 63 | public int TotalEvents { get; } 64 | 65 | public int LostEvents { get; } 66 | 67 | public double FileSize { get; } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/TraceLogDeserializer.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft. All rights reserved. 3 | // 4 | 5 | namespace PerfViewJS 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Globalization; 10 | using System.IO; 11 | using System.Text; 12 | using System.Text.RegularExpressions; 13 | using Microsoft.Diagnostics.Symbols; 14 | using Microsoft.Diagnostics.Tracing; 15 | using Microsoft.Diagnostics.Tracing.Etlx; 16 | using Microsoft.Diagnostics.Tracing.Stacks; 17 | 18 | public sealed class TraceLogDeserializer 19 | { 20 | private readonly TraceLog traceLog; 21 | 22 | private readonly Dictionary internalEventMapping = new Dictionary(); 23 | 24 | public TraceLogDeserializer(string etlFileNameWithTimeStamps) 25 | { 26 | var endTimeStart = etlFileNameWithTimeStamps.LastIndexOf('*'); 27 | var e = etlFileNameWithTimeStamps.Substring(endTimeStart + 1, etlFileNameWithTimeStamps.Length - (endTimeStart + 1)); 28 | var startTimeStart = etlFileNameWithTimeStamps.LastIndexOf('*', endTimeStart - 1); 29 | var s = etlFileNameWithTimeStamps.Substring(startTimeStart + 1, endTimeStart - (startTimeStart + 1)); 30 | var etlFileName = etlFileNameWithTimeStamps.Substring(0, startTimeStart); 31 | 32 | if (!DateTime.TryParse(s, out var startTime)) 33 | { 34 | startTime = DateTime.MinValue; 35 | } 36 | 37 | if (!DateTime.TryParse(e, out var endTime)) 38 | { 39 | endTime = DateTime.MaxValue; 40 | } 41 | 42 | var etlxPath = etlFileName + "." + startTime.ToString("yyyyddMHHmmss") + "-" + endTime.ToString("yyyyddMHHmmss") + ".etlx"; 43 | if (!File.Exists(etlxPath)) 44 | { 45 | var tmp = etlxPath + ".new"; 46 | try 47 | { 48 | TraceLog.CreateFromEventTraceLogFile(etlFileName, tmp, new TraceLogOptions { MaxEventCount = int.MaxValue, KeepAllEvents = true }, new TraceEventDispatcherOptions { StartTime = startTime, EndTime = endTime }); 49 | File.Move(tmp, etlxPath); 50 | } 51 | finally 52 | { 53 | if (File.Exists(tmp)) 54 | { 55 | File.Delete(tmp); 56 | } 57 | } 58 | } 59 | 60 | this.traceLog = new TraceLog(etlxPath); 61 | this.EventStats = new Dictionary(this.traceLog.Stats.Count); 62 | this.TraceProcesses = this.traceLog.Processes; 63 | this.TraceModuleFiles = this.traceLog.ModuleFiles; 64 | this.TraceInfo = new TraceInfo(this.traceLog.MachineName, this.traceLog.OSName, this.traceLog.OSBuild, this.traceLog.UTCOffsetMinutes, TimeZoneInfo.Local.BaseUtcOffset, this.traceLog.BootTime, this.traceLog.SessionStartTime, this.traceLog.SessionEndTime, this.traceLog.SessionEndTimeRelativeMSec, this.traceLog.SessionDuration, this.traceLog.CpuSpeedMHz, this.traceLog.NumberOfProcessors, this.traceLog.MemorySizeMeg, this.traceLog.PointerSize, this.traceLog.SampleProfileInterval, this.traceLog.EventCount, this.traceLog.EventsLost, this.traceLog.Size); 65 | 66 | int i = 1; 67 | this.internalEventMapping.Add(new EtwProviderInfo(new Guid("{9e814aad-3204-11d2-9a82-006008a86939}"), 0), 0); // for EventTrace Header 68 | foreach (var eventStat in this.traceLog.Stats) 69 | { 70 | this.EventStats.Add(i, eventStat); 71 | this.internalEventMapping.Add(new EtwProviderInfo(eventStat.IsClassic ? eventStat.TaskGuid : eventStat.ProviderGuid, eventStat.IsClassic ? (int)eventStat.Opcode : (int)eventStat.EventID), i); 72 | this.TotalStackCount += eventStat.StackCount; 73 | this.TotalEventCount += eventStat.Count; 74 | i++; 75 | } 76 | } 77 | 78 | public int TotalEventCount { get; } 79 | 80 | public int TotalStackCount { get; } 81 | 82 | public Dictionary EventStats { get; } 83 | 84 | public TraceProcesses TraceProcesses { get; } 85 | 86 | public TraceModuleFiles TraceModuleFiles { get; } 87 | 88 | public TraceInfo TraceInfo { get; } 89 | 90 | public StackSource GetStackSource(ProcessIndex processIndex, int stackType, double start, double end) 91 | { 92 | var sessionEndMsec = this.traceLog.SessionEndTimeRelativeMSec; 93 | var events = this.traceLog.Events.FilterByTime(start, Math.Abs(end) < 0.006 ? sessionEndMsec : (end > sessionEndMsec ? sessionEndMsec : end)); 94 | 95 | TraceProcess process = null; 96 | if (processIndex != ProcessIndex.Invalid) 97 | { 98 | process = this.TraceProcesses[processIndex]; 99 | } 100 | 101 | if (this.EventStats.TryGetValue(stackType, out var value)) 102 | { 103 | if (value.IsClassic) 104 | { 105 | if (value.TaskGuid == new Guid("{ce1dbfb4-137e-4da6-87b0-3f59aa102cbc}")) 106 | { 107 | return events.CPUStacks(process); 108 | } 109 | 110 | return events.SingleEventTypeStack(process, @event => @event.TaskGuid == value.TaskGuid && @event.Opcode == value.Opcode); 111 | } 112 | 113 | if (!value.IsClassic) 114 | { 115 | if (value.ProviderGuid == new Guid("{e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}") && value.EventID == (TraceEventID)80) 116 | { 117 | return events.Exceptions(process); 118 | } 119 | 120 | return events.SingleEventTypeStack(process, @event => @event.ProviderGuid == value.ProviderGuid && @event.ID == value.EventID); 121 | } 122 | } 123 | 124 | return events.AnyStacks(process); 125 | } 126 | 127 | public List GetEvents(HashSet eventTypes, string textFilter, int maxEventCount, double start, double end) 128 | { 129 | var returnEvents = new List(maxEventCount); 130 | int i = 0; 131 | var events = this.traceLog.Events.FilterByTime(start, Math.Abs(end) < 0.006 ? this.traceLog.SessionEndTimeRelativeMSec : end); 132 | var regex = new Regex(textFilter, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); 133 | 134 | foreach (var @event in events) 135 | { 136 | if (i < maxEventCount) 137 | { 138 | Guid providerGuid = @event.IsClassicProvider ? @event.TaskGuid : @event.ProviderGuid; 139 | if (!this.internalEventMapping.TryGetValue(new EtwProviderInfo(providerGuid, @event.IsClassicProvider ? (int)@event.Opcode : (int)@event.ID), out var mapId)) 140 | { 141 | continue; 142 | } 143 | 144 | if (eventTypes.Count < 1 || eventTypes.Contains(mapId)) 145 | { 146 | var eventData = new EventData 147 | { 148 | Timestamp = @event.TimeStampRelativeMSec.ToString("F3"), 149 | EventName = @event.ProviderName + "/" + @event.EventName, 150 | ProcessName = @event.ProcessName + $" ({@event.ProcessID})", 151 | EventIndex = (int)@event.EventIndex, 152 | }; 153 | 154 | var sb = new StringBuilder(); 155 | 156 | if (@event.CallStackIndex() != CallStackIndex.Invalid) 157 | { 158 | eventData.HasStack = true; 159 | } 160 | 161 | sb.Append("ThreadID=\""); 162 | sb.Append(@event.ThreadID); 163 | sb.Append("\" "); 164 | 165 | sb.Append("ProcessorNumber=\""); 166 | sb.Append(@event.ProcessorNumber); 167 | sb.Append("\" "); 168 | 169 | if (@event.ActivityID != Guid.Empty) 170 | { 171 | sb.Append("ActivityID=\""); 172 | sb.Append(@event.ActivityID.ToString()); 173 | sb.Append("\" "); 174 | } 175 | 176 | if (@event.RelatedActivityID != Guid.Empty) 177 | { 178 | sb.Append("RelatedActivityID=\""); 179 | sb.Append(@event.RelatedActivityID.ToString()); 180 | sb.Append("\" "); 181 | } 182 | 183 | var payloadNames = @event.PayloadNames; 184 | if (payloadNames.Length == 0 && @event.EventDataLength != 0) 185 | { 186 | eventData.Rest = "DataLength=\"" + @event.EventDataLength.ToString() + "\""; 187 | } 188 | else 189 | { 190 | try 191 | { 192 | for (int j = 0; j < payloadNames.Length; j++) 193 | { 194 | sb.Append(payloadNames[j]); 195 | sb.Append("=\""); 196 | sb.Append(@event.PayloadString(j)); 197 | sb.Append("\" "); 198 | } 199 | 200 | eventData.Rest = sb.ToString(); 201 | } 202 | catch (Exception) 203 | { 204 | eventData.Rest = "Error Parsing Field. DataLength=\"" + @event.EventDataLength.ToString() + "\""; 205 | } 206 | } 207 | 208 | var compareInfo = CultureInfo.InvariantCulture.CompareInfo; 209 | if (compareInfo.IndexOf(eventData.EventName, textFilter, CompareOptions.IgnoreCase) >= 0 || compareInfo.IndexOf(eventData.ProcessName, textFilter, CompareOptions.IgnoreCase) >= 0 || compareInfo.IndexOf(eventData.Rest, textFilter, CompareOptions.IgnoreCase) >= 0 || regex.IsMatch(eventData.Rest)) 210 | { 211 | returnEvents.Add(eventData); 212 | i++; 213 | } 214 | } 215 | } 216 | else 217 | { 218 | break; 219 | } 220 | } 221 | 222 | return returnEvents; 223 | } 224 | 225 | public DetailedProcessInfo GetDetailedProcessInfo(int processIndex) 226 | { 227 | var traceProcesses = this.traceLog.Processes; 228 | if (processIndex > traceProcesses.Count) 229 | { 230 | throw new ArgumentException(); 231 | } 232 | 233 | var traceProcess = traceProcesses[(ProcessIndex)processIndex]; 234 | 235 | var threadList = new List(); 236 | foreach (var thread in traceProcess.Threads) 237 | { 238 | threadList.Add(new ThreadInfo(thread.ThreadID, (int)thread.ThreadIndex, thread.CPUMSec, thread.StartTime, thread.StartTimeRelativeMSec, thread.EndTime, thread.EndTimeRelativeMSec)); 239 | } 240 | 241 | var moduleList = new List(); 242 | foreach (var loadedModule in traceProcess.LoadedModules) 243 | { 244 | var moduleFile = loadedModule.ModuleFile; 245 | moduleList.Add(new ModuleInfo((int)moduleFile.ModuleFileIndex, moduleFile.CodeAddressesInModule, moduleFile.FilePath)); 246 | } 247 | 248 | moduleList.Sort(); 249 | 250 | var processInfo = new ProcessInfo(traceProcess.Name + $" ({traceProcess.ProcessID})", (int)traceProcess.ProcessIndex, traceProcess.CPUMSec, traceProcess.ProcessID, traceProcess.ParentID, traceProcess.CommandLine); 251 | 252 | return new DetailedProcessInfo(processInfo, threadList, moduleList); 253 | } 254 | 255 | public string LookupSymbol(int moduleIndex) 256 | { 257 | var moduleFiles = this.traceLog.ModuleFiles; 258 | if (moduleIndex > moduleFiles.Count) 259 | { 260 | return $"ModuleIndex ({moduleIndex}) is larger than possible. It is invalid."; 261 | } 262 | 263 | var moduleFile = moduleFiles[(ModuleFileIndex)moduleIndex]; 264 | if (moduleFile != null) 265 | { 266 | var writer = new StringWriter(); 267 | using (var symbolReader = new SymbolReader(writer)) 268 | { 269 | this.traceLog.CallStacks.CodeAddresses.LookupSymbolsForModule(symbolReader, moduleFile); 270 | } 271 | 272 | return writer.ToString(); 273 | } 274 | 275 | return $"ModuleIndex ({moduleIndex}) is invalid."; 276 | } 277 | 278 | public string LookupSymbols(int[] moduleIndices) 279 | { 280 | var moduleFiles = this.traceLog.ModuleFiles; 281 | var writer = new StringWriter(); 282 | using (var symbolReader = new SymbolReader(writer)) 283 | { 284 | foreach (var moduleIndex in moduleIndices) 285 | { 286 | if (moduleIndex > moduleFiles.Count) 287 | { 288 | return $"ModuleIndex ({moduleIndex}) is larger than possible. It is invalid."; 289 | } 290 | 291 | var moduleFile = moduleFiles[(ModuleFileIndex)moduleIndex]; 292 | if (moduleFile != null) 293 | { 294 | this.traceLog.CallStacks.CodeAddresses.LookupSymbolsForModule(symbolReader, moduleFile); 295 | } 296 | } 297 | } 298 | 299 | return writer.ToString(); 300 | } 301 | 302 | private struct EtwProviderInfo : IEquatable 303 | { 304 | private readonly Guid providerId; 305 | 306 | private readonly int eventId; 307 | 308 | public EtwProviderInfo(Guid providerId, int eventId) 309 | { 310 | this.providerId = providerId; 311 | this.eventId = eventId; 312 | } 313 | 314 | public bool Equals(EtwProviderInfo other) 315 | { 316 | return this.providerId == other.providerId && this.eventId == other.eventId; 317 | } 318 | 319 | public override int GetHashCode() 320 | { 321 | int hash = 17; 322 | hash = (hash * 31) + this.providerId.GetHashCode(); 323 | hash = (hash * 31) + this.eventId.GetHashCode(); 324 | return hash; 325 | } 326 | 327 | public override bool Equals(object other) 328 | { 329 | if (other is EtwProviderInfo info) 330 | { 331 | return this.Equals(info); 332 | } 333 | 334 | return false; 335 | } 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/msdia140.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/perfviewjs/1f04a8bfa4fe2e7e7cec9131c2964f41935df8d6/src/msdia140.dll -------------------------------------------------------------------------------- /src/spa/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | *.d.ts 24 | -------------------------------------------------------------------------------- /src/spa/compress.js: -------------------------------------------------------------------------------- 1 | const brotli = require('brotli'); 2 | const fs = require('fs'); 3 | const zlib = require('zlib'); 4 | 5 | const brotliSettings = { 6 | extension: 'br', 7 | skipLarger: true, 8 | mode: 1, // 0 = generic, 1 = text, 2 = font (WOFF2) 9 | quality: 11, // 0 - 11, 10 | lgwin: 12 // default 11 | }; 12 | var dirs = ['build', 'build/static/css', 'build/static/js']; 13 | dirs.forEach(dir => { 14 | fs.readdirSync(dir).forEach(file => { 15 | if (file.endsWith('.js') || file.endsWith('.css') || file.endsWith('.html')) { 16 | // brotli 17 | const result = brotli.compress(fs.readFileSync(dir + '/' + file), brotliSettings); 18 | fs.writeFileSync(dir + '/' + file + '.br', result); 19 | // gzip 20 | const fileContents = fs.createReadStream(dir + '/' + file); 21 | const writeStream = fs.createWriteStream(dir + '/' + file + '.gz'); 22 | const zip = zlib.createGzip(); 23 | fileContents 24 | .pipe(zip) 25 | .on('error', err => console.error(err)) 26 | .pipe(writeStream) 27 | .on('error', err => console.error(err)); 28 | } 29 | }) 30 | }); -------------------------------------------------------------------------------- /src/spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PerfViewJS", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@monaco-editor/react": "^3.6.3", 7 | "@types/jest": "^26.0.14", 8 | "@types/react": "^16.9.53", 9 | "@types/react-dom": "^16.9.8", 10 | "@types/react-router-dom": "^5.1.6", 11 | "@types/reactstrap": "^8.5.1", 12 | "base64url": "^3.0.1", 13 | "bootstrap": "^4.5.3", 14 | "ejs": "^3.1.6", 15 | "glob-parent": "^5.1.2", 16 | "immer": "^9.0.6", 17 | "lodash": "^4.17.21", 18 | "monaco-editor": "^0.21.2", 19 | "react": "^16.14.0", 20 | "react-dom": "^16.14.0", 21 | "react-router-bootstrap": "^0.25.0", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "^4.0.3", 24 | "reactstrap": "^8.6.0", 25 | "rimraf": "^3.0.2", 26 | "typescript": "^4.0.3" 27 | }, 28 | "devDependencies": { 29 | "ajv": "^6.12.6", 30 | "brotli": "^1.3.2", 31 | "cross-env": "^7.0.2", 32 | "eslint-config-react-app": "^5.2.1", 33 | "eslint-plugin-flowtype": "^5.2.0", 34 | "eslint-plugin-import": "^2.22.1", 35 | "eslint-plugin-jsx-a11y": "^6.3.1", 36 | "eslint-plugin-react": "^7.21.5", 37 | "zlib": "^1.0.5" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "scripts": { 43 | "start": "rimraf ./build && react-scripts start", 44 | "compress": "node compress.js", 45 | "build": "react-scripts build", 46 | "test": "cross-env CI=true react-scripts test --env=jsdom", 47 | "eject": "react-scripts eject", 48 | "lint": "eslint ./src/" 49 | }, 50 | "proxy": "http://localhost:5000", 51 | "browserslist": [ 52 | ">5%", 53 | "not dead", 54 | "not ie <= 11", 55 | "not op_mini all" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/spa/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/perfviewjs/1f04a8bfa4fe2e7e7cec9131c2964f41935df8d6/src/spa/public/favicon.ico -------------------------------------------------------------------------------- /src/spa/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | PerfViewJS 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/spa/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "PerfViewJS Web Viewer", 3 | "name": "PerfViewJS Web Viewer", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /src/spa/scss/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/perfviewjs/1f04a8bfa4fe2e7e7cec9131c2964f41935df8d6/src/spa/scss/custom.scss -------------------------------------------------------------------------------- /src/spa/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render( 9 | 10 | 11 | , div); 12 | }); -------------------------------------------------------------------------------- /src/spa/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { Callers } from './components/Callers'; 4 | import { EventList } from './components/EventList'; 5 | import { EventViewer } from './components/EventViewer'; 6 | import { Home } from './components/Home'; 7 | import { Hotspots } from './components/Hotspots'; 8 | import { Layout } from './components/Layout'; 9 | import { ModuleList } from './components/ModuleList'; 10 | import { ProcessChooser } from './components/ProcessChooser'; 11 | import { ProcessInfo } from './components/ProcessInfo'; 12 | import { ProcessList } from './components/ProcessList'; 13 | import { Route } from 'react-router'; 14 | import { SourceViewer } from './components/SourceViewer'; 15 | import { TraceInfo } from './components/TraceInfo'; 16 | 17 | export default class App extends Component { 18 | static displayName = App.name; 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/spa/src/components/Callers.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyTNode, TNode } from './TNode'; 2 | 3 | import { Link } from 'react-router-dom' 4 | import { NavMenu } from './NavMenu'; 5 | import React from 'react'; 6 | import { StackViewerFilter } from './StackViewerFilter' 7 | import { TreeNode } from './TreeNode' 8 | import base64url from 'base64url' 9 | 10 | export interface Props { 11 | match: any; 12 | } 13 | 14 | interface State { 15 | loading: boolean; 16 | node: TNode; 17 | } 18 | 19 | export class Callers extends React.Component { 20 | 21 | ignoreLastFetch: boolean; 22 | 23 | static displayName = Callers.name; 24 | 25 | fetchData() { 26 | 27 | this.setState({ node: new EmptyTNode(), loading: true }); // HACK: Why is this required? 28 | 29 | fetch('/api/treenode?' + Callers.constructAPICacheKeyFromRouteKey(this.props.match.params.routeKey) + '&name=' + this.props.match.params.callTreeNodeId + '&subtree=' + this.props.match.params.subtree, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 30 | .then(res => res.json()) 31 | .then(data => { 32 | if (!this.ignoreLastFetch) { 33 | if (data === null) { 34 | window.location.href = '/ui/stackviewer/hotspots/' + this.props.match.params.routeKey; 35 | } else { 36 | data.hasChildren = true; // HACK: Because the api doesn't return this set true. 37 | this.setState({ node: data, loading: false }); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | constructor(props: Props) { 44 | super(props); 45 | this.ignoreLastFetch = false; 46 | this.state = { loading: true, node: new EmptyTNode() }; 47 | } 48 | 49 | componentWillUnmount() { 50 | this.ignoreLastFetch = true 51 | } 52 | 53 | componentDidMount() { 54 | this.fetchData(); 55 | } 56 | 57 | componentDidUpdate(prevProps: Props) { 58 | let oldId = prevProps.match.params.callTreeNodeId 59 | let newId = this.props.match.params.callTreeNodeId 60 | if (newId !== oldId) { 61 | this.fetchData() 62 | } 63 | } 64 | 65 | static constructAPICacheKeyFromRouteKey(r: string) { 66 | var routeKey = JSON.parse(base64url.decode(r, "utf8")); 67 | return 'filename=' + routeKey.a + '&stackType=' + routeKey.b + '&pid=' + routeKey.c + '&start=' + base64url.encode(routeKey.d, "utf8") + '&end=' + base64url.encode(routeKey.e, "utf8") + '&groupPats=' + base64url.encode(routeKey.f, "utf8") + '&foldPats=' + base64url.encode(routeKey.g, "utf8") + '&incPats=' + base64url.encode(routeKey.h, "utf8") + '&excPats=' + base64url.encode(routeKey.i, "utf8") + '&foldPct=' + routeKey.j + '&drillIntoKey=' + routeKey.k; 68 | } 69 | 70 | static renderCallersTable(routeKey: string, callTreeNodeId: string, node: TNode) { 71 | return ( 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
NameSourceExclusive Metric %Exclusive CountInclusive Metric %Inclusive CountFold CountWhenFirstLast
91 | ); 92 | } 93 | 94 | render() { 95 | let contents = this.state.loading ?

Loading...

: Callers.renderCallersTable(this.props.match.params.routeKey, this.props.match.params.callTreeNodeId, this.state.node); 96 | 97 | return ( 98 |
99 | 100 |
101 |
102 |

{base64url.decode(JSON.parse(base64url.decode(this.props.match.params.routeKey, "utf8")).l, "utf8")} » Hotspots » Callers

103 | 104 |
105 | {contents} 106 |
107 |
108 | 109 | ); 110 | } 111 | } -------------------------------------------------------------------------------- /src/spa/src/components/EventList.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import { NavMenu } from './NavMenu'; 3 | import React from 'react'; 4 | import base64url from 'base64url' 5 | 6 | export interface Props { 7 | match: any; 8 | } 9 | 10 | interface State { 11 | dataFile: string; 12 | events: Event[]; 13 | loading: boolean; 14 | error: boolean; 15 | } 16 | 17 | interface Event { 18 | stackEventCount: number; 19 | eventId: string; 20 | name: string; 21 | eventCount: string; 22 | eventName: string; 23 | } 24 | 25 | export class EventList extends React.Component { 26 | static displayName = EventList.name; 27 | 28 | constructor(props: Props) { 29 | super(props); 30 | var dataFile = this.props.match.params.dataFile; 31 | this.state = { dataFile: dataFile, events: [], loading: true, error: false }; 32 | fetch('/api/eventlistos?filename=' + dataFile, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 33 | .then(res => res.json()) 34 | .then(data => { 35 | this.setState({ events: data, loading: false }); 36 | }); 37 | } 38 | 39 | static renderEventListTable(events: Event[], dataFile: string) { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {events.map(event => 51 | 52 | 53 | 54 | 55 | 56 | )} 57 | 58 |
Event NameStack CountEvent Count
{event.stackEventCount !== 0 ? {event.eventName} : event.eventName}{event.stackEventCount}{event.eventCount}
59 | ); 60 | } 61 | 62 | render() { 63 | 64 | if (this.state.error) { 65 | return (
{this.state.error}
) 66 | } 67 | 68 | let contents = this.state.loading ?

Loading...

: EventList.renderEventListTable(this.state.events, this.state.dataFile); 69 | 70 | return ( 71 |
72 | 73 |

Choose Stack Type

74 | {contents} 75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/spa/src/components/EventViewer.tsx: -------------------------------------------------------------------------------- 1 | import { NavMenu } from './NavMenu'; 2 | import React from 'react'; 3 | import base64url from 'base64url'; 4 | 5 | export interface Props { 6 | match: any; 7 | } 8 | 9 | interface State { 10 | events: Event[]; 11 | eventTypes: any; 12 | traceInfo: any; 13 | loading: boolean; 14 | selectedEvents: string; 15 | start: string; 16 | end: string; 17 | textFilter: string; 18 | maxEventCount: string; 19 | eventNameFilter: string; 20 | } 21 | 22 | interface Event { 23 | eventId: string; 24 | name: string; 25 | rest: string; 26 | eventIndex: number; 27 | eventName: string; 28 | timestamp: string; 29 | processName: string; 30 | hasStack: boolean; 31 | } 32 | 33 | export class EventViewer extends React.Component { 34 | 35 | static displayName = EventViewer.name; 36 | 37 | myRef: React.RefObject; 38 | static windowCurrentScrollY: number = 0; 39 | 40 | constructor(props: Props) { 41 | super(props); 42 | this.myRef = React.createRef(); 43 | this.state = { eventNameFilter: '', traceInfo: null, events: [], eventTypes: [], loading: true, selectedEvents: '', start: '0.000', end: '', textFilter: '', maxEventCount: '1000' }; 44 | this.handleChange = this.handleChange.bind(this); 45 | this.handleOnClick = this.handleOnClick.bind(this); 46 | 47 | this.handleStartChange = this.handleStartChange.bind(this); 48 | this.handleEndChange = this.handleEndChange.bind(this); 49 | this.handleTextFilterChange = this.handleTextFilterChange.bind(this); 50 | this.handleMaxEventCountChange = this.handleMaxEventCountChange.bind(this); 51 | this.handleEventTypeFilterList = this.handleEventTypeFilterList.bind(this); 52 | 53 | fetch('/api/traceinfo?filename=' + this.props.match.params.dataFile, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 54 | .then(res => res.json()) 55 | .then(data => { 56 | this.setState({ end: data.endTimeRelativeMSec }); 57 | }); 58 | 59 | fetch('/api/eventliston?filename=' + this.props.match.params.dataFile, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 60 | .then(res => res.json()) 61 | .then(data => { 62 | this.setState({ eventTypes: data, loading: false }); 63 | }); 64 | 65 | window.addEventListener('scroll', (e) => { EventViewer.windowCurrentScrollY = window.scrollY; }); 66 | } 67 | 68 | handleOnClick(e: any) { 69 | e.preventDefault(); 70 | 71 | fetch('/api/eventdata?filename=' + this.props.match.params.dataFile + '&maxEventCount=' + this.state.maxEventCount + '&start=' + this.state.start + '&end=' + this.state.end + '&filter=' + base64url.encode(this.state.textFilter, "utf8") + '&eventTypes=' + this.state.selectedEvents, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 72 | .then(res => res.json()) 73 | .then(data => { 74 | this.setState({ events: data }); 75 | }); 76 | } 77 | 78 | handleStartChange(e: any) { 79 | this.setState({ start: e.target.value }); 80 | } 81 | 82 | handleEndChange(e: any) { 83 | this.setState({ end: e.target.value }); 84 | } 85 | 86 | handleTextFilterChange(e: any) { 87 | this.setState({ textFilter: e.target.value }); 88 | } 89 | 90 | handleMaxEventCountChange(e: any) { 91 | this.setState({ maxEventCount: e.target.value }); 92 | } 93 | 94 | handleChange(e: any) { 95 | let selectedOptions: HTMLOptionsCollection = e.target.selectedOptions; 96 | 97 | if (this.state.eventTypes.length === selectedOptions.length) { 98 | this.setState({ selectedEvents: '' }); 99 | } 100 | else { 101 | 102 | let result: string = ''; 103 | 104 | for (let i = 0; i < selectedOptions.length; ++i) { 105 | result += selectedOptions.item(i)?.value + ','; 106 | } 107 | 108 | result = result.substring(0, result.length - 1); 109 | 110 | this.setState({ selectedEvents: result }); 111 | } 112 | } 113 | 114 | handleEventTypeFilterList(e: any) { 115 | this.setState({ eventNameFilter: e.target.value }); 116 | } 117 | 118 | static renderEventListTable(events: Event[], obj: EventViewer, eventNameFilter: string) { 119 | return ({}); 120 | } 121 | 122 | render() { 123 | 124 | let contents = this.state.loading ?

Loading...

: EventViewer.renderEventListTable(this.state.eventTypes, this, this.state.eventNameFilter); 125 | 126 | return ( 127 |
128 | 129 |
130 |

Event Viewer

131 |
132 |
133 |
134 | 135 | 136 | 137 | 138 |
139 |
140 | 141 | 142 | 143 | 144 |
145 |
146 | 147 |
148 |
149 |
150 |
Event Type Filter:
151 |
152 |
153 | {contents} 154 |
155 |
156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | {this.state.events.map(event => 167 | 168 | 169 | 170 | 171 | 172 | 173 | )} 174 | 175 |
Event NameTime MSecProcess NameRest
{event.eventName}{event.timestamp}{event.processName}{event.hasStack ? HasStack="True" : ''} {event.rest}
176 |
177 |
178 |
179 |
180 | ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/spa/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'react-router'; 3 | import base64url from 'base64url' 4 | 5 | export interface Props { 6 | match: any; 7 | } 8 | 9 | interface State { 10 | dataFile: string; 11 | startTime: string; 12 | endTime: string; 13 | redirect: boolean; 14 | files: string[]; 15 | } 16 | 17 | export class Home extends React.Component { 18 | 19 | static displayName = Home.name; 20 | 21 | constructor(props: any) { 22 | super(props); 23 | this.state = { files: [], dataFile: "", startTime: "", endTime: "", redirect: false }; 24 | this.handleDataFileChange = this.handleDataFileChange.bind(this); 25 | this.handleStartTimeChange = this.handleStartTimeChange.bind(this); 26 | this.handleEndTimeChange = this.handleEndTimeChange.bind(this); 27 | this.handleOnClick = this.handleOnClick.bind(this); 28 | 29 | fetch('/api/datadirectorylisting', { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 30 | .then(res => res.json()) 31 | .then(data => { 32 | this.setState({ files: data }); 33 | }); 34 | } 35 | 36 | handleDataFileChange(e: string) { 37 | this.setState({ dataFile: e }); 38 | } 39 | 40 | handleStartTimeChange(e: any) { 41 | this.setState({ startTime: e.target.value }); 42 | } 43 | 44 | handleEndTimeChange(e: any) { 45 | this.setState({ endTime: e.target.value }); 46 | } 47 | 48 | handleOnClick() { 49 | this.setState({ redirect: true }); 50 | } 51 | 52 | render() { 53 | 54 | if (this.state.redirect) { 55 | var encoded = base64url.encode(this.state.dataFile + "*" + this.state.startTime + "*" + this.state.endTime, "utf8"); 56 | return ; 57 | } 58 | 59 | return ( 60 |
61 |

PerfViewJS

62 | 63 | File Path: 64 | Start Time: 65 | End Time: 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {this.state.files.map(file => 77 | 78 | 79 | )} 80 | 81 |
Choose a File (it populates the above form)
82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/spa/src/components/Hotspots.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import { NavMenu } from './NavMenu'; 3 | import React from 'react'; 4 | import { StackViewerFilter } from './StackViewerFilter' 5 | import { TNode } from './TNode'; 6 | import base64url from 'base64url' 7 | 8 | export interface Props { 9 | match: any; 10 | } 11 | 12 | interface State { 13 | loading: boolean; 14 | nodes: TNode[]; 15 | } 16 | 17 | export class Hotspots extends React.Component { 18 | 19 | ignoreLastFetch: boolean; 20 | 21 | static displayName = Hotspots.name; 22 | 23 | constructor(props: Props) { 24 | super(props); 25 | this.ignoreLastFetch = false; 26 | this.state = { loading: true, nodes: [] }; 27 | this.handleDrillIntoClick = this.handleDrillIntoClick.bind(this); 28 | } 29 | 30 | fetchData() { 31 | 32 | this.setState({ loading: true }); // HACK: Why is this required? 33 | 34 | fetch('/api/hotspots?' + StackViewerFilter.constructAPICacheKeyFromRouteKey(this.props.match.params.routeKey), { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 35 | .then(res => res.json()) 36 | .then(data => { 37 | if (!this.ignoreLastFetch) { 38 | this.setState({ nodes: data, loading: false }); 39 | } 40 | }); 41 | } 42 | 43 | handleDrillIntoClick(d: string, t: string) { 44 | 45 | var drillType = '/api/drillinto/exclusive?' 46 | if (d === 'i') { 47 | drillType = '/api/drillinto/inclusive?'; 48 | } 49 | 50 | fetch(drillType + StackViewerFilter.constructAPICacheKeyFromRouteKey(this.props.match.params.routeKey) + '&name=' + t, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 51 | .then(res => res.json()) 52 | .then(data => { 53 | var newRouteKey = JSON.parse(base64url.decode(this.props.match.params.routeKey, "utf8")); 54 | newRouteKey.k = data; 55 | window.location.href = '/ui/stackviewer/hotspots/' + base64url.encode(JSON.stringify(newRouteKey)); 56 | }); 57 | } 58 | 59 | componentWillUnmount() { 60 | this.ignoreLastFetch = true 61 | } 62 | 63 | componentDidMount() { 64 | this.fetchData() 65 | } 66 | 67 | componentDidUpdate(prevProps: Props) { 68 | let oldId = prevProps.match.params.routeKey 69 | let newId = this.props.match.params.routeKey 70 | if (newId !== oldId) { 71 | this.fetchData() 72 | } 73 | } 74 | 75 | static renderHotspotsTable(nodes: TNode[], routeKey: string, obj: Hotspots) { 76 | return ( 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {nodes.map(node => 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | )} 105 | 106 |
NameExclusive Metric %Exclusive CountInclusive Metric %Inclusive CountFold CountWhenFirstLast
{node.name}{node.exclusiveMetricPercent}%{node.inclusiveMetricPercent}%{node.exclusiveFoldedMetric}{node.inclusiveMetricByTimeString}{node.firstTimeRelativeMSec}{node.lastTimeRelativeMSec}
107 | ); 108 | } 109 | 110 | render() { 111 | 112 | let contents = this.state.loading ?

Loading...

: Hotspots.renderHotspotsTable(this.state.nodes, this.props.match.params.routeKey, this); 113 | 114 | return ( 115 |
116 | 117 |
118 |
119 |

{base64url.decode(JSON.parse(base64url.decode(this.props.match.params.routeKey, "utf8")).l, "utf8")} » Hotspots

120 | 121 |
122 | {contents} 123 |
124 |
125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/spa/src/components/Layout.css: -------------------------------------------------------------------------------- 1 | .table td { padding: 2px; } 2 | .center { text-align: center; } 3 | .bpc { margin-left: 10px; margin-right: 0; } 4 | .btn-tiny { padding: 0px 3px 0px 3px; font-size: 0.75rem; line-height: 1.4; border-radius: 0.2rem; } 5 | #pd { font-family: monospace; font-size: 12px; padding: 0; margin: 0; } -------------------------------------------------------------------------------- /src/spa/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import './Layout.css'; 2 | 3 | import React, { Component } from 'react'; 4 | 5 | export class Layout extends Component { 6 | static displayName = Layout.name; 7 | 8 | render() { 9 | return ( 10 |
11 | {this.props.children} 12 |
13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /src/spa/src/components/ModuleList.tsx: -------------------------------------------------------------------------------- 1 | import { NavMenu } from './NavMenu'; 2 | import React from 'react'; 3 | 4 | export interface Props { 5 | match: any; 6 | } 7 | 8 | interface State { 9 | modules: any; 10 | loading: boolean; 11 | } 12 | 13 | interface Module { 14 | modulePath: string; 15 | id: number; 16 | addrCount: number; 17 | } 18 | 19 | export class ModuleList extends React.Component { 20 | 21 | static displayName = ModuleList.name; 22 | 23 | constructor(props: Props) { 24 | super(props); 25 | this.state = { modules: [], loading: true }; 26 | fetch('/api/modulelist?filename=' + this.props.match.params.dataFile, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 27 | .then(res => res.json()) 28 | .then(data => { 29 | this.setState({ modules: data, loading: false }); 30 | }); 31 | } 32 | 33 | static renderModuleListTable(modules: Module[], dataFile: string) { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {modules.map(module => 44 | 45 | 46 | 47 | 48 | )} 49 | 50 |
Module NameNumber of address occurrences in all stacks
{module.modulePath}{module.addrCount}
51 | ); 52 | } 53 | 54 | render() { 55 | let contents = this.state.loading ?

Loading...

: ModuleList.renderModuleListTable(this.state.modules, this.props.match.params.dataFile); 56 | 57 | return ( 58 |
59 | 60 |

Module List

61 | {contents} 62 |
63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/spa/src/components/NavMenu.css: -------------------------------------------------------------------------------- 1 | a.navbar-brand { 2 | white-space: normal; 3 | text-align: center; 4 | word-break: break-all; 5 | } 6 | 7 | html { 8 | font-size: 14px; 9 | } 10 | @media (min-width: 768px) { 11 | html { 12 | font-size: 16px; 13 | } 14 | } 15 | 16 | .box-shadow { 17 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 18 | } -------------------------------------------------------------------------------- /src/spa/src/components/NavMenu.tsx: -------------------------------------------------------------------------------- 1 | import './NavMenu.css'; 2 | 3 | import { Collapse, Container, NavItem, NavLink, Navbar, NavbarBrand, NavbarToggler } from 'reactstrap'; 4 | 5 | import { Link } from 'react-router-dom'; 6 | import React from 'react'; 7 | import base64url from 'base64url' 8 | 9 | export interface Props { 10 | dataFile: string; 11 | } 12 | 13 | interface State { 14 | collapsed: boolean; 15 | dataFile: string; 16 | fileName: string; 17 | startTime: string; 18 | endTime: string; 19 | } 20 | 21 | export class NavMenu extends React.Component { 22 | 23 | static displayName = NavMenu.name; 24 | 25 | constructor(props: Props) { 26 | super(props); 27 | let arr = base64url.decode(this.props.dataFile, "utf8").split('*'); 28 | this.toggleNavbar = this.toggleNavbar.bind(this); 29 | this.state = { 30 | fileName: arr[0], 31 | startTime: arr[1], 32 | endTime: arr[2], 33 | collapsed: true, 34 | dataFile: props.dataFile, 35 | }; 36 | } 37 | 38 | toggleNavbar() { 39 | this.setState({ 40 | collapsed: !this.state.collapsed 41 | }); 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 | 48 | {this.state.fileName}     {this.state.startTime !== '' || this.state.endTime !== '' ? Time Filter Applied : null} {this.state.startTime !== '' ? Start: {this.state.startTime} : null}   {this.state.endTime !== '' ? End: {this.state.endTime} : null} 49 | 50 | PerfViewJS 51 | 52 | 53 |
    54 | 55 | Trace Info 56 | 57 | 58 | Event Viewer 59 | 60 | 61 | Stack Viewer 62 | 63 | 64 | Process List 65 | 66 | 67 | Module List 68 | 69 |
70 |
71 |
72 |
73 |
74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/spa/src/components/ProcessChooser.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { NavMenu } from './NavMenu'; 3 | import React from 'react'; 4 | import base64url from 'base64url'; 5 | 6 | export interface Props { 7 | match: any; 8 | } 9 | 10 | interface State { 11 | processes: any; 12 | loading: boolean; 13 | } 14 | 15 | interface Process { 16 | name: string; 17 | id: number; 18 | cpumSec: number; 19 | } 20 | 21 | export class ProcessChooser extends React.Component { 22 | 23 | static displayName = ProcessChooser.name; 24 | 25 | constructor(props: Props) { 26 | super(props); 27 | this.state = { processes: [], loading: true }; 28 | fetch('/api/processchooser?filename=' + this.props.match.params.dataFile + '&stacktype=' + this.props.match.params.stackType, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 29 | .then(res => res.json()) 30 | .then(data => { 31 | this.setState({ processes: data, loading: false }); 32 | }); 33 | } 34 | 35 | static renderProcessChooserTable(processes: Process[], dataFile: string, stackType: number, stackTypeName: string) { 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {processes.map(process => 46 | 47 | 48 | 49 | 50 | )} 51 | 52 |
Process NameCPU MSec
{process.name}{process.cpumSec}
53 | ); 54 | } 55 | 56 | render() { 57 | let contents = this.state.loading ?

Loading...

: ProcessChooser.renderProcessChooserTable(this.state.processes, this.props.match.params.dataFile, this.props.match.params.stackType, this.props.match.params.stackTypeName); 58 | 59 | return ( 60 |
61 | 62 |

Event {base64url.decode(this.props.match.params.stackTypeName, "utf8")} » Choose Process

63 | {contents} 64 |
65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/spa/src/components/ProcessInfo.tsx: -------------------------------------------------------------------------------- 1 | import { ModuleList } from './ModuleList'; 2 | import { NavMenu } from './NavMenu'; 3 | import { ProcessList } from './ProcessList'; 4 | import React from 'react'; 5 | 6 | export interface Props { 7 | match: any; 8 | } 9 | 10 | interface State { 11 | processInfo: any; 12 | loading: boolean; 13 | } 14 | 15 | interface Process { 16 | id: number; 17 | processId: number; 18 | parentId: number; 19 | name: string; 20 | commandLine: string; 21 | cpumSec: number; 22 | } 23 | 24 | interface Thread { 25 | threadId: number; 26 | threadIndex: number; 27 | cpumSec: number; 28 | startTime: string; 29 | startTimeRelativeMSec: number; 30 | endTime: string; 31 | endTimeRelativeMSec: number; 32 | } 33 | 34 | interface Module { 35 | id: number; 36 | addrCount: number; 37 | modulePath: string; 38 | } 39 | 40 | interface DetailedProcessInfo { 41 | processInfo: Process; 42 | threads: Thread[]; 43 | modules: Module[]; 44 | } 45 | 46 | export class ProcessInfo extends React.Component { 47 | 48 | static displayName = ProcessInfo.name; 49 | 50 | constructor(props: Props) { 51 | super(props); 52 | this.state = { processInfo: null, loading: true }; 53 | fetch('/api/processinfo?filename=' + this.props.match.params.dataFile + '&processIndex=' + this.props.match.params.processIndex, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 54 | .then(res => res.json()) 55 | .then(data => { 56 | this.setState({ processInfo: data, loading: false }); 57 | }); 58 | } 59 | 60 | static render(processInfo: DetailedProcessInfo, dataFile: string) { 61 | return ( 62 |
63 | {ProcessList.renderProcessListTable([processInfo.processInfo], dataFile)} 64 | {ModuleList.renderModuleListTable(processInfo.modules, dataFile)} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {processInfo.threads.map(thread => 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | )} 87 | 88 |
Thread IDStart TimeStart Time Relative MSecEnd TimeEnd Time Relative MSecCPU Milliseconds
{thread.threadId}{thread.startTime}{thread.startTimeRelativeMSec}{thread.endTime}{thread.endTimeRelativeMSec}{thread.cpumSec}
89 |
90 | ); 91 | } 92 | 93 | render() { 94 | let contents = this.state.loading ?

Loading...

: ProcessInfo.render(this.state.processInfo, this.props.match.params.dataFile); 95 | 96 | return ( 97 |
98 | 99 | {contents} 100 |
101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/spa/src/components/ProcessList.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { NavMenu } from './NavMenu'; 3 | import React from 'react'; 4 | 5 | export interface Props { 6 | match: any; 7 | } 8 | 9 | interface State { 10 | processes: any; 11 | loading: boolean; 12 | } 13 | 14 | interface Process { 15 | id: number; 16 | processId: number; 17 | parentId: number; 18 | name: string; 19 | commandLine: string; 20 | cpumSec: number; 21 | } 22 | 23 | export class ProcessList extends React.Component { 24 | 25 | static displayName = ProcessList.name; 26 | 27 | constructor(props: Props) { 28 | super(props); 29 | this.state = { processes: [], loading: true }; 30 | fetch('/api/processchooser?filename=' + this.props.match.params.dataFile, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 31 | .then(res => res.json()) 32 | .then(data => { 33 | this.setState({ processes: data, loading: false }); 34 | }); 35 | } 36 | 37 | static renderProcessListTable(processes: Process[], dataFile: string) { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {processes.filter(function (p) { return p.id !== -1 }).map(process => 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | )} 59 | 60 |
Process NameProcess IdParent IdCPU MillisecondsCommand Line
{process.processId === 0 ? "Idle" : process.processId === 4 ? "System" : ({process.name})}{process.processId}{process.parentId}{process.cpumSec}{process.commandLine}
61 | ); 62 | } 63 | 64 | render() { 65 | let contents = this.state.loading ?

Loading...

: ProcessList.renderProcessListTable(this.state.processes, this.props.match.params.dataFile); 66 | 67 | return ( 68 |
69 | 70 |

Process List

71 | {contents} 72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/spa/src/components/SourceViewer.css: -------------------------------------------------------------------------------- 1 | .hotlines { border: 1px solid #000; padding: 2px; margin-left: 20px; margin-bottom: 20px; } 2 | .hotlines td { border: 1px solid #000; } 3 | .lineDecoration { background: red; } 4 | .inlineDecoration { background-color: #603311; } -------------------------------------------------------------------------------- /src/spa/src/components/SourceViewer.tsx: -------------------------------------------------------------------------------- 1 | import './SourceViewer.css'; 2 | 3 | import Editor from '@monaco-editor/react'; 4 | import { NavMenu } from './NavMenu'; 5 | import React from 'react'; 6 | import { StackViewerFilter } from './StackViewerFilter' 7 | import base64url from 'base64url'; 8 | 9 | export interface Props { 10 | match: any; 11 | } 12 | 13 | interface State { 14 | loading: boolean; 15 | sourceInformation: SourceInformation | null; 16 | } 17 | 18 | interface LineInformation { 19 | lineNumber: number; 20 | metric: number; 21 | } 22 | 23 | interface SourceInformation { 24 | url: string; 25 | log: string; 26 | summary: LineInformation[]; 27 | data: string; 28 | buildTimeFilePath: string; 29 | } 30 | 31 | export class SourceViewer extends React.PureComponent { 32 | 33 | static displayName = SourceViewer.name; 34 | 35 | constructor(props: Props) { 36 | super(props); 37 | this.handleEditorDidMount = this.handleEditorDidMount.bind(this); 38 | this.state = { loading: true, sourceInformation: null }; 39 | 40 | fetch('/api/getsource?' + StackViewerFilter.constructAPICacheKeyFromRouteKey(this.props.match.params.routeKey) + '&name=' + this.props.match.params.callTreeNodeId, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 41 | .then(res => res.json()) 42 | .then(data => { 43 | this.setState({ sourceInformation: data, loading: false }); 44 | }); 45 | } 46 | 47 | handleEditorDidMount(_: any, editor: any) { 48 | 49 | let summary = this.state.sourceInformation?.summary; 50 | if (summary !== undefined && summary.length > 0) { 51 | 52 | editor.revealPosition({ lineNumber: summary[0].lineNumber, column: 1 }, true, false); 53 | 54 | summary.forEach(e => { 55 | editor.changeDecorations(function (changeAccessor: any) { 56 | return changeAccessor.addDecoration({ 57 | startLineNumber: e.lineNumber, 58 | startColumn: 1, 59 | endLineNumber: e.lineNumber, 60 | endColumn: 1 61 | }, { 62 | isWholeLine: true, 63 | glyphMarginClassName: 'lineDecoration', 64 | inlineClassName: 'inlineDecoration' 65 | }); 66 | }); 67 | }); 68 | } 69 | } 70 | 71 | static renderEditor(routeKey: string, sourceInformation: SourceInformation, obj: SourceViewer) { 72 | return ( 73 |
74 | 75 |
76 | 79 | 80 | 81 | 82 | 83 | 84 | {sourceInformation.summary.map(line => 85 | 86 | 87 | 88 | 89 | )} 90 | 91 |
Line NumberHit Count
{line.lineNumber}{line.metric}
92 | 0 ? sourceInformation.summary[0].lineNumber : 1} /> 93 |
94 |
95 | ); 96 | } 97 | 98 | render() { 99 | return (this.state.loading || this.state.sourceInformation === null ?

Loading...

: SourceViewer.renderEditor(this.props.match.params.routeKey, this.state.sourceInformation, this)); 100 | } 101 | } -------------------------------------------------------------------------------- /src/spa/src/components/StackViewerFilter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import base64url from 'base64url'; 3 | 4 | export interface Props { 5 | routeKey: string; 6 | } 7 | 8 | interface State { 9 | newRouteKey: string; 10 | start: string; 11 | end: string; 12 | groupPats: string; 13 | foldPats: string; 14 | incPats: string; 15 | excPats: string; 16 | foldPct: string; 17 | drillIntoKey: string; 18 | minCount: number; 19 | symbolLookupStatus: string; 20 | symbolLog: string; 21 | } 22 | 23 | export class StackViewerFilter extends React.PureComponent { 24 | 25 | static displayName = StackViewerFilter.name; 26 | 27 | constructor(props: Props) { 28 | super(props); 29 | var data = JSON.parse(base64url.decode(this.props.routeKey, "utf8")); 30 | this.state = { symbolLog: '', symbolLookupStatus: '', minCount: 50, newRouteKey: this.props.routeKey, start: data.d, end: data.e, groupPats: data.f, foldPats: data.g, incPats: data.h, excPats: data.i, foldPct: data.j, drillIntoKey: data.k }; 31 | 32 | this.handleGroupPatsChange = this.handleGroupPatsChange.bind(this); 33 | this.handleFoldPatsChange = this.handleFoldPatsChange.bind(this); 34 | this.handleStartChange = this.handleStartChange.bind(this); 35 | this.handleEndChange = this.handleEndChange.bind(this); 36 | this.handleIncPatsChange = this.handleIncPatsChange.bind(this); 37 | this.handleExcPatsChange = this.handleExcPatsChange.bind(this); 38 | this.handleOnClick = this.handleOnClick.bind(this); 39 | this.handleLookupWarmSymbols = this.handleLookupWarmSymbols.bind(this); 40 | this.handleLookupWarmSymbolsMinCount = this.handleLookupWarmSymbolsMinCount.bind(this); 41 | } 42 | 43 | handleGroupPatsChange(e: any) { 44 | this.setState({ groupPats: e.target.value }); 45 | } 46 | 47 | handleFoldPatsChange(e: any) { 48 | this.setState({ foldPats: e.target.value }); 49 | } 50 | 51 | handleStartChange(e: any) { 52 | this.setState({ start: e.target.value }); 53 | } 54 | 55 | handleEndChange(e: any) { 56 | this.setState({ end: e.target.value }); 57 | } 58 | 59 | handleIncPatsChange(e: any) { 60 | this.setState({ incPats: e.target.value }); 61 | } 62 | 63 | handleExcPatsChange(e: any) { 64 | this.setState({ excPats: e.target.value }); 65 | } 66 | 67 | handleOnClick(e: any) { 68 | 69 | e.preventDefault(); 70 | 71 | var oldRouteKey = JSON.parse(base64url.decode(this.props.routeKey, "utf8")); 72 | var newRouteKeyJsonString = JSON.stringify({ a: oldRouteKey.a, b: oldRouteKey.b, c: -1, d: this.state.start, e: this.state.end || '', f: this.state.groupPats || '', g: this.state.foldPats || '', h: this.state.incPats || '', i: this.state.excPats || '', j: this.state.foldPct || '', k: this.state.drillIntoKey, l: oldRouteKey.l }); 73 | 74 | if (JSON.stringify(oldRouteKey) !== newRouteKeyJsonString) { 75 | window.location.href = '/ui/stackviewer/hotspots/' + base64url.encode(newRouteKeyJsonString, "utf8"); // HACK: But the "react" way is annoying. Any ideas? 76 | } 77 | } 78 | 79 | handleLookupWarmSymbolsMinCount(e: any) { 80 | this.setState({ minCount: e.target.value }); 81 | } 82 | 83 | handleLookupWarmSymbols() { 84 | 85 | this.setState({ symbolLookupStatus: ' ... performing lookup.' }); 86 | fetch("/api/lookupwarmsymbols?minCount=" + this.state.minCount + "&" + StackViewerFilter.constructAPICacheKeyFromRouteKey(this.props.routeKey), { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 87 | .then(res => res.json()) 88 | .then(data => { 89 | window.location.href = '/ui/stackviewer/hotspots/' + this.props.routeKey; 90 | }); 91 | } 92 | 93 | static constructAPICacheKeyFromRouteKey(r: string) { 94 | var routeKey = JSON.parse(base64url.decode(r, "utf8")); 95 | return 'filename=' + routeKey.a + '&stackType=' + routeKey.b + '&pid=' + routeKey.c + '&start=' + base64url.encode(routeKey.d, "utf8") + '&end=' + base64url.encode(routeKey.e, "utf8") + '&groupPats=' + base64url.encode(routeKey.f, "utf8") + '&foldPats=' + base64url.encode(routeKey.g, "utf8") + '&incPats=' + base64url.encode(routeKey.h, "utf8") + '&excPats=' + base64url.encode(routeKey.i, "utf8") + '&foldPct=' + routeKey.j + '&drillIntoKey=' + routeKey.k; 96 | } 97 | 98 | render() { 99 | return ( 100 |
101 |
102 |
103 |
104 | 105 | 106 | 107 | 108 |
109 |
110 | 111 | 112 | 113 | 114 |
115 |
116 | 117 | 118 | 119 | 120 |
121 | 122 |
123 | 124 |
125 |
126 |
127 | 128 | {this.state.symbolLookupStatus} 129 |
130 |
131 |
132 | Grouping Patterns Examples 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |
PatternComment
{`{`}%}!->module $1Group Modules - Provides high-level overview (i.e. per dll/module cost)
{`{`}*}!=>module $1Group Full Path Module Entries
{`{`}%}!=>module $1Group Module Entries
{`{`}%!*}.%(->class $1;{`{`}%!*}::->class $1Group Classes
{`{`}%!*}.%(=>class $1;{`{`}%!*}::=>class $1Group Class Entries
Thread -> AllThreadsFold Threads
146 |
147 |
148 | ); 149 | } 150 | } -------------------------------------------------------------------------------- /src/spa/src/components/TNode.tsx: -------------------------------------------------------------------------------- 1 | export interface TNode { 2 | exclusiveMetricPercent: number; 3 | inclusiveMetricPercent: number; 4 | hasChildren: boolean; 5 | exclusiveFoldedMetric: number; 6 | inclusiveMetricByTimeString: string; 7 | firstTimeRelativeMSec: number; 8 | lastTimeRelativeMSec: number; 9 | exclusiveCount: number; 10 | inclusiveCount: number; 11 | base64EncodedId: string; 12 | name: string; 13 | path: string; 14 | autoExpand: boolean; 15 | } 16 | 17 | export class EmptyTNode implements TNode { 18 | exclusiveMetricPercent: number; 19 | inclusiveMetricPercent: number; 20 | hasChildren: boolean; 21 | exclusiveFoldedMetric: number; 22 | inclusiveMetricByTimeString: string; 23 | firstTimeRelativeMSec: number; 24 | lastTimeRelativeMSec: number; 25 | exclusiveCount: number; 26 | inclusiveCount: number; 27 | base64EncodedId: string; 28 | name: string; 29 | path: string; 30 | autoExpand: boolean; 31 | 32 | constructor() { 33 | this.exclusiveCount = 0; 34 | this.inclusiveCount = 0; 35 | this.base64EncodedId = ''; 36 | this.name = ''; 37 | this.path = ''; 38 | this.firstTimeRelativeMSec = 0; 39 | this.lastTimeRelativeMSec = 0; 40 | this.inclusiveMetricByTimeString = ''; 41 | this.exclusiveFoldedMetric = 0; 42 | this.hasChildren = false; 43 | this.exclusiveMetricPercent = 0; 44 | this.inclusiveMetricPercent = 0; 45 | this.autoExpand = false; 46 | } 47 | } -------------------------------------------------------------------------------- /src/spa/src/components/TraceInfo.tsx: -------------------------------------------------------------------------------- 1 | import { NavMenu } from './NavMenu'; 2 | import React from 'react'; 3 | 4 | export interface Props { 5 | match: any; 6 | } 7 | 8 | interface State { 9 | traceInfo: TraceInfoInterface | null; 10 | loading: boolean; 11 | } 12 | 13 | interface TraceInfoInterface { 14 | machineName: string; 15 | operatingSystemName: string; 16 | operatingSystemBuildNumber: string; 17 | utcDiff: number; 18 | utcOffsetCurrentProcess: number; 19 | bootTime: string; 20 | startTime: string; 21 | endTime: string; 22 | duration: number; 23 | processorSpeed: number; 24 | numberOfProcessors: number; 25 | memorySize: number; 26 | pointerSize: number; 27 | sampleProfileInterval: number; 28 | totalEvents: number; 29 | lostEvents: number; 30 | fileSize: number; 31 | } 32 | 33 | export class TraceInfo extends React.Component { 34 | 35 | static displayName = TraceInfo.name; 36 | 37 | constructor(props: Props) { 38 | super(props); 39 | this.state = { loading: true, traceInfo: null }; 40 | fetch('/api/traceinfo?filename=' + this.props.match.params.dataFile, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 41 | .then(res => res.json()) 42 | .then(data => { 43 | this.setState({ traceInfo: data, loading: false }); 44 | }); 45 | } 46 | 47 | static renderTraceInfoTable(traceInfo: TraceInfoInterface) { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Info TypeInfo Value
Machine Name{traceInfo.machineName}
Operating System{traceInfo.operatingSystemName}
OS Build Number{traceInfo.operatingSystemBuildNumber}
UTC Diff{traceInfo.utcDiff}
Current UTC (of this tool){traceInfo.utcOffsetCurrentProcess}
OS Boot Time{traceInfo.bootTime}
Trace Start Time{traceInfo.startTime}
Trace End Time{traceInfo.endTime}
Trace Duration (Sec){traceInfo.duration}
CPU Frequency (MHz){traceInfo.processorSpeed}
Number Of Processors{traceInfo.numberOfProcessors}
Memory Size{traceInfo.memorySize}
Sample Profile Interval (MSec){traceInfo.sampleProfileInterval}
Total Events{traceInfo.totalEvents}
Lost Events{traceInfo.lostEvents}
File Size (MB){traceInfo.fileSize}
75 | ); 76 | } 77 | 78 | render() { 79 | let contents = this.state.loading ?

Loading...

: this.state.traceInfo != null ? TraceInfo.renderTraceInfoTable(this.state.traceInfo) : "Null Data"; 80 | 81 | return ( 82 |
83 | 84 |

Trace Info

85 | {contents} 86 |
87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/spa/src/components/TreeNode.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import React from 'react'; 3 | import { TNode } from './TNode'; 4 | import base64url from 'base64url'; 5 | 6 | export interface Props { 7 | routeKey: string; 8 | callTreeNodeId: string; 9 | node: TNode; 10 | indent: number; 11 | autoExpand: boolean; 12 | } 13 | 14 | interface State { 15 | isCollapsed: boolean; 16 | children: TNode[]; 17 | } 18 | 19 | export class TreeNode extends React.PureComponent { 20 | 21 | static displayName = TreeNode.name; 22 | 23 | constructor(props: Props) { 24 | super(props); 25 | this.state = { children: [], isCollapsed: true }; 26 | this.toggleTreeNode = this.toggleTreeNode.bind(this); 27 | this.handleDrillIntoClick = this.handleDrillIntoClick.bind(this); 28 | 29 | if (props.autoExpand === true) { 30 | this.expandTreeNode(); 31 | } 32 | } 33 | 34 | handleDrillIntoClick(d: string) { 35 | 36 | var drillType = '/api/drillinto/exclusive?' 37 | if (d === 'i') { 38 | drillType = '/api/drillinto/inclusive?'; 39 | } 40 | 41 | fetch(drillType + TreeNode.constructAPICacheKeyFromRouteKey(this.props.routeKey) + '&name=' + this.props.callTreeNodeId + '&path=' + this.props.node.path, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 42 | .then(res => res.json()) 43 | .then(data => { 44 | var newRouteKey = JSON.parse(base64url.decode(this.props.routeKey, "utf8")); 45 | newRouteKey.k = data; 46 | window.location.href = '/ui/stackviewer/hotspots/' + base64url.encode(JSON.stringify(newRouteKey)); 47 | }); 48 | } 49 | 50 | toggleTreeNode() { 51 | if (this.state.isCollapsed) { 52 | this.expandTreeNode(); 53 | } 54 | else { 55 | this.collapseTreeNode(); 56 | } 57 | } 58 | 59 | collapseTreeNode() { 60 | this.setState({ children: [], isCollapsed: true }); 61 | } 62 | 63 | expandTreeNode() { 64 | fetch('/api/callerchildren?' + TreeNode.constructAPICacheKeyFromRouteKey(this.props.routeKey) + '&name=' + this.props.callTreeNodeId + '&path=' + this.props.node.path, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) 65 | .then(res => res.json()) 66 | .then(data => { this.setState({ children: data, isCollapsed: false }); }); 67 | } 68 | 69 | static constructAPICacheKeyFromRouteKey(r: string) { 70 | var routeKey = JSON.parse(base64url.decode(r, "utf8")); 71 | return 'filename=' + routeKey.a + '&stackType=' + routeKey.b + '&pid=' + routeKey.c + '&start=' + base64url.encode(routeKey.d, "utf8") + '&end=' + base64url.encode(routeKey.e, "utf8") + '&groupPats=' + base64url.encode(routeKey.f, "utf8") + '&foldPats=' + base64url.encode(routeKey.g, "utf8") + '&incPats=' + base64url.encode(routeKey.h, "utf8") + '&excPats=' + base64url.encode(routeKey.i, "utf8") + '&foldPct=' + routeKey.j + '&drillIntoKey=' + routeKey.k; 72 | } 73 | 74 | static renderTreeNode(routeKey: string, node: TNode, indent: number, callTreeNodeId: string, children: TNode[], autoExpand: boolean, isCollapsed: boolean, toggleTreeNode: TreeNode["toggleTreeNode"], obj: TreeNode) { 75 | return ( 76 | 77 | 78 | {node.hasChildren && } {node.name} 79 | [S] 80 | {node.exclusiveMetricPercent}% 81 | 82 | {node.inclusiveMetricPercent}% 83 | 84 | {node.exclusiveFoldedMetric} 85 | {node.inclusiveMetricByTimeString} 86 | {node.firstTimeRelativeMSec} 87 | {node.lastTimeRelativeMSec} 88 | 89 | {children.map(child => )} 90 | 91 | ); 92 | } 93 | 94 | render() { 95 | return TreeNode.renderTreeNode(this.props.routeKey, this.props.node, this.props.indent, this.props.callTreeNodeId, this.props.node.hasChildren ? this.state.children : [], this.props.node.hasChildren && this.state.children.length === 1, this.state.isCollapsed, this.toggleTreeNode, this); 96 | } 97 | } -------------------------------------------------------------------------------- /src/spa/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | 3 | import App from './App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href'); 9 | const rootElement = document.getElementById('root'); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | rootElement); -------------------------------------------------------------------------------- /src/spa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Microsoft" 6 | } 7 | } 8 | } 9 | --------------------------------------------------------------------------------