Implement reclient metrics uploading
This cl will be submitted after the cl to add the metadata to the reproxy.cfg file has been submitted to chromium/src: crrev/c/4513215 Bug: b/281504726 Change-Id: Ifa6d5f56d4a85ccb9ec8e4f70207d8b6b9382e89 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4516023 Commit-Queue: Ben Segall <bentekkie@google.com> Reviewed-by: Philipp Wollermann <philwo@google.com> Reviewed-by: Junji Watanabe <jwata@google.com> Reviewed-by: Takuto Ikuta <tikuta@chromium.org> Reviewed-by: Gavin Mak <gavinmak@google.com>changes/23/4516023/15
parent
04afb4b256
commit
530d86d40b
@ -0,0 +1,61 @@
|
||||
# Reclient metric collection
|
||||
|
||||
[TOC]
|
||||
|
||||
## Overview
|
||||
|
||||
When chromium developers enable download_remoteexec_cfg and run their build with use_remoteexec enabled,
|
||||
|
||||
e.g.
|
||||
|
||||
`.gclient`
|
||||
```
|
||||
solutions = [
|
||||
{
|
||||
"name": "src",
|
||||
"url": "https://chromium.googlesource.com/chromium/src.git",
|
||||
"managed": False,
|
||||
"custom_deps": {},
|
||||
"custom_vars": {
|
||||
...
|
||||
"download_remoteexec_cfg": True,
|
||||
...
|
||||
},
|
||||
...
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
```
|
||||
$ gclient runhooks
|
||||
$ gn gen -C out/Release --args="use_remoteexec=true"
|
||||
$ autoninja -C out/Release chrome
|
||||
```
|
||||
|
||||
reproxy uploads reclient's build metrics. The download_remoteexec_cfg gclient flag is only available for Google employees.
|
||||
|
||||
Before uploading metrics, reproxy will show a message 10 times to warn users that we will collect build metrics.
|
||||
|
||||
Users can opt in by running the following command.
|
||||
$ python3 reclient_metrics.py opt-in
|
||||
|
||||
Users can opt out by running the following command.
|
||||
$ python3 reclient_metrics.py opt-out
|
||||
|
||||
## What type of data are collected?
|
||||
|
||||
We upload the contents of <ninja-out>/.reproxy_tmp/logs/rbe_metrics.txt.
|
||||
This contains
|
||||
* Flags passed to reproxy
|
||||
* Auth related flags are filtered out by reproxy
|
||||
* Start and end time of build tasks
|
||||
* Aggregated durations and counts of events during remote build actions
|
||||
* OS (e.g. Win, Mac or Linux)
|
||||
* Number of cpu cores and the amount of RAM of the building machine
|
||||
|
||||
We don't collect personally identifiable information
|
||||
(e.g. username, ip address).
|
||||
|
||||
## Why are reproxy metrics collected? / How are the metrics collected?
|
||||
|
||||
We (Chrome build team/Reclient team) collect build metrics to find slow build tasks that harm developer's productivity. Based on collected stats, we find the place/build tasks where we need to focus on. Also we use collected stats to track Chrome build performance on developer's machine. We'll use these stats to measure how much we can/can't improve build performance on developer machines.
|
||||
@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2023 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
"""This script manages the counter for how many times developers should
|
||||
be notified before uploading reclient metrics."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
THIS_DIR = os.path.dirname(__file__)
|
||||
CONFIG = os.path.join(THIS_DIR, 'reclient_metrics.cfg')
|
||||
VERSION = 1
|
||||
|
||||
|
||||
def default_config():
|
||||
return {
|
||||
'is-googler': is_googler(),
|
||||
'countdown': 10,
|
||||
'version': VERSION,
|
||||
}
|
||||
|
||||
|
||||
def load_config():
|
||||
config = None
|
||||
try:
|
||||
with open(CONFIG) as f:
|
||||
raw_config = json.load(f)
|
||||
if raw_config['version'] == VERSION:
|
||||
raw_config['countdown'] = max(0, raw_config['countdown'] - 1)
|
||||
config = raw_config
|
||||
except Exception:
|
||||
pass
|
||||
if not config:
|
||||
config = default_config()
|
||||
save_config(config)
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config):
|
||||
with open(CONFIG, 'w') as f:
|
||||
json.dump(config, f)
|
||||
|
||||
|
||||
def show_message(config, ninja_out):
|
||||
print("""
|
||||
Your reclient metrics will be uploaded to the chromium build metrics database. The uploaded metrics will be used to analyze user side build performance.
|
||||
|
||||
We upload the contents of {ninja_out_abs}.
|
||||
This contains
|
||||
* Flags passed to reproxy
|
||||
* Auth related flags are filtered out by reproxy
|
||||
* Start and end time of build tasks
|
||||
* Aggregated durations and counts of events during remote build actions
|
||||
* OS (e.g. Win, Mac or Linux)
|
||||
* Number of cpu cores and the amount of RAM of the building machine
|
||||
|
||||
Uploading reclient metrics will be started after you run autoninja another {config_count} time(s).
|
||||
|
||||
If you don't want to upload reclient metrics, please run following command.
|
||||
$ python3 {file_path} opt-out
|
||||
|
||||
If you want to allow upload reclient metrics from next autoninja run, please run the
|
||||
following command.
|
||||
$ python3 {file_path} opt-in
|
||||
|
||||
If you have questions about this, please send an email to foundry-x@google.com
|
||||
|
||||
You can find a more detailed explanation in
|
||||
{metrics_readme_path}
|
||||
or
|
||||
https://chromium.googlesource.com/chromium/tools/depot_tools/+/main/reclient_metrics.README.md
|
||||
""".format(
|
||||
ninja_out_abs=os.path.abspath(
|
||||
os.path.join(ninja_out, ".reproxy_tmp", "logs", "rbe_metrics.txt")),
|
||||
config_count=config.get("countdown", 0),
|
||||
file_path=__file__,
|
||||
metrics_readme_path=os.path.abspath(
|
||||
os.path.join(THIS_DIR, "reclient_metrics.README.md")),
|
||||
))
|
||||
|
||||
|
||||
def is_googler(config=None):
|
||||
"""Check whether this user is Googler or not."""
|
||||
if config is not None and 'is-googler' in config:
|
||||
return config['is-googler']
|
||||
# Use cipd auth-info to check for googler status as
|
||||
# downloading rewrapper configs already requires cipd to be logged in
|
||||
p = subprocess.run('cipd auth-info',
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
shell=True)
|
||||
if p.returncode != 0:
|
||||
return False
|
||||
lines = p.stdout.splitlines()
|
||||
if len(lines) == 0:
|
||||
return False
|
||||
l = lines[0]
|
||||
# |l| will be like 'Logged in as <user>@google.com.' for googlers.
|
||||
return l.startswith('Logged in as ') and l.endswith('@google.com.')
|
||||
|
||||
|
||||
def check_status(ninja_out):
|
||||
"""Checks metrics collections status and shows notice to user if needed.
|
||||
|
||||
Returns True if metrics should be collected."""
|
||||
config = load_config()
|
||||
if not is_googler(config):
|
||||
return False
|
||||
if 'opt-in' in config:
|
||||
return config['opt-in']
|
||||
if config.get("countdown", 0) > 0:
|
||||
show_message(config, ninja_out)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main(argv):
|
||||
cfg = load_config()
|
||||
|
||||
if not is_googler(cfg):
|
||||
save_config(cfg)
|
||||
return 0
|
||||
|
||||
if len(argv) == 2 and argv[1] == 'opt-in':
|
||||
cfg['opt-in'] = True
|
||||
cfg['countdown'] = 0
|
||||
save_config(cfg)
|
||||
print('reclient metrics upload is opted in.')
|
||||
return 0
|
||||
|
||||
if len(argv) == 2 and argv[1] == 'opt-out':
|
||||
cfg['opt-in'] = False
|
||||
save_config(cfg)
|
||||
print('reclient metrics upload is opted out.')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||
@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2023 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import io
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
|
||||
import reclient_metrics
|
||||
|
||||
|
||||
class ReclientMetricsTest(unittest.TestCase):
|
||||
def test_is_googler(self):
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
self.assertTrue(reclient_metrics.is_googler())
|
||||
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 1
|
||||
self.assertFalse(reclient_metrics.is_googler())
|
||||
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = ''
|
||||
self.assertFalse(reclient_metrics.is_googler())
|
||||
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as foo@example.com.'
|
||||
self.assertFalse(reclient_metrics.is_googler())
|
||||
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
self.assertTrue(reclient_metrics.is_googler({
|
||||
'is-googler': True,
|
||||
}))
|
||||
self.assertFalse(reclient_metrics.is_googler({
|
||||
'is-googler': False,
|
||||
}))
|
||||
run_mock.assert_not_called()
|
||||
|
||||
def test_load_and_save_config(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
cfg1 = reclient_metrics.load_config()
|
||||
self.assertDictEqual(
|
||||
cfg1, {
|
||||
'is-googler': True,
|
||||
'countdown': 10,
|
||||
'version': reclient_metrics.VERSION,
|
||||
})
|
||||
reclient_metrics.save_config(cfg1)
|
||||
cfg2 = reclient_metrics.load_config()
|
||||
self.assertDictEqual(
|
||||
cfg2, {
|
||||
'is-googler': True,
|
||||
'countdown': 9,
|
||||
'version': reclient_metrics.VERSION,
|
||||
})
|
||||
run_mock.assert_called_once()
|
||||
|
||||
def test_check_status(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
for i in range(10):
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs",
|
||||
"rbe_metrics.txt"), stdout_mock.getvalue())
|
||||
self.assertIn("you run autoninja another %d time(s)" % (10 - i),
|
||||
stdout_mock.getvalue())
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertTrue(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@example.com.'
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 1
|
||||
run_mock.return_value.stdout = ''
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
reclient_metrics.main(["reclient_metrics.py", "opt-in"])
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertTrue(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
for i in range(3):
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs",
|
||||
"rbe_metrics.txt"), stdout_mock.getvalue())
|
||||
self.assertIn("you run autoninja another %d time(s)" % (10 - i),
|
||||
stdout_mock.getvalue())
|
||||
reclient_metrics.main(["reclient_metrics.py", "opt-in"])
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertTrue(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@example.com.'
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
reclient_metrics.main(["reclient_metrics.py", "opt-in"])
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
reclient_metrics.CONFIG = os.path.join(tmpdir, 'reclient_metrics.cfg')
|
||||
with unittest.mock.patch('subprocess.run') as run_mock:
|
||||
run_mock.return_value.returncode = 0
|
||||
run_mock.return_value.stdout = 'Logged in as abc@google.com.'
|
||||
for i in range(3):
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs",
|
||||
"rbe_metrics.txt"), stdout_mock.getvalue())
|
||||
self.assertIn("you run autoninja another %d time(s)" % (10 - i),
|
||||
stdout_mock.getvalue())
|
||||
reclient_metrics.main(["reclient_metrics.py", "opt-out"])
|
||||
with unittest.mock.patch('sys.stdout',
|
||||
new=io.StringIO()) as stdout_mock:
|
||||
self.assertFalse(reclient_metrics.check_status("outdir"))
|
||||
self.assertNotIn("Your reclient metrics will", stdout_mock.getvalue())
|
||||
self.assertNotIn(
|
||||
os.path.join("outdir", ".reproxy_tmp", "logs", "rbe_metrics.txt"),
|
||||
stdout_mock.getvalue())
|
||||
run_mock.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue