| // 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. |
| |
| package bloaty |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "os" |
| "os/exec" |
| "sort" |
| "strings" |
| "sync" |
| ) |
| |
| type bloatyOutput struct { |
| data row |
| file string |
| err error |
| } |
| |
| // TODO(jakehehrlich): Add reading ids.txt to elflib, since there are now three |
| // different tools that each need to read it for mostly unrelated reasons |
| func getFiles(idsPath string) ([]string, error) { |
| out := []string{} |
| idsFile, err := os.Open(idsPath) |
| if err != nil { |
| return nil, err |
| } |
| defer idsFile.Close() |
| scanner := bufio.NewScanner(idsFile) |
| for line := 1; scanner.Scan(); line++ { |
| parts := strings.SplitN(scanner.Text(), " ", 2) |
| if len(parts) != 2 { |
| return nil, fmt.Errorf("invalid ids.txt: error on line %d", line) |
| } |
| out = append(out, parts[1]) |
| } |
| return out, nil |
| } |
| |
| func run(bloatyPath, file string, out chan<- bloatyOutput) { |
| args := []string{ |
| "-d", "segments,compileunits,symbols", |
| "-s", "file", |
| "--tsv", |
| "-n", "0", |
| file, |
| } |
| cmd := exec.Command(bloatyPath, args...) |
| fmt.Printf("running: %s\n", file) |
| stdout, err := cmd.StdoutPipe() |
| if err != nil { |
| out <- bloatyOutput{err: fmt.Errorf("pipe: %s: %s", file, err)} |
| return |
| } |
| |
| var stderr bytes.Buffer |
| cmd.Stderr = &stderr |
| |
| if err := cmd.Start(); err != nil { |
| out <- bloatyOutput{err: fmt.Errorf("start (%s): %s: %s\n", err, file, cmd.Stderr)} |
| stdout.Close() |
| return |
| } |
| |
| err = ReadCSV(stdout, out, file) |
| if err != nil { |
| out <- bloatyOutput{err: fmt.Errorf("csv: %s: %s", file, err)} |
| stdout.Close() |
| return |
| } |
| |
| if err := cmd.Wait(); err != nil { |
| out <- bloatyOutput{err: fmt.Errorf("wait (%s): %s: %s\n", err, file, cmd.Stderr)} |
| return |
| } |
| } |
| |
| func updateSymbol(newSym *row, file string, sym *Symbol) { |
| sym.Name = newSym.Symbol |
| sym.Vmsz += newSym.Vmsz |
| sym.Filesz += newSym.Filesz |
| sym.Binaries = append(sym.Binaries, file) |
| } |
| |
| func addRowToOutput(r *row, file string, output map[string]*Segment) { |
| if _, ok := output[r.Seg]; !ok { |
| output[r.Seg] = &Segment{make(map[string]*File)} |
| } |
| seg := output[r.Seg] |
| |
| if _, ok := seg.Files[r.File]; !ok { |
| seg.Files[r.File] = &File{Symbols: make(map[string]*Symbol)} |
| } |
| f := seg.Files[r.File] |
| |
| if _, ok := f.Symbols[r.Symbol]; !ok { |
| f.Symbols[r.Symbol] = &Symbol{} |
| } |
| updateSymbol(r, file, f.Symbols[r.Symbol]) |
| seg.Files[r.File] = f |
| output[r.Seg] = seg |
| } |
| |
| func getTopN(fileSizes map[string]uint64, topFiles, topSyms uint64, output *map[string]*Segment) { |
| // If both topFiles and topSyms are 0, bail early because we're returning everything. |
| if topFiles == 0 && topSyms == 0 { |
| return |
| } |
| type sortedFile struct { |
| name string |
| size uint64 |
| } |
| |
| smallFiles := make(map[string]uint64) |
| if topFiles > 0 && topFiles < uint64(len(fileSizes)) { |
| var all []struct { |
| name string |
| size uint64 |
| } |
| for name, size := range fileSizes { |
| all = append(all, sortedFile{name, size}) |
| } |
| sort.Slice(all, func(i, j int) bool { |
| return all[i].size < all[j].size |
| }) |
| |
| for _, d := range all[:uint64(len(all))-topFiles] { |
| smallFiles[d.name] = d.size |
| } |
| } |
| |
| for _, segData := range *output { |
| smallFilesSize := uint64(0) |
| for file, fileData := range segData.Files { |
| smallSyms := Symbol{Name: "all small syms"} |
| // If the file labeled a small file, add to small files size and delete the sym data. |
| if size, exists := smallFiles[file]; exists { |
| smallFilesSize += size |
| delete(segData.Files, file) |
| } else if topSyms > 0 && topSyms < uint64(len(fileData.Symbols)) { |
| var all []*Symbol |
| for _, sym := range fileData.Symbols { |
| all = append(all, sym) |
| } |
| sort.Slice(all, func(i, j int) bool { |
| return all[i].Filesz < all[j].Filesz |
| }) |
| |
| for _, d := range all[:uint64(len(all))-topSyms] { |
| if sym, exists := fileData.Symbols[d.Name]; exists { |
| smallSyms.Vmsz += sym.Vmsz |
| smallSyms.Filesz += sym.Filesz |
| delete(fileData.Symbols, d.Name) |
| } |
| } |
| } |
| |
| if topSyms > 0 { |
| fileData.Symbols["all small syms"] = &smallSyms |
| } |
| } |
| |
| if topFiles > 0 { |
| segData.Files["all small files"] = &File{TotalFilesz: smallFilesSize} |
| } |
| } |
| } |
| |
| // RunBloaty runs bloaty on all files in ids.txt, and returns a mapping of the |
| // symbols and files by segment. |
| func RunBloaty(bloatyPath, idsPath string, topFiles, topSyms uint64, jobs int) (map[string]*Segment, error) { |
| files, err := getFiles(idsPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| var wg sync.WaitGroup |
| data := make(chan bloatyOutput) |
| output := make(map[string]*Segment) |
| fileSizes := make(map[string]uint64) |
| |
| // Start up the data collection process. |
| dataComplete := make(chan struct{}, 1) |
| go func() { |
| for d := range data { |
| if d.err != nil { |
| fmt.Printf("%v", d.err) |
| continue |
| } |
| addRowToOutput(&d.data, d.file, output) |
| fileSizes[d.data.File] += d.data.Filesz |
| } |
| dataComplete <- struct{}{} |
| }() |
| |
| // Only allow up to max jobs concurrent executions. |
| sem := make(chan struct{}, jobs) |
| |
| // Start a bloaty run on each file. |
| for _, file := range files { |
| wg.Add(1) |
| sem <- struct{}{} |
| go func(file string) { |
| defer wg.Done() |
| run(bloatyPath, file, data) |
| <-sem |
| }(file) |
| } |
| |
| wg.Wait() |
| close(data) |
| <-dataComplete |
| |
| getTopN(fileSizes, topFiles, topSyms, &output) |
| return output, nil |
| } |