blob: 7ba59bed9a3fd69b773a3745a662e148df44020c [file] [log] [blame]
// Copyright 2014 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.
package ninjalog
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// Trace is an entry of trace format.
// https://code.google.com/p/trace-viewer/
type Trace struct {
Name string `json:"name"`
Category string `json:"cat"`
EventType string `json:"ph"`
TimestampMicros int `json:"ts"`
DurationMicros int `json:"dur"`
ProcessID int `json:"pid"`
ThreadID int `json:"tid"`
Args map[string]interface{} `json:"args,omitempty"`
ID int `json:"id,omitempty"`
BindingPoint string `json:"bp,omitempty"`
}
// Event types used in conversions.
//
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit#heading=h.puwqg050lyuy
const (
completeEvent = "X"
flowEventStart = "s"
flowEventEnd = "f"
)
type traceByStart []Trace
func (t traceByStart) Len() int { return len(t) }
func (t traceByStart) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t traceByStart) Less(i, j int) bool { return t[i].TimestampMicros < t[j].TimestampMicros }
func toTrace(step Step, pid int, tid int) Trace {
tr := Trace{
Name: step.Out,
Category: step.Category(),
EventType: completeEvent,
TimestampMicros: int(step.Start / time.Microsecond),
DurationMicros: int(step.Duration() / time.Microsecond),
ProcessID: pid,
ThreadID: tid,
}
tr.Args = map[string]interface{}{
"total float": step.TotalFloat.String(),
}
if step.Command != nil {
tr.Args["command"] = step.Command.Command
}
if step.OnCriticalPath {
// Add "critical_path" to category to allow easy searching and highlighting
// in tracer viewer.
//
// Note: given chrome trace viewer does a full text search, searching
// "critical_path" might yield false positives if they happen to have this
// string in their output name or command. If we see this happening often we
// can make this string more unique.
tr.Category += ",critical_path"
tr.Args["drag"] = step.Drag.String()
}
return tr
}
func toFlowEvents(from, to Step, id int, pid int, tids map[string]int) []Trace {
return []Trace{
{
Name: "critical_path",
Category: "critical_path",
EventType: flowEventStart,
// Start flow event arrow from the middle of the "from" event.
TimestampMicros: int((from.Start + from.Duration()/2) / time.Microsecond),
ProcessID: pid,
ThreadID: tids[from.Out],
ID: id,
},
{
Name: "critical_path",
Category: "critical_path",
EventType: flowEventEnd,
// Point flow event arrow to the middle of the "to" event.
TimestampMicros: int((to.Start + to.Duration()/2) / time.Microsecond),
ProcessID: pid,
ThreadID: tids[to.Out],
ID: id,
// Set "enclosing slice" as the binding point. This tells the trace viewer
// `TimestampMicros` is in the middle of the "to" event.
//
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit#heading=h.4qqub5rv9ybk
BindingPoint: "e",
},
}
}
// ToTraces converts Flow outputs into trace log.
//
// If a non-empty `criticalPath` is provided, steps on the critical path will
// have "critical_path" in their category to enable quick searching and
// highlighting.
func ToTraces(steps [][]Step, pid int) []Trace {
var criticalThreads, nonCriticalThreads [][]Step
Outer:
for _, thread := range steps {
// Elevate threads with critical steps to the top of the trace.
for _, s := range thread {
if s.OnCriticalPath {
criticalThreads = append(criticalThreads, thread)
continue Outer
}
}
nonCriticalThreads = append(nonCriticalThreads, thread)
}
steps = append(criticalThreads, nonCriticalThreads...)
var criticalPath []Step
var traces []Trace
// Record `tid`s of all critical steps for generating flow events later.
tids := make(map[string]int)
for tid, thread := range steps {
for _, step := range thread {
traces = append(traces, toTrace(step, pid, tid))
if step.OnCriticalPath {
tids[step.Out] = tid
criticalPath = append(criticalPath, step)
}
}
}
sort.Slice(criticalPath, func(i, j int) bool { return criticalPath[i].Start < criticalPath[j].Start })
// Draw lines (flow events) between all critical steps.
for i := 1; i < len(criticalPath); i++ {
traces = append(traces, toFlowEvents(criticalPath[i-1], criticalPath[i], i, pid, tids)...)
}
sort.Sort(traceByStart(traces))
return traces
}
// clangTrace matches the JSON output format from clang when time-trace is
// enabled.
type clangTrace struct {
// TraceEvents contains all events in this trace.
TraceEvents []Trace
// BeginningOfTimeMicros identifies the time when this clang command started,
// using microseconds since epoch.
BeginningOfTimeMicros int `json:"beginningOfTime"`
}
// ClangTracesToInterleave returns all clang traces that can be interleaved
// directly into their corresponding build steps from `mainTraces`.
//
// `buildRoot` should point to the directory where the Ninja build of
// `mainTrace` is executed, where this function will look for clang traces next
// to object files built by clang. Object files with no clang traces next to
// them are skipped.
//
// Clang traces include events with very short durations, so `granularity` is
// provided to filter them and reduce the size of returned slice.
func ClangTracesToInterleave(mainTraces []Trace, buildRoot string, granularity time.Duration) ([]Trace, error) {
var interleaved []Trace
for _, mainTrace := range mainTraces {
if !strings.HasSuffix(mainTrace.Name, ".o") {
continue
}
// Clang writes a .json file next to the compiled object file when
// time-trace is enabled.
//
// https://releases.llvm.org/9.0.0/tools/clang/docs/ReleaseNotes.html#new-compiler-flags
clangTracePath := filepath.Join(buildRoot, strings.TrimSuffix(mainTrace.Name, ".o")+".json")
if _, err := os.Stat(clangTracePath); err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
traceFile, err := os.Open(clangTracePath)
if err != nil {
return nil, err
}
var cTrace clangTrace
if err := json.NewDecoder(traceFile).Decode(&cTrace); err != nil {
return nil, fmt.Errorf("failed to decode clang trace %s: %w", clangTracePath, err)
}
for _, t := range cTrace.TraceEvents {
// Event names starting with "Total" are sums of event durations of a
// type, for example: "Total Frontend". They are used to form a bar chart
// in clang traces, so they always stat from time 0 on separate threads.
// We exclude them so they don't cause interleaved events to misalign.
if strings.HasPrefix(t.Name, "Total ") || t.EventType != completeEvent || t.DurationMicros < int(granularity/time.Microsecond) {
continue
}
if t.DurationMicros > mainTrace.DurationMicros {
return nil, fmt.Errorf("clang trace for %q has an event %q with duration %dµs, which is longer than the duration of the this clang build %dµs, please make sure they are from the same build", mainTrace.Name, t.Name, t.DurationMicros, mainTrace.DurationMicros)
}
// Left align this event with the corresponding step in the main trace,
// and put them in the same thread, so in the trace viewer it will be
// displayed right below that step.
t.ProcessID = mainTrace.ProcessID
t.ThreadID = mainTrace.ThreadID
t.TimestampMicros += mainTrace.TimestampMicros
interleaved = append(interleaved, t)
}
}
return interleaved, nil
}