blob: 5e0c4e3271d5afc56710cd91f863a81bddd6c5d8 [file] [log] [blame]
// Copyright 2021 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 memory
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"path"
"regexp"
"strings"
"github.com/shirou/gopsutil/process"
"chromiumos/tast/common/perf"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
type diskCategory struct {
pathRE *regexp.Regexp
name string
}
// diskCategories defines categories used to aggregate the fincore memory of
// files used as disks in crosvm.
var diskCategories = []diskCategory{
{
pathRE: regexp.MustCompile(`^/opt/google/vms/android/`),
name: "arcvm_file",
}, {
pathRE: regexp.MustCompile(`.*`),
name: "crosvm_file",
},
}
type fincoreJSONEntry struct {
Resident uint64 `json:"res"`
Pages uint64 `json:"pages"`
Size uint64 `json:"size"`
File string `json:"file"`
}
type fincoreJSON struct {
Fincore []fincoreJSONEntry `json:"fincore"`
}
type fincoreJSONv1 struct {
fincoreJSON
}
// UnmarshalJSON overrides the default JSON parsing to be compatible with an
// older version of fincore output.
func (f fincoreJSONv1) UnmarshalJSON(b []byte) error {
var v1 struct {
Fincore []struct {
Resident uint64 `json:"res,string"`
Pages uint64 `json:"pages,string"`
Size uint64 `json:"size,string"`
File string `json:"file"`
} `json:"fincore"`
}
if err := json.Unmarshal(b, &v1); err != nil {
return err
}
f.Fincore = make([]fincoreJSONEntry, len(v1.Fincore))
for i, e := range v1.Fincore {
f.Fincore[i] = fincoreJSONEntry{
Resident: e.Resident,
Pages: e.Pages,
Size: e.Size,
File: e.File,
}
}
return nil
}
func parseFincoreJSON(ctx context.Context, bytes []byte) (*fincoreJSON, error) {
var v2 fincoreJSON
err2 := json.Unmarshal(bytes, &v2)
if err2 == nil {
return &v2, nil
}
var v1 fincoreJSONv1
err1 := json.Unmarshal(bytes, &v1)
if err1 == nil {
return &(v1.fincoreJSON), nil
}
// Failure, log fincore output and the errors from the previous versions.
testing.ContextLogf(ctx, "Failed to parse fincore output %q", string(bytes))
testing.ContextLog(ctx, "Failed to parse with v1 format: ", err1)
// Return the error from parsing the most recent format.
return nil, errors.Wrap(err2, "failed to parse fincore output")
}
var fincoreArcVMDiskArgRe = regexp.MustCompile("^(--disk|--rwdisk|--root|--rwroot)$")
// CrosvmFincoreMetrics logs a JSON file with the amount resident memory for
// each file used as a disk by crosvm. If p is not nil, the amount of memory
// used by each VM type is logged as perf.Values. If outdir is "", then no logs
// are written.
func CrosvmFincoreMetrics(ctx context.Context, p *perf.Values, outdir, suffix string) error {
// Look for crosvm processes with
processes, err := process.Processes()
const crosvmPath = "/usr/bin/crosvm"
disks := make(map[string]bool)
for _, p := range processes {
if exe, err := p.Exe(); err != nil {
// Some processes don't have a /proc/<pid>/exe, this process might
// have terminated.
continue
} else if exe != crosvmPath {
// We only care about crosvm
continue
}
args, err := p.CmdlineSlice()
if err != nil {
return errors.Wrapf(err, "failed to get arguments for process %d", p.Pid)
}
for i, arg := range args {
if fincoreArcVMDiskArgRe.MatchString(arg) {
if i+1 >= len(args) {
return errors.Errorf("crosvm has --disk arg with no path, args=%v", args)
}
file := strings.Split(args[i+1], ",")[0]
disks[file] = true
}
}
}
if len(disks) == 0 {
return nil
}
args := []string{"--bytes", "--json"}
for disk := range disks {
args = append(args, disk)
}
fincoreBytes, err := testexec.CommandContext(ctx, "fincore", args...).Output(testexec.DumpLogOnError)
if err != nil {
return errors.Wrapf(err, "failed to get fincore for %v", args)
}
if len(outdir) > 0 {
filename := fmt.Sprintf("fincore%s.json", suffix)
if err := ioutil.WriteFile(path.Join(outdir, filename), fincoreBytes, 0644); err != nil {
return errors.Wrapf(err, "failed to write fincore JSON to %s", filename)
}
}
if p == nil {
return nil
}
fincore, err := parseFincoreJSON(ctx, fincoreBytes)
if err != nil {
return errors.Wrap(err, "failed to parse fincore JSON output")
}
metrics := make(map[string]float64)
for _, file := range fincore.Fincore {
for _, category := range diskCategories {
if category.pathRE.MatchString(file.File) {
metrics[category.name] += float64(file.Resident) / MiB
break
}
}
}
for name, resident := range metrics {
p.Set(
perf.Metric{
Name: fmt.Sprintf("%s%s", name, suffix),
Unit: "MiB",
Direction: perf.SmallerIsBetter,
},
resident,
)
}
return nil
}