@@ -4,8 +4,10 @@ import (
44 "bufio"
55 "bytes"
66 "context"
7+ "encoding/json"
78 "fmt"
89 "os/exec"
10+ "regexp"
911 "strconv"
1012 "strings"
1113 "time"
@@ -141,24 +143,244 @@ func (p *goPprofManager) convertPprofToRaw(pprofFilePath string) (string, error)
141143
142144}
143145
146+ type Sample struct {
147+ Count int
148+ CPUCost int
149+ StackIDs []int
150+ StackFuncs []string
151+ }
152+
153+ type Node struct {
154+ Name string `json:"name"`
155+ File string `json:"file,omitempty"`
156+ Value int `json:"value"`
157+ Children []* Node `json:"children"`
158+ childMap map [string ]* Node
159+ }
160+
161+ func (m * goPprofManager ) convertRawToJson (content string ) (string , error ) {
162+ lines := strings .Split (content , "\n " )
163+ header := make (map [string ]interface {})
164+ var rawSamples []Sample
165+ locations := make (map [int ][]string )
166+ mappings := make (map [int ]string )
167+
168+ mode := ""
169+ var currentLocID int
170+
171+ sampleRegex := regexp .MustCompile (`^\s*(\d+)\s+(\d+):\s+(.+)` )
172+ locLineRegex := regexp .MustCompile (`^\d+:` )
173+
174+ for _ , line := range lines {
175+ line = strings .TrimSpace (line )
176+ if mode == "" {
177+ switch {
178+ case strings .HasPrefix (line , "PeriodType:" ):
179+ header ["PeriodType" ] = strings .TrimSpace (strings .SplitN (line , ":" , 2 )[1 ])
180+ case strings .HasPrefix (line , "Period:" ):
181+ val , _ := strconv .Atoi (strings .TrimSpace (strings .SplitN (line , ":" , 2 )[1 ]))
182+ header ["Period" ] = val
183+ case strings .HasPrefix (line , "Time:" ):
184+ header ["Time" ] = strings .TrimSpace (strings .SplitN (line , ":" , 2 )[1 ])
185+ case strings .HasPrefix (line , "Duration:" ):
186+ val , _ := strconv .ParseFloat (strings .TrimSpace (strings .SplitN (line , ":" , 2 )[1 ]), 64 )
187+ header ["Duration" ] = val
188+ case strings .HasPrefix (line , "samples/count" ):
189+ mode = "samples"
190+ }
191+ } else if mode == "samples" {
192+ if line == "" || strings .HasPrefix (line , "Locations" ) {
193+ mode = "locations"
194+ continue
195+ }
196+ if match := sampleRegex .FindStringSubmatch (line ); match != nil {
197+ count , _ := strconv .Atoi (match [1 ])
198+ cost , _ := strconv .Atoi (match [2 ])
199+ rawIDs := strings .Fields (match [3 ])
200+ stackIDs := []int {}
201+ for _ , idStr := range rawIDs {
202+ id , _ := strconv .Atoi (idStr )
203+ stackIDs = append (stackIDs , id )
204+ }
205+ rawSamples = append (rawSamples , Sample {Count : count , CPUCost : cost , StackIDs : stackIDs })
206+ }
207+ } else if mode == "locations" {
208+ if line == "" || strings .HasPrefix (line , "Mappings" ) {
209+ mode = "mappings"
210+ continue
211+ }
212+ if locLineRegex .MatchString (line ) {
213+ parts := strings .SplitN (line , ":" , 2 )
214+ currentLocID , _ = strconv .Atoi (parts [0 ])
215+ frame := strings .TrimSpace (parts [1 ])
216+ if frame != "" {
217+ locations [currentLocID ] = append (locations [currentLocID ], frame )
218+ }
219+ } else {
220+ locations [currentLocID ] = append (locations [currentLocID ], line )
221+ }
222+ } else if mode == "mappings" {
223+ if locLineRegex .MatchString (line ) {
224+ parts := strings .SplitN (line , ":" , 2 )
225+ id , _ := strconv .Atoi (parts [0 ])
226+ mappings [id ] = strings .TrimSpace (parts [1 ])
227+ }
228+ }
229+ }
230+
231+ var resolved []Sample
232+ for _ , s := range rawSamples {
233+ var funcs []string
234+ for _ , id := range s .StackIDs {
235+ frames := locations [id ]
236+ for _ , f := range frames {
237+ fn := strings .Fields (f )
238+ if len (fn ) > 0 {
239+ funcs = append (funcs , fn [0 ])
240+ }
241+ }
242+ }
243+ s .StackFuncs = funcs
244+ resolved = append (resolved , s )
245+ }
246+
247+ result := map [string ]interface {}{
248+ "header" : header ,
249+ "samples" : resolved ,
250+ "locations" : locations ,
251+ "mappings" : mappings ,
252+ }
253+ samples := result ["samples" ].([]Sample )
254+ locations = result ["locations" ].(map [int ][]string )
255+
256+ root := m .buildTree (samples , locations )
257+
258+ outBytes , err := json .MarshalIndent (root , "" , " " )
259+ if err != nil {
260+ return "" , errors .Wrap (err , "failed to marshal JSON" )
261+ }
262+ out := string (outBytes )
263+ return out , nil
264+ }
265+
266+ func (m * goPprofManager ) buildTree (samples []Sample , locations map [int ][]string ) * Node {
267+ reverseStrings := func (s []string ) []string {
268+ for i := 0 ; i < len (s )/ 2 ; i ++ {
269+ s [i ], s [len (s )- 1 - i ] = s [len (s )- 1 - i ], s [i ]
270+ }
271+ return s
272+ }
273+ getSourceFile := func (fn string , locID int ) string {
274+ for _ , line := range locations [locID ] {
275+ match := regexp .MustCompile (`(.+\.go):(\d+):` ).FindStringSubmatch (line )
276+ if match != nil {
277+ return match [1 ]
278+ }
279+ }
280+ return ""
281+ }
282+
283+ addStack := func (node * Node , stack []string , locIDs []int , value int ) {
284+ for i , fn := range stack {
285+ var locID int
286+ if i < len (locIDs ) {
287+ locID = locIDs [i ]
288+ }
289+ src := getSourceFile (fn , locID )
290+ key := fn
291+ if src != "" {
292+ key += "@" + src
293+ }
294+
295+ child , exists := node .childMap [key ]
296+ if ! exists {
297+ child = & Node {
298+ Name : fn ,
299+ File : src ,
300+ Value : 0 ,
301+ Children : []* Node {},
302+ childMap : make (map [string ]* Node ),
303+ }
304+ node .Children = append (node .Children , child )
305+ node .childMap [key ] = child
306+ }
307+ child .Value += value
308+ node = child
309+ }
310+ }
311+
312+ root := & Node {
313+ Name : "root" ,
314+ Children : []* Node {},
315+ childMap : make (map [string ]* Node ),
316+ }
317+
318+ for _ , s := range samples {
319+ stack := reverseStrings (s .StackFuncs )
320+ ids := m .reverseInts (s .StackIDs )
321+ addStack (root , stack , ids , s .CPUCost )
322+ root .Value += s .CPUCost
323+ }
324+
325+ m .cleanTree (root )
326+ return root
327+ }
328+
329+ func (m * goPprofManager ) cleanTree (n * Node ) {
330+ n .childMap = nil
331+ for _ , child := range n .Children {
332+ m .cleanTree (child )
333+ }
334+ }
335+
336+ func (m * goPprofManager ) reverseStrings (s []string ) []string {
337+ for i := 0 ; i < len (s )/ 2 ; i ++ {
338+ s [i ], s [len (s )- 1 - i ] = s [len (s )- 1 - i ], s [i ]
339+ }
340+ return s
341+ }
342+
343+ func (m * goPprofManager ) reverseInts (s []int ) []int {
344+ for i := 0 ; i < len (s )/ 2 ; i ++ {
345+ s [i ], s [len (s )- 1 - i ] = s [len (s )- 1 - i ], s [i ]
346+ }
347+ return s
348+ }
349+
144350func (m * goPprofManager ) fetchProfileFromPID (job * job.ProfilingJob ) error {
145351 port , err := findListeningPortForPID (job .PID )
146352 if err != nil {
147353 log .ErrorLogLn (fmt .Sprintf ("failed to find listening port for PID %s: %s" , job .PID , err ))
148354 port = "8080"
149355 log .DebugLogLn (fmt .Sprintf ("using default port %s" , port ))
150356 }
151- rawFilePath := common .GetResultFile (common .TmpDir (), job .Tool , job .OutputType , job .PID , job .Iteration )
357+ resultFilePath := common .GetResultFile (common .TmpDir (), job .Tool , job .OutputType , job .PID , job .Iteration )
152358 if job .OutputType == api .HeapDump {
153- err = m .heapProfile (job , port , rawFilePath )
359+ err = m .heapProfile (job , port , resultFilePath )
154360 if err != nil {
155361 return errors .Wrapf (err , "failed to fetch heap profile for PID %s" , job .PID )
156362 }
157363 } else if job .OutputType == api .Pprof {
158- err = m .cpuProfile (job , port , rawFilePath )
364+ err = m .cpuProfile (job , port , resultFilePath )
159365 if err != nil {
160366 return errors .Wrapf (err , "failed to fetch CPU profile for PID %s" , job .PID )
161367 }
368+ } else if job .OutputType == api .FlameJson {
369+ err = m .cpuProfile (job , port , resultFilePath )
370+ profileFilePath := common .GetResultFile (common .TmpDir (), job .Tool , "cpu" , job .PID , job .Iteration )
371+ if err != nil {
372+ return errors .Wrapf (err , "failed to fetch CPU profile for PID %s" , job .PID )
373+ }
374+ profileRaw , err := m .convertPprofToRaw (profileFilePath )
375+ if err != nil {
376+ return errors .Wrapf (err , "failed to convert CPU profile for PID %s" , job .PID )
377+ }
378+ flameJson , err := m .convertRawToJson (profileRaw )
379+ if err != nil {
380+ return errors .Wrapf (err , "failed to convert raw profile to JSON for PID %s" , job .PID )
381+ }
382+ file .Write (resultFilePath , flameJson )
383+
162384 } else if job .OutputType == api .Raw {
163385 profileFilePath := common .GetResultFile (common .TmpDir (), job .Tool , "cpu" , job .PID , job .Iteration )
164386 err = m .cpuProfile (job , port , profileFilePath )
@@ -178,12 +400,12 @@ func (m *goPprofManager) fetchProfileFromPID(job *job.ProfilingJob) error {
178400 if err != nil {
179401 return errors .Wrapf (err , "failed to convert heap profile for PID %s" , job .PID )
180402 }
181- file .Write (rawFilePath , fmt .Sprintf ("heap dump\n %s \n cpu dump\n %s" , heapRaw , profileRaw ))
403+ file .Write (resultFilePath , fmt .Sprintf ("heap dump\n %s \n cpu dump\n %s" , heapRaw , profileRaw ))
182404 } else {
183405 return errors .New ("unsupported output type for Go pprof profiler" )
184406 }
185407 // Finally, publish the file as before
186- return m .publisher .Do (job .Compressor , rawFilePath , job .OutputType )
408+ return m .publisher .Do (job .Compressor , resultFilePath , job .OutputType )
187409}
188410
189411func findListeningPortForPID (pid string ) (string , error ) {
0 commit comments