blob: 8ab585eb114ebf3fd1b6c8c456a448dab038605c [file] [log] [blame] [edit]
// Copyright 2019 The Chromium OS 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 platform
import (
"bytes"
"context"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"chromiumos/tast/common/perf"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/bundles/cros/platform/fsinfo"
"chromiumos/tast/local/chrome/lacros/launcher"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: ReportDiskUsage,
Desc: "Reports available disk space in the root filesystem",
Contacts: []string{"norvez@chromium.org", "sarthakkukreti@chromium.org", "chromeos-storage@google.com"},
// chromeos-assets is not available on devices without Chrome, require chrome
SoftwareDeps: []string{"chrome"},
Attr: []string{"group:crosbolt", "crosbolt_perbuild"},
})
}
func ReportDiskUsage(ctx context.Context, s *testing.State) {
pv := perf.NewValues()
defer func() {
// Save output for both crosbolt and chromeperf to avoid passing flags and
// ease migration to the preferred chromeperf format. crbug.com/1047454.
// These are very small JSON files so the overhead is minimal.
if err := pv.SaveAs(ctx, s.OutDir(), perf.Crosbolt); err != nil {
s.Error("Failed to save crosbolt perf data: ", err)
}
if err := pv.SaveAs(ctx, s.OutDir(), perf.Chromeperf); err != nil {
s.Error("Failed to save chromeperf data: ", err)
}
}()
// Report the production image size if it exists.
const prodFile = "/root/bytes-rootfs-prod"
if b, err := ioutil.ReadFile(prodFile); err == nil {
if size, err := strconv.ParseInt(string(bytes.TrimSpace(b)), 10, 64); err != nil {
s.Errorf("Failed to parse %q from %v: %v", string(b), prodFile, err)
} else {
pv.Set(perf.Metric{
Name: "bytes_rootfs_prod",
Unit: "bytes",
Direction: perf.SmallerIsBetter,
}, float64(size))
}
}
// Report the live size reported by df.
if info, err := fsinfo.Get(ctx, "/"); err != nil {
s.Error("Failed to get information about root filesystem: ", err)
} else {
pv.Set(perf.Metric{
Name: "bytes_rootfs_test",
Unit: "bytes",
Direction: perf.SmallerIsBetter,
}, float64(info.Used))
}
// Report the size of specific directories that are particularly large.
metrics := map[string]string{
"/opt/": "bytes_opt",
"/opt/google/chrome/": "bytes_chrome",
"/usr/bin": "bytes_bin",
"/usr/sbin": "bytes_sbin",
"/usr/share/chromeos-assets/": "bytes_assets",
"/usr/share/chromeos-assets/input_methods/": "bytes_ime",
"/usr/share/chromeos-assets/speech_synthesis/": "bytes_tts",
"/usr/share/fonts/": "bytes_fonts",
}
switch arch := runtime.GOARCH; arch {
case "amd64":
metrics["/usr/lib64/"] = "bytes_lib"
case "arm64":
metrics["/usr/lib64/"] = "bytes_lib"
case "arm":
metrics["/usr/lib/"] = "bytes_lib"
default:
s.Errorf("Unsupported architecture %q", arch)
}
if arc.Supported() {
if t, ok := arc.Type(); ok {
switch t {
case arc.Container:
metrics[arc.ARCPath] = "bytes_arc"
metrics[filepath.Join(arc.ARCPath, "system.raw.img")] = "bytes_arc_system_raw_img"
metrics[filepath.Join(arc.ARCPath, "vendor.raw.img")] = "bytes_arc_vendor_raw_img"
case arc.VM:
metrics[arc.ARCVMPath] = "bytes_arc"
metrics[filepath.Join(arc.ARCVMPath, "system.raw.img")] = "bytes_arc_system_raw_img"
metrics[filepath.Join(arc.ARCVMPath, "vendor.raw.img")] = "bytes_arc_vendor_raw_img"
default:
s.Errorf("Unsupported ARC type %d", t)
}
} else {
s.Error("Failed to detect ARC type")
}
}
// Report the lacros squashfs size, if it exists.
if _, err := os.Stat(launcher.LacrosSquashFSPath); err == nil {
metrics[launcher.LacrosSquashFSPath] = "bytes_rootfs_lacros"
}
// Log the size of the individual files/directories inside |path|.
//
// Having the size information of individual files and directories available
// will make debugging size regressions easier.
//
// To reduce the amount of lines printed only the files/directories bigger
// than 1M are displayed.
logFileSizes := func(path string) {
const minFileSize = "1000000" // in bytes
duCmd := testexec.CommandContext(ctx, "du",
"-a", // Print files and directories.
"-x", // Do not include mounted filesystems.
"-B1", // Print results in bytes.
"--max-depth=3", // Recurse 3 directories deep.
"--threshold", minFileSize, // Don't include objects smaller than this.
path,
)
sortCmd := testexec.CommandContext(ctx, "sort", "-nr")
out, err := pipeAndGetOutput(duCmd, sortCmd)
if err != nil {
s.Error("du command failed: ", err)
return
}
s.Logf("All files and directories bigger than %s bytes in %q%s%s", minFileSize, path, "\n", string(out))
s.Log("")
}
// Find the space used in a directory, in bytes.
//
// This function uses 'du' to find the size of directories on a live DUT.
// In practice this test will run on "test" images that are ever so
// slightly different from the "base" image, so the results could be
// slightly different from production images. However:
// - the difference is tiny, overall size difference between base and
// test images is <100KB
// - if anything the size will be slightly larger on the test image, so
// we're monitoring the worst case scenario anyway
dirSize := func(path string) (int64, error) {
cmd := testexec.CommandContext(ctx, "du", "-s", "-B1", "-x", path)
out, err := cmd.Output(testexec.DumpLogOnError)
if err != nil {
return 0, errors.Wrap(err, "du command failed")
}
// Parse the output from a "du -s -B1 -x <path>" command.
//
// The output is expected to have the following form:
// 419024896 /opt/google/chrome/
fields := strings.Fields(strings.TrimSpace(string(out)))
if len(fields) != 2 {
return 0, errors.Errorf("expected 2 fields in line %q", string(out))
}
size, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil {
return 0, errors.Errorf("failed to parse value %q", fields[0])
}
return size, nil
}
// Go through the metrics in the same order every time.
keys := make([]string, 0, len(metrics))
for k := range metrics {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if size, err := dirSize(k); err != nil {
s.Errorf("Failed to get the size of directory %q: %v", k, err)
} else {
logFileSizes(k)
pv.Set(perf.Metric{
Name: metrics[k],
Unit: "bytes",
Direction: perf.SmallerIsBetter,
}, float64(size))
}
}
}
// pipeAndGetOutput executes both commands, and pipes the output of |first|
// to the input of |second| (so it does "first | second").
//
// It returns the output of |second|.
func pipeAndGetOutput(first, second *testexec.Cmd) ([]byte, error) {
var result bytes.Buffer
var err error
second.Stdin, err = first.StdoutPipe()
if err != nil {
return nil, errors.Wrap(err, "failed piping command")
}
second.Stdout = &result
if err := first.Start(); err != nil {
return nil, errors.Wrap(err, "failed first.Start")
}
if err := second.Start(); err != nil {
return nil, errors.Wrap(err, "failed second.Start")
}
if err := first.Wait(); err != nil {
return nil, errors.Wrap(err, "failed first.Wait")
}
if err := second.Wait(); err != nil {
return nil, errors.Wrap(err, "failed second.Wait")
}
return result.Bytes(), nil
}