blob: 9fd0998be4566ae12b348d5b0c24a0bd4185195f [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This Go utility takes in a JSON tracing file and converts it to a CSV file.
// This CSV file includes information needed for a playback utility
// //zircon/system/uapp/blk-playback/blk-playback.c which re-creates
// events that have previously occured on a block device.
package main
import (
"encoding/csv"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strconv"
)
const OneSecInUsecs float64 = 1000000
const OneMsecInUsecs float64 = 1000
var (
verbose = false
)
func check(e error) {
if e != nil {
panic(e)
}
}
func checkError(message string, err error) {
if err != nil {
log.Fatal(message, err)
}
}
type Trace struct {
TraceEvents []TraceEvent
DisplayTimeUnit string
}
type TraceEvent struct {
Cat string
Name string
Ph string
Pid int
Tid int
Ts float64
Id int
Dur float64
Args map[string]interface{}
}
type Thread struct {
pid int
tid int
}
type CatAndId struct {
cat string
id int
}
func printTraceEvent(e TraceEvent) {
fmt.Printf(
"ph=%s %.2f %-15s %-20s\tpid=%-8d\ttid=%-8d\tid=%-8d\tDur=%.4g\t%s\n",
e.Ph, e.Ts, e.Cat, e.Name, e.Pid, e.Tid, e.Id, e.Dur, e.Args)
}
func convert(duration []TraceEvent) [][]string {
outputData := make([][]string, 0, len(duration)+1)
// Add columm headings
outputData = append(outputData, []string{
"category",
"name",
"start_time",
"end_time",
"duration",
"tid",
"length",
"offset_dev",
"command",
"offset_vmo",
})
for _, event := range duration {
outputData = append(outputData, []string{
event.Cat,
event.Name,
strconv.FormatFloat(event.Ts, 'f', -1, 64),
strconv.FormatFloat(event.Ts+event.Dur, 'f', -1, 64),
strconv.FormatFloat(event.Dur, 'f', -1, 64),
strconv.Itoa(event.Tid),
strconv.Itoa(int(event.Args["length"].(float64))),
strconv.Itoa(int(event.Args["offset_dev"].(float64))),
strconv.Itoa(int(event.Args["command"].(float64))),
strconv.Itoa(int(event.Args["offset_vmo"].(float64))),
})
}
return outputData
}
// Returns a list with only one event per duration event.
// The |Dur| member is populated with duration.
func calculateEventDurations(events []TraceEvent, category string) []TraceEvent {
durations := make([]TraceEvent, 0)
eventStacks := make(map[Thread][]TraceEvent)
asyncEvents := make(map[CatAndId]TraceEvent)
for _, event := range events {
ph := event.Ph
thread := Thread{event.Pid, event.Tid}
cat := event.Cat
if verbose {
printTraceEvent(event)
}
if cat != category {
continue
}
if ph == "X" {
// Complete event
durations = append(durations, event)
} else if ph == "b" {
// Async begin duration event
asyncEvents[CatAndId{event.Cat, event.Id}] = event
} else if ph == "e" {
// Async end duration event
beginEvent, ok := asyncEvents[CatAndId{event.Cat, event.Id}]
if ok {
if beginEvent.Cat != event.Cat {
panic("Category for begin and end event does not match")
}
if beginEvent.Id != event.Id {
panic("Id for begin and end event does not match")
}
// Set duration on the end event.
event.Dur = event.Ts - beginEvent.Ts
mergeArgs(&beginEvent, &event)
durations = append(durations, event)
}
} else if ph == "B" {
// Begin duration event.
eventStacks[thread] = append(eventStacks[thread], event)
} else if ph == "E" {
// End duration event.
eventStack := eventStacks[thread]
if eventStack != nil && len(eventStack) > 0 {
// Peek at last event
beginEvent := eventStack[len(eventStack)-1]
if beginEvent.Cat != event.Cat || beginEvent.Name != event.Name {
// This is possible since events are not necessarily in
// chronological order; they are grouped by source. So, when
// processing a new batch of events, it's possible that we
// get an end event that didn't have a begin event because
// we started tracing mid-event.
eventStacks[thread] = nil
continue
}
// Pop last event from event stack.
eventStacks[thread] = eventStack[:len(eventStack)-1]
// Set duration on the end event.
event.Dur = event.Ts - beginEvent.Ts
mergeArgs(&beginEvent, &event)
durations = append(durations, event)
}
}
}
return durations
}
func mergeArgs(event1 *TraceEvent, event2 *TraceEvent) {
// Merge 'Args' maps of both events.
if event1.Args != nil && event2.Args == nil {
event2.Args = event1.Args
} else if event1.Args != nil && event2.Args != nil {
for k, v := range event2.Args {
event1.Args[k] = v
}
event2.Args = event1.Args
}
}
type ByTimestamp []TraceEvent
func (a ByTimestamp) Len() int { return len(a) }
func (a ByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByTimestamp) Less(i, j int) bool { return a[i].Ts < a[j].Ts }
func main() {
verbosePtr := flag.Bool("v", false, "verbose mode")
flag.Parse()
verbose = *verbosePtr
if len(flag.Args()) != 3 {
log.Fatal("Usage: driver_trace_parsing trace_category trace_in_file_name parsing_out_file_name")
}
category := flag.Args()[0]
inputFilename := flag.Args()[1]
outputFilename := flag.Args()[2]
traceFile, err := ioutil.ReadFile(inputFilename)
check(err)
var trace Trace
err = json.Unmarshal([]byte(traceFile), &trace)
check(err)
events := trace.TraceEvents
if events == nil || len(events) == 0 {
panic("No events found")
}
// Get a list of the duration events. |events| must be in its original order
// before calling this; sorting can cause the ordering requirements to be
// violated.
durations := calculateEventDurations(events, category)
// Convert trace events to the actual output data
outputData := convert(durations)
fmt.Printf("Received %d tracing records.\n", len(outputData))
outputFile, err := os.Create(outputFilename)
checkError("Cannot create file", err)
defer outputFile.Close()
writer := csv.NewWriter(outputFile)
defer writer.Flush()
for _, value := range outputData {
err := writer.Write(value)
checkError("Cannot write to file", err)
}
fmt.Printf("Wrote output to csv file '%s'.\n", outputFilename)
}