blob: 5229530c8003efeb33f54a62cd332b9e34efb268 [file] [log] [blame]
// Copyright 2020 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 lacros
import (
"context"
"fmt"
"io/ioutil"
"regexp"
"strconv"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/lacros"
"chromiumos/tast/testing"
)
type testMode int
const (
openURLMode testMode = iota
openTabMode
)
type testParams struct {
mode testMode
url string
numTabs int
}
func init() {
testing.AddTest(&testing.Test{
Func: Memory,
LacrosStatus: testing.LacrosVariantExists,
Desc: "Tests lacros memory usage",
Contacts: []string{"erikchen@chromium.org", "hidehiko@chromium.org", "edcourtney@chromium.org", "lacros-team@google.com"},
SoftwareDeps: []string{"chrome"},
Fixture: "lacros",
Timeout: 60 * time.Minute,
Params: []testing.Param{{
Name: "blank",
Val: testParams{mode: openURLMode, url: "about:blank"},
}, {
Name: "docs",
Val: testParams{mode: openURLMode, url: "https://docs.google.com/document/d/1_WmgE1F5WUrhwkPqJis3dWyOiUmQKvpXp5cd4w86TvA/edit"},
}, {
Name: "reddit",
Val: testParams{mode: openURLMode, url: "https://old.reddit.com/"},
}, {
Name: "youtube",
Val: testParams{mode: openURLMode, url: "https://www.youtube.com/watch?v=uS33jC2VYNU"},
}, {
Name: "twentytabs",
Val: testParams{mode: openTabMode, numTabs: 20},
},
},
})
}
// findMatch looks for lines of the form `[stat]: 123 kB` and sums the
// numerical values, returning the output in bytes.
func findMatch(input []byte, stat string) (int, error) {
re := regexp.MustCompile(stat + `:\s*(\d*)\s*kB`)
results := re.FindAllSubmatch(input, -1)
sum := 0
for _, result := range results {
n, err := strconv.Atoi(string(result[1]))
if err != nil {
return 0, err
}
sum += n * 1024
}
return sum, nil
}
// procSum is a complex function.
// 1. It finds all processes whose command line includes path.
// 2. It queries /proc/{pid}/{endpoint} for each process.
// 3. It filters and sums across all statistics that match stat.
func procSum(ctx context.Context, path, endpoint, stat string) (int, error) {
pids, err := lacros.PidsFromPath(ctx, path)
if err != nil {
return 0, errors.Wrap(err, "failed to get pids for "+path)
}
var total = 0
for _, pid := range pids {
// Query /proc. Ignore errors reading the file because the
// process may no longer exist.
content, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/%s", pid, endpoint))
if err == nil {
value, err := findMatch(content, stat)
if err != nil {
return 0, errors.Wrap(err, "failed to find match")
}
total += value
}
}
return total, nil
}
// measureProcesses returns memory estimates for all processes that contain a path
// in their command line. The first int is (RssAnon + VmSwap). This is Chrome's
// definition of PrivateMemoryFootprint, and serves as an underestimate of
// memory usage. The second int is (Pss). This is an overestimate of memory
// usage.
func measureProcesses(ctx context.Context, path string) (int, int, error) {
j, err := procSum(ctx, path, "status", "RssAnon")
if err != nil {
return 0, 0, err
}
k, err := procSum(ctx, path, "status", "VmSwap")
if err != nil {
return 0, 0, err
}
p, err := procSum(ctx, path, "smaps", "Pss")
if err != nil {
return 0, 0, err
}
return j + k, p, nil
}
// measureBothChrome measures the current memory usage of both lacros-chrome and
// ash-chrome. Returns (pmf, pss) in bytes.
func measureBothChrome(ctx context.Context, s *testing.State) (int, int) {
// As a rule of thumb, we wait 60 seconds before taking any
// measurements. This gives time for previous operations to finish and
// the system to quiesce. In particular, both lacros-chrome and
// ash-chrome will sometimes spawn/keep around unnecessary
// processes, but most will go away after 60 seconds.
testing.Sleep(ctx, 60*time.Second)
tconn, err := s.FixtValue().(chrome.HasChrome).Chrome().TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to connect to test API: ", err)
}
info, err := lacros.InfoSnapshot(ctx, tconn)
if err != nil {
s.Fatal("Failed to get lacros info: ", err)
}
pmf, pss, err := measureProcesses(ctx, info.LacrosPath)
if err != nil {
s.Fatal("Failed to measure memory of lacros-chrome: ", err)
}
chromeosChromePath := "/opt/google/chrome"
pmf1, pss1, err := measureProcesses(ctx, chromeosChromePath)
if err != nil {
s.Fatal("Failed to measure memory of ash-chrome: ", err)
}
return pmf + pmf1, pss + pss1
}
// setWindowSize sets the last focused window to 800x600 in size. For lacros
// windows, use the lacros TestConn. For ash, use the ash TestConn.
func setWindowSize(ctx context.Context, tconn *chrome.TestConn) error {
// Set the window to 800x600 in size.
if err := tconn.Call(ctx, nil, `async () => {
const win = await tast.promisify(chrome.windows.getLastFocused)();
await tast.promisify(chrome.windows.update)(win.id, {width: 800, height:600, state:"normal"});
}`); err != nil {
return errors.Wrap(err, "setting window size failed")
}
return nil
}
// Memory is a basic test for lacros memory usage. It measures the PMF and PSS
// overhead for lacros-chrome with a single about:blank tab. It also makes the
// same measurements for ash-chrome. This estimate is not perfect. For
// example, this test does not measure the size of the ash-chrome test API
// extension, but it does include the extension for lacros-chrome.
// Furthermore, this test does not have fine control over ash-chrome,
// which may choose to spawn/kill utility or renderer processes for its own
// purposes. My running the same code 10 times, outliers become obvious.
func Memory(ctx context.Context, s *testing.State) {
params := s.Param().(testParams)
url := params.url
for i := 0; i < 10; i++ {
// Measure memory before launching lacros-chrome.
pmf1, pss1 := measureBothChrome(ctx, s)
tconn, err := s.FixtValue().(chrome.HasChrome).Chrome().TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to connect to test API: ", err)
}
l, err := lacros.Launch(ctx, tconn)
if err != nil {
s.Fatal("Failed to launch lacros-chrome: ", err)
}
ltconn, err := l.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to get lacros-chrome TestConn: ", err)
}
if err := setWindowSize(ctx, ltconn); err != nil {
s.Fatal("Failed to set lacros-chrome window size: ", err)
}
if params.mode == openTabMode {
if err := openTabsLacros(ctx, l, params.numTabs); err != nil {
s.Fatal("Failed to oepn lacros-chrome tabs: ", err)
}
} else {
if err := navigateSingleTabToURLLacros(ctx, url, l); err != nil {
s.Fatal("Failed to open a lacros tab: ", err)
}
}
// Measure memory after launching lacros-chrome.
pmf2, pss2 := measureBothChrome(ctx, s)
testing.ContextLogf(ctx, "lacros-chrome RssAnon + VmSwap (MB): %v. Pss (MB): %v ", (pmf2-pmf1)/1024/1024, (pss2-pss1)/1024/1024)
// Close lacros-chrome
l.Close(ctx)
// Measure memory before launching ash-chrome.
pmf3, pss3 := measureBothChrome(ctx, s)
var conns []*chrome.Conn
if params.mode == openTabMode {
conns, err = openTabsChromeOS(ctx, s.FixtValue().(chrome.HasChrome).Chrome(), params.numTabs)
if err != nil {
s.Fatal("Failed to open ash-chrome tabs: ", err)
}
} else {
// Open a new tab to url.
conn, err := s.FixtValue().(chrome.HasChrome).Chrome().NewConn(ctx, url)
if err != nil {
s.Fatal("Failed to open ash-chrome tab: ", err)
}
conns = append(conns, conn)
}
for _, conn := range conns {
defer conn.Close()
}
if err := setWindowSize(ctx, tconn); err != nil {
s.Fatal("Failed to set lacros-chrome window size: ", err)
}
// Measure memory after launching ash-chrome.
pmf4, pss4 := measureBothChrome(ctx, s)
testing.ContextLogf(ctx, "ash-chrome RssAnon + VmSwap (MB): %v. Pss (MB): %v ", (pmf4-pmf3)/1024/1024, (pss4-pss3)/1024/1024)
// Close ash-chrome
for _, conn := range conns {
conn.CloseTarget(ctx)
}
}
}
// navigateSingleTabToURLLacros assumes that there's a freshly launched instance
// of lacros-chrome, with a single tab open to about:blank, then, navigates the
// blank tab to the given url.
func navigateSingleTabToURLLacros(ctx context.Context, url string, l *lacros.Lacros) error {
// Open a new tab and navigate to url.
conn, err := l.NewConnForTarget(ctx, chrome.MatchTargetURL("about:blank"))
if err != nil {
return errors.Wrap(err, "failed to find an about:blank tab")
}
defer conn.Close()
if err := conn.Navigate(ctx, url); err != nil {
return errors.Wrapf(err, "failed to navigate to %q", url)
}
return nil
}
// openTabsLacros assumes that lacros-chrome has been freshly launched,
// with a single tab opened to about:blank.
func openTabsLacros(ctx context.Context, l *lacros.Lacros, numTabs int) error {
for i := 0; i < numTabs-1; i++ {
// Open a new tab and navigate to about blank
conn, err := l.NewConn(ctx, "about:blank")
if err != nil {
return err
}
if err := conn.Close(); err != nil {
return err
}
// Wait one second to quiesce.
testing.Sleep(ctx, time.Second)
}
return nil
}
// openTabsChromeOS assumes that ash-chrome is running, but that
// there is no open window.
func openTabsChromeOS(ctx context.Context, c *chrome.Chrome, numTabs int) ([]*chrome.Conn, error) {
var conns []*chrome.Conn
for i := 0; i < numTabs; i++ {
conn, err := c.NewConn(ctx, "about:blank")
if err != nil {
for _, conn := range conns {
conn.Close()
}
return nil, err
}
conns = append(conns, conn)
}
return conns, nil
}