blob: c2e183f7da5e5e774f8866d2ce3ae1bf74821a5b [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 perfboot
import (
"bufio"
"context"
"io/ioutil"
"regexp"
"strconv"
"strings"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/chrome"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
// GetPerfValues parses ARC log files and extracts performance metrics Android boot flow.
func GetPerfValues(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC) (map[string]time.Duration, error) {
const (
logcatTimeout = 30 * time.Second
// logcatLastEventTag is the last event tag to be processed.
// The test should read logcat until this tag appears.
logcatLastEventTag = "boot_progress_enable_screen"
// logcatIgnoreEventTag is a logcat event tags to be ignored.
// TODO(niwa): Clean this up after making PerfBoot reboot DUT.
// (Using time of boot_progress_system_run makes sense only after rebooting DUT.)
logcatIgnoreEventTag = "boot_progress_system_run"
)
// logcatEventEntryRegexp extracts boot pregress event name and time from a logcat entry.
logcatEventEntryRegexp := regexp.MustCompile(`\d+ I (boot_progress_[^:]+): (\d+)`)
var arcStartTimeMS float64
if err := tconn.Eval(ctx, "tast.promisify(chrome.autotestPrivate.getArcStartTime)()", &arcStartTimeMS); err != nil {
return nil, errors.Wrap(err, "failed to run getArcStartTime()")
}
adjustedArcStartTime := time.Duration(arcStartTimeMS * float64(time.Millisecond))
testing.ContextLogf(ctx, "ARC start time in host clock: %fs", adjustedArcStartTime.Seconds())
vmEnabled, err := arc.VMEnabled()
if err != nil {
return nil, errors.Wrap(err, "failed to check whether ARCVM is enabled")
}
if vmEnabled {
clockDelta, err := clockDelta(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain clock delta")
}
// Guest clock and host clock are different on ARCVM, so we adjust ARC start time.
// adjustedArcStartTime is expected to be a negative value.
adjustedArcStartTime -= clockDelta
testing.ContextLogf(ctx, "ARC start time in guest clock: %fs", adjustedArcStartTime.Seconds())
}
// Set timeout for the logcat command below.
ctx, cancel := context.WithTimeout(ctx, logcatTimeout)
defer cancel()
cmd := a.Command(ctx, "logcat", "-b", "events", "-v", "threadtime")
cmdStr := shutil.EscapeSlice(cmd.Args)
pipe, err := cmd.StdoutPipe()
if err != nil {
return nil, errors.Wrapf(err, "failed to obtain a pipe for %s", cmdStr)
}
if err := cmd.Start(); err != nil {
return nil, errors.Wrapf(err, "failed to start %s", cmdStr)
}
defer func() {
cmd.Kill()
cmd.Wait()
}()
p := make(map[string]time.Duration)
lastEventSeen := false
testing.ContextLog(ctx, "Scanning logcat output")
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
l := scanner.Text()
m := logcatEventEntryRegexp.FindStringSubmatch(l)
if m == nil {
continue
}
eventTag := m[1]
if eventTag == logcatIgnoreEventTag {
continue
}
eventTimeMs, err := strconv.ParseInt(m[2], 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to extract event time from %q", l)
}
p[eventTag] = time.Duration(eventTimeMs*int64(time.Millisecond)) - adjustedArcStartTime
if eventTag == logcatLastEventTag {
lastEventSeen = true
break
}
}
if err := scanner.Err(); err != nil {
return nil, errors.Wrap(err, "error while scanning logcat")
}
if !lastEventSeen {
return nil, errors.Errorf("timeout while waiting for event %q to appear in logcat",
logcatLastEventTag)
}
return p, nil
}
// clockDelta returns (the host's CLOCK_MONOTONIC - the guest's CLOCK_MONOTONIC) as time.Duration.
func clockDelta(ctx context.Context) (time.Duration, error) {
// /proc/timer_list contains a line which says "now at %Ld nsecs".
// This clock value comes from CLOCK_MONOTONIC (see the kernel's kernel/time/timer_list.c).
parse := func(output string) (int64, error) {
for _, line := range strings.Split(output, "\n") {
tokens := strings.Split(line, " ")
if len(tokens) == 4 && tokens[0] == "now" && tokens[1] == "at" && tokens[3] == "nsecs" {
return strconv.ParseInt(tokens[2], 10, 64)
}
}
return 0, errors.Errorf("unexpected format of /proc/timer_list: %q", output)
}
// Use android-sh to read /proc/timer_list which only root can read.
out, err := arc.BootstrapCommand(ctx, "/system/bin/cat", "/proc/timer_list").Output(testexec.DumpLogOnError)
if err != nil {
return 0, errors.Wrap(err, "failed to read guest's /proc/timer_list")
}
guestClockNS, err := parse(string(out))
if err != nil {
return 0, errors.Wrap(err, "failed to prase guest's /proc/timer_list")
}
out, err = ioutil.ReadFile("/proc/timer_list")
if err != nil {
return 0, errors.Wrap(err, "failed to read host's /proc/timer_list")
}
hostClockNS, err := parse(string(out))
if err != nil {
return 0, errors.Wrap(err, "failed to prase host's /proc/timer_list")
}
testing.ContextLogf(ctx, "Host clock: %d ns, Guest clock: %d ns", hostClockNS, guestClockNS)
return time.Duration((hostClockNS - guestClockNS) * int64(time.Nanosecond)), nil
}