1
+ // Copyright (c) Alexandre Mutel. All rights reserved.
2
+ // Licensed under the BSD-Clause 2 license.
3
+ // See license.txt file in the project root for full license information.
4
+
5
+ using Ultra . Core . Markers ;
6
+
7
+ namespace Ultra . Core ;
8
+
9
+ /// <summary>
10
+ /// Generates a markdown report from a Firefox profile.
11
+ /// </summary>
12
+ internal sealed class MarkdownReportGenerator
13
+ {
14
+ private readonly FirefoxProfiler . Profile _profile ;
15
+ private readonly StreamWriter _writer ;
16
+
17
+ private MarkdownReportGenerator ( FirefoxProfiler . Profile profile , StreamWriter writer )
18
+ {
19
+ _profile = profile ;
20
+ _writer = writer ;
21
+ }
22
+
23
+ /// <summary>
24
+ /// Generates a markdown report from a Firefox profile.
25
+ /// </summary>
26
+ /// <param name="profile">The Firefox profile.</param>
27
+ /// <param name="writer">The writer to write the markdown report.</param>
28
+ public static void Generate ( FirefoxProfiler . Profile profile , StreamWriter writer )
29
+ {
30
+ var generator = new MarkdownReportGenerator ( profile , writer ) ;
31
+ generator . Generate ( ) ;
32
+ }
33
+
34
+ private void Generate ( )
35
+ {
36
+ var pidAndNameList = new HashSet < ProcessInfo > ( _profile . Threads . Select ( x => new ProcessInfo ( x . Pid , x . ProcessName ) ) ) ;
37
+
38
+ _writer . WriteLine ( $ "# Ultra Report for \\ [{ string . Join ( ", " , pidAndNameList . Select ( x => x . Name ) ) } ]") ;
39
+ _writer . WriteLine ( ) ;
40
+ _writer . WriteLine ( $ "_Generated on { DateTime . Now : s} _") ;
41
+ _writer . WriteLine ( ) ;
42
+
43
+ foreach ( var pidAndName in pidAndNameList )
44
+ {
45
+ var threads = _profile . Threads . Where ( x => string . Equals ( x . Pid , pidAndName . Pid , StringComparison . OrdinalIgnoreCase ) ) . ToList ( ) ;
46
+ GenerateProcess ( pidAndName , threads ) ;
47
+ }
48
+ }
49
+
50
+ private void GenerateProcess ( ProcessInfo processInfo , List < FirefoxProfiler . Thread > threads )
51
+ {
52
+ _writer . WriteLine ( $ "## Process { processInfo . Name } ") ;
53
+ _writer . WriteLine ( ) ;
54
+
55
+ GenerateJit ( threads ) ;
56
+
57
+ _writer . WriteLine ( ) ;
58
+ _writer . WriteLine ( "_Report generated by [ultra](https://github.com/xoofx/ultra)_" ) ;
59
+ }
60
+
61
+ private void GenerateJit ( List < FirefoxProfiler . Thread > threads )
62
+ {
63
+ var jitEvents = CollectMarkersFromThreads < JitCompileEvent > ( threads , EtwConverterToFirefox . CategoryJit ) ;
64
+
65
+ if ( jitEvents . Count == 0 )
66
+ {
67
+ return ;
68
+ }
69
+
70
+ double totalTime = 0.0 ;
71
+ long totalILSize = 0 ;
72
+
73
+ Dictionary < string , ( double DurationInMs , long ILSize , int MethodCount ) > namespaceStats = new ( StringComparer . Ordinal ) ;
74
+
75
+ // Sort by duration descending
76
+ jitEvents . Sort ( ( left , right ) => right . DurationInMs . CompareTo ( left . DurationInMs ) ) ;
77
+
78
+ foreach ( var jitEvent in jitEvents )
79
+ {
80
+ totalTime += jitEvent . DurationInMs ;
81
+ totalILSize += jitEvent . Data . MethodILSize ;
82
+
83
+ var ns = GetNamespace ( jitEvent . Data . MethodNamespace ) ;
84
+ var indexOfLastDot = ns . LastIndexOf ( '.' ) ;
85
+ ns = indexOfLastDot > 0 ? ns . Substring ( 0 , indexOfLastDot ) : "<no namespace>" ;
86
+
87
+ if ( ! namespaceStats . TryGetValue ( ns , out var stats ) )
88
+ {
89
+ stats = ( 0 , 0 , 0 ) ;
90
+ }
91
+
92
+ stats . DurationInMs += jitEvent . DurationInMs ;
93
+ stats . ILSize += jitEvent . Data . MethodILSize ;
94
+ stats . MethodCount ++ ;
95
+
96
+ namespaceStats [ ns ] = stats ;
97
+ }
98
+
99
+ _writer . WriteLine ( "### JIT Statistics" ) ;
100
+ _writer . WriteLine ( ) ;
101
+
102
+ _writer . WriteLine ( $ "- Total JIT time: `{ totalTime : 0.0} ms`") ;
103
+ _writer . WriteLine ( $ "- Total JIT IL size: `{ totalILSize } `") ;
104
+
105
+ _writer . WriteLine ( ) ;
106
+ _writer . WriteLine ( "#### JIT Top 10 Namespaces" ) ;
107
+ _writer . WriteLine ( ) ;
108
+
109
+ _writer . WriteLine ( "| Namespace | Duration (ms) | IL Size| Methods |" ) ;
110
+ _writer . WriteLine ( "|-----------|---------------|--------|-------" ) ;
111
+ var cumulativeTotalTime = 0.0 ;
112
+ foreach ( var ( namespaceName , stats ) in namespaceStats . OrderByDescending ( x => x . Value . DurationInMs ) )
113
+ {
114
+ _writer . WriteLine ( $ "| ``{ namespaceName } `` | `{ stats . DurationInMs : 0.0} ` | `{ stats . ILSize } ` |`{ stats . MethodCount } ` |") ;
115
+ cumulativeTotalTime += stats . DurationInMs ;
116
+ if ( cumulativeTotalTime > totalTime * 0.9 )
117
+ {
118
+ break ;
119
+ }
120
+ }
121
+
122
+ // TODO: Add a report for Generic Namespace arguments to namespace (e.g ``System.Collections.Generic.List`1[MyNamespace.MyClass...]`)
123
+ // MyNamespace.MyClass should be reported as a separate namespace that contributes to System.Collections
124
+
125
+ _writer . WriteLine ( ) ;
126
+ _writer . WriteLine ( "#### JIT Top 10 Methods" ) ;
127
+ _writer . WriteLine ( ) ;
128
+ _writer . WriteLine ( "| Method | Duration (ms) | IL Size" ) ;
129
+ _writer . WriteLine ( "|--------|---------------|--------|" ) ;
130
+ foreach ( var jitEvent in jitEvents . Take ( 10 ) )
131
+ {
132
+ _writer . WriteLine ( $ "| ``{ jitEvent . Data . FullName } `` | `{ jitEvent . DurationInMs : 0.0} ` | `{ jitEvent . Data . MethodILSize } ` |") ;
133
+ }
134
+ }
135
+
136
+ private static List < PayloadEvent < TPayload > > CollectMarkersFromThreads < TPayload > ( List < FirefoxProfiler . Thread > threads , int category ) where TPayload : FirefoxProfiler . MarkerPayload
137
+ {
138
+ var markers = new List < PayloadEvent < TPayload > > ( ) ;
139
+ foreach ( var thread in threads )
140
+ {
141
+ var threadMarkers = thread . Markers ;
142
+ var markerLength = threadMarkers . Length ;
143
+ for ( int i = 0 ; i < markerLength ; i ++ )
144
+ {
145
+ if ( threadMarkers . Category [ i ] == category )
146
+ {
147
+ var payload = ( TPayload ) threadMarkers . Data [ i ] ! ;
148
+ var duration = threadMarkers . EndTime [ i ] ! . Value - threadMarkers . StartTime [ i ] ! . Value ;
149
+ markers . Add ( new ( payload , duration ) ) ;
150
+ }
151
+ }
152
+ }
153
+ return markers ;
154
+ }
155
+
156
+ private static string GetNamespace ( string fullTypeName )
157
+ {
158
+ var index = fullTypeName . IndexOf ( '`' ) ; // For generics
159
+ if ( index > 0 )
160
+ {
161
+ fullTypeName = fullTypeName . Substring ( 0 , index ) ;
162
+ }
163
+ index = fullTypeName . LastIndexOf ( '.' ) ;
164
+ return index > 0 ? fullTypeName . Substring ( 0 , index ) : "<no namespace>" ;
165
+ }
166
+
167
+ private record struct ProcessInfo ( string Pid , string ? Name ) ;
168
+
169
+ private record struct PayloadEvent < TPayload > ( TPayload Data , double DurationInMs ) where TPayload : FirefoxProfiler . MarkerPayload ;
170
+ }
0 commit comments