blob: eebf76591034c4c046cddb9c184548d21ba73786 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2017 The ChromiumOS Authors
Allen Li51bb6122017-06-21 12:04:13 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Process metrics."""
6
7from __future__ import absolute_import
Allen Li51bb6122017-06-21 12:04:13 -07008
Allen Li3992c662018-01-05 15:26:36 -08009from functools import partial
Chris McDonald59650c32021-07-20 15:29:28 -060010import logging
Allen Li3992c662018-01-05 15:26:36 -080011
Mike Frysingercb56b642019-08-25 15:33:08 -040012import psutil # pylint: disable=import-error
Allen Li51bb6122017-06-21 12:04:13 -070013
Allen Lia9c6e802017-07-11 15:42:47 -070014from chromite.lib import metrics
Allen Li51bb6122017-06-21 12:04:13 -070015
Chris McDonald59650c32021-07-20 15:29:28 -060016
Allen Li51bb6122017-06-21 12:04:13 -070017logger = logging.getLogger(__name__)
18
Allen Lia9c6e802017-07-11 15:42:47 -070019_count_metric = metrics.GaugeMetric(
Alex Klein1699fab2022-09-08 08:46:06 -060020 "proc/count", description="Number of processes currently running."
21)
Congbin Guo16b64d52023-02-10 17:50:30 -080022_thread_count_metric = metrics.GaugeMetric(
23 "proc/thread_count", description="Number of threads currently running."
24)
Allen Lia9c6e802017-07-11 15:42:47 -070025_cpu_percent_metric = metrics.GaugeMetric(
Alex Klein1699fab2022-09-08 08:46:06 -060026 "proc/cpu_percent", description="CPU usage percent of processes."
27)
Congbin Guo18b4ed72023-02-11 19:16:16 -080028_cpu_times_metric = metrics.CumulativeMetric(
29 "proc/cpu_times",
30 description="Accumulated CPU time in each specific mode of processes.",
31)
Congbin Guocf2750c2023-02-11 21:25:16 -080032_read_count_metric = metrics.CounterMetric(
33 "proc/read/count",
34 description="Accumulated read operation count of processes.",
35)
36_read_bytes_metric = metrics.CounterMetric(
37 "proc/read/bytes", description="Accumulated read bytes of processes."
38)
39_read_chars_metric = metrics.CounterMetric(
40 "proc/read/chars",
41 description="Accumulated buffered read bytes of processes.",
42)
43_write_count_metric = metrics.CounterMetric(
44 "proc/write/count",
45 description="Accumulated write operation count of processes.",
46)
47_write_bytes_metric = metrics.CounterMetric(
48 "proc/write/bytes", description="Accumulated write bytes of processes."
49)
50_write_chars_metric = metrics.CounterMetric(
51 "proc/write/chars",
52 description="Accumulated buffered write bytes of processes.",
53)
Allen Li51bb6122017-06-21 12:04:13 -070054
55
56def collect_proc_info():
Alex Klein1699fab2022-09-08 08:46:06 -060057 collector = _ProcessMetricsCollector()
58 collector.collect()
Allen Li6bb74d52017-06-22 14:44:53 -070059
60
61class _ProcessMetricsCollector(object):
Alex Klein1699fab2022-09-08 08:46:06 -060062 """Class for collecting process metrics."""
Allen Li6bb74d52017-06-22 14:44:53 -070063
Congbin Guo18b4ed72023-02-11 19:16:16 -080064 # We need to store some per process metrics of last run in order to
65 # calculate the detla and aggregate them.
66 old_cpu_times = {}
Congbin Guocf2750c2023-02-11 21:25:16 -080067 old_io_counters = {}
Congbin Guo18b4ed72023-02-11 19:16:16 -080068
Alex Klein1699fab2022-09-08 08:46:06 -060069 def __init__(self):
70 self._metrics = [
Congbin Guo4ccf0632023-02-12 00:01:14 -080071 _ProcessMetric("adb", test_func=partial(_is_process_name, "adb")),
Alex Klein1699fab2022-09-08 08:46:06 -060072 _ProcessMetric("autoserv", test_func=_is_parent_autoserv),
Congbin Guo522cd982022-10-06 11:47:28 -070073 _ProcessMetric(
Congbin Guo4ccf0632023-02-12 00:01:14 -080074 "bbagent", test_func=partial(_is_process_name, "bbagent")
75 ),
76 _ProcessMetric(
Congbin Guo3cdc11e2022-10-11 16:02:32 -070077 "cache-downloader",
Congbin Guofcb436b2023-01-23 20:36:01 -080078 test_func=partial(_is_process_name, "downloader"),
Congbin Guo3cdc11e2022-10-11 16:02:32 -070079 ),
Congbin Guoa8432502023-01-23 20:31:01 -080080 _ProcessMetric("cipd", test_func=partial(_is_process_name, "cipd")),
Congbin Guo3cdc11e2022-10-11 16:02:32 -070081 _ProcessMetric(
Congbin Guo4ccf0632023-02-12 00:01:14 -080082 "cloudtail", test_func=partial(_is_process_name, "cloudtail")
83 ),
84 _ProcessMetric(
Congbin Guo522cd982022-10-06 11:47:28 -070085 "common-tls", test_func=partial(_is_process_name, "common-tls")
86 ),
Alex Klein1699fab2022-09-08 08:46:06 -060087 _ProcessMetric("curl", test_func=partial(_is_process_name, "curl")),
88 _ProcessMetric(
Congbin Guo522cd982022-10-06 11:47:28 -070089 "dnsmasq", test_func=partial(_is_process_name, "dnsmasq")
Alex Klein1699fab2022-09-08 08:46:06 -060090 ),
91 _ProcessMetric(
Congbin Guo522cd982022-10-06 11:47:28 -070092 "drone-agent",
Congbin Guofcb436b2023-01-23 20:36:01 -080093 test_func=partial(_is_process_name, "drone-agent"),
Congbin Guo522cd982022-10-06 11:47:28 -070094 ),
95 _ProcessMetric(
96 "fleet-tlw", test_func=partial(_is_process_name, "fleet-tlw")
97 ),
98 _ProcessMetric(
99 "getty", test_func=partial(_is_process_name, "getty")
Alex Klein1699fab2022-09-08 08:46:06 -0600100 ),
101 _ProcessMetric(
102 "gs_offloader",
103 test_func=partial(_is_process_name, "gs_offloader.py"),
104 ),
105 _ProcessMetric("gsutil", test_func=_is_gsutil),
106 _ProcessMetric("java", test_func=partial(_is_process_name, "java")),
Congbin Guo4ccf0632023-02-12 00:01:14 -0800107 _ProcessMetric("k8s_system", test_func=_is_k8s_system),
Alex Klein1699fab2022-09-08 08:46:06 -0600108 _ProcessMetric(
Congbin Guo522cd982022-10-06 11:47:28 -0700109 "labservice", test_func=partial(_is_process_name, "labservice")
110 ),
111 _ProcessMetric(
Alex Klein1699fab2022-09-08 08:46:06 -0600112 "lxc-attach", test_func=partial(_is_process_name, "lxc-attach")
113 ),
114 _ProcessMetric(
115 "lxc-start", test_func=partial(_is_process_name, "lxc-start")
116 ),
Congbin Guoa8432502023-01-23 20:31:01 -0800117 _ProcessMetric(
Congbin Guo91355e12023-02-14 13:51:20 -0800118 "podman-pull",
119 test_func=partial(_is_cmd_with_subcmd, "podman", "pull"),
Congbin Guoa8432502023-01-23 20:31:01 -0800120 ),
Congbin Guo4ccf0632023-02-12 00:01:14 -0800121 _ProcessMetric(
Congbin Guo91355e12023-02-14 13:51:20 -0800122 "podman-run",
123 test_func=partial(_is_cmd_with_subcmd, "podman", "run"),
124 ),
125 _ProcessMetric(
126 "phosphorus-fetch-crashes",
127 test_func=partial(
128 _is_cmd_with_subcmd, "phosphorus", "fetch-crashes"
129 ),
130 ),
131 _ProcessMetric(
132 "phosphorus-prejob",
133 test_func=partial(_is_cmd_with_subcmd, "phosphorus", "prejob"),
134 ),
135 _ProcessMetric(
136 "phosphorus-run-test",
137 test_func=partial(
138 _is_cmd_with_subcmd, "phosphorus", "run-test"
139 ),
140 ),
141 _ProcessMetric(
142 "phosphorus-upload-to-gs",
143 test_func=partial(
144 _is_cmd_with_subcmd, "phosphorus", "upload-to-gs"
145 ),
146 ),
147 _ProcessMetric(
148 "phosphorus-upload-to-tko",
149 test_func=partial(
150 _is_cmd_with_subcmd, "phosphorus", "upload-to-tko"
151 ),
152 ),
153 # Catch all phosphorus subcommands we missed.
154 _ProcessMetric(
155 "phosphorus-other",
156 test_func=partial(_is_process_name, "phosphorus"),
Congbin Guo4ccf0632023-02-12 00:01:14 -0800157 ),
158 _ProcessMetric("recipe", test_func=_is_recipe),
Alex Klein1699fab2022-09-08 08:46:06 -0600159 _ProcessMetric("sshd", test_func=partial(_is_process_name, "sshd")),
160 _ProcessMetric("swarming_bot", test_func=_is_swarming_bot),
161 _ProcessMetric(
Congbin Guo4ccf0632023-02-12 00:01:14 -0800162 "swarming_sub_task", test_func=_is_swarming_sub_task
163 ),
164 _ProcessMetric(
Alex Klein1699fab2022-09-08 08:46:06 -0600165 "sysmon",
166 test_func=partial(_is_python_module, "chromite.scripts.sysmon"),
167 ),
Congbin Guo522cd982022-10-06 11:47:28 -0700168 _ProcessMetric("tko_proxy", test_func=_is_tko_proxy),
Alex Klein1699fab2022-09-08 08:46:06 -0600169 ]
170 self._other_metric = _ProcessMetric("other")
Allen Li6bb74d52017-06-22 14:44:53 -0700171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 def collect(self):
Congbin Guo18b4ed72023-02-11 19:16:16 -0800173 new_cpu_times = {}
Congbin Guocf2750c2023-02-11 21:25:16 -0800174 new_io_counters = {}
Alex Klein1699fab2022-09-08 08:46:06 -0600175 for proc in psutil.process_iter():
Congbin Guo18b4ed72023-02-11 19:16:16 -0800176 new_cpu_times[proc.pid] = proc.cpu_times()
Congbin Guocf2750c2023-02-11 21:25:16 -0800177 new_io_counters[proc.pid] = proc.io_counters()
Alex Klein1699fab2022-09-08 08:46:06 -0600178 self._collect_proc(proc)
179 self._flush()
Congbin Guo18b4ed72023-02-11 19:16:16 -0800180 _ProcessMetricsCollector.old_cpu_times = new_cpu_times
Congbin Guocf2750c2023-02-11 21:25:16 -0800181 _ProcessMetricsCollector.old_io_counters = new_io_counters
Allen Li6bb74d52017-06-22 14:44:53 -0700182
Alex Klein1699fab2022-09-08 08:46:06 -0600183 def _collect_proc(self, proc):
184 for metric in self._metrics:
185 if metric.add(proc):
186 break
187 else:
188 self._other_metric.add(proc)
Allen Li6bb74d52017-06-22 14:44:53 -0700189
Alex Klein1699fab2022-09-08 08:46:06 -0600190 def _flush(self):
191 for metric in self._metrics:
192 metric.flush()
193 self._other_metric.flush()
Allen Li6bb74d52017-06-22 14:44:53 -0700194
195
196class _ProcessMetric(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600197 """Class for gathering process metrics."""
Allen Li6bb74d52017-06-22 14:44:53 -0700198
Alex Klein1699fab2022-09-08 08:46:06 -0600199 def __init__(self, process_name, test_func=lambda proc: True):
200 """Initialize instance.
Allen Li6bb74d52017-06-22 14:44:53 -0700201
Alex Klein1699fab2022-09-08 08:46:06 -0600202 process_name is used to identify the metric stream.
Allen Li6bb74d52017-06-22 14:44:53 -0700203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 test_func is a function called
205 for each process. If it returns True, the process is counted. The
206 default test is to count every process.
207 """
208 self._fields = {
209 "process_name": process_name,
210 }
211 self._test_func = test_func
212 self._count = 0
Congbin Guo16b64d52023-02-10 17:50:30 -0800213 self._thread_count = 0
Alex Klein1699fab2022-09-08 08:46:06 -0600214 self._cpu_percent = 0
Congbin Guo18b4ed72023-02-11 19:16:16 -0800215 self._cpu_times = _CPUTimes()
Congbin Guocf2750c2023-02-11 21:25:16 -0800216 self._io_counters = _IOCounters()
Allen Li6bb74d52017-06-22 14:44:53 -0700217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 def add(self, proc):
219 """Do metric collection for the given process.
Allen Li6bb74d52017-06-22 14:44:53 -0700220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 Returns True if the process was collected.
222 """
223 if not self._test_func(proc):
224 return False
225 self._count += 1
Congbin Guo16b64d52023-02-10 17:50:30 -0800226 self._thread_count += proc.num_threads()
Alex Klein1699fab2022-09-08 08:46:06 -0600227 self._cpu_percent += proc.cpu_percent()
Congbin Guo18b4ed72023-02-11 19:16:16 -0800228
229 self._cpu_times += _CPUTimes(
230 proc.cpu_times()
231 ) - _ProcessMetricsCollector.old_cpu_times.get(proc.pid)
232
Congbin Guocf2750c2023-02-11 21:25:16 -0800233 self._io_counters += _IOCounters(
234 proc.io_counters()
235 ) - _ProcessMetricsCollector.old_io_counters.get(proc.pid)
236
Alex Klein1699fab2022-09-08 08:46:06 -0600237 return True
Allen Li6bb74d52017-06-22 14:44:53 -0700238
Alex Klein1699fab2022-09-08 08:46:06 -0600239 def flush(self):
240 """Finish collection and send metrics."""
241 _count_metric.set(self._count, fields=self._fields)
242 self._count = 0
Congbin Guo16b64d52023-02-10 17:50:30 -0800243
244 _thread_count_metric.set(self._thread_count, fields=self._fields)
245 self._thread_count = 0
246
Alex Klein1699fab2022-09-08 08:46:06 -0600247 _cpu_percent_metric.set(
248 int(round(self._cpu_percent)), fields=self._fields
249 )
250 self._cpu_percent = 0
Allen Li51bb6122017-06-21 12:04:13 -0700251
Congbin Guo18b4ed72023-02-11 19:16:16 -0800252 for mode, t in self._cpu_times.asdict().items():
253 _cpu_times_metric.increment_by(
254 t, fields={**self._fields, "mode": mode}
255 )
256 self._cpu_times = _CPUTimes()
257
Congbin Guocf2750c2023-02-11 21:25:16 -0800258 _read_count_metric.increment_by(
259 self._io_counters.read_count, fields=self._fields
260 )
261 _read_bytes_metric.increment_by(
262 self._io_counters.read_bytes, fields=self._fields
263 )
264 _read_chars_metric.increment_by(
265 self._io_counters.read_chars, fields=self._fields
266 )
267 _write_count_metric.increment_by(
268 self._io_counters.write_count, fields=self._fields
269 )
270 _write_bytes_metric.increment_by(
271 self._io_counters.write_bytes, fields=self._fields
272 )
273 _write_chars_metric.increment_by(
274 self._io_counters.write_chars, fields=self._fields
275 )
276 self._io_counters = _IOCounters()
277
Congbin Guo18b4ed72023-02-11 19:16:16 -0800278
279class _CPUTimes(object):
280 """A container for CPU times metrics."""
281
282 def __init__(self, v=None):
283 self.system = v.system if v else 0
284 self.user = v.user if v else 0
285 self.iowait = v.iowait if v else 0
286 self.children_system = v.children_system if v else 0
287 self.children_user = v.children_user if v else 0
288
289 def __sub__(self, rhs):
290 if not rhs:
291 return self
292
293 r = _CPUTimes()
294 r.system = self.system - rhs.system
295 r.user = self.user - rhs.user
296 r.iowait = self.iowait - rhs.iowait
297 r.children_system = self.children_system - rhs.children_system
298 r.children_user = self.children_user - rhs.children_user
299 return r
300
301 def __iadd__(self, rhs):
302 if not rhs:
303 return self
304
305 self.system += rhs.system
306 self.user += rhs.user
307 self.iowait += rhs.iowait
308 self.children_system += rhs.children_system
309 self.children_user += rhs.children_user
310 return self
311
312 def asdict(self):
313 return {
314 "system": self.system,
315 "user": self.user,
316 "iowait": self.iowait,
317 "children_system": self.children_system,
318 "children_user": self.children_user,
319 }
320
Allen Li51bb6122017-06-21 12:04:13 -0700321
322def _is_parent_autoserv(proc):
Alex Klein1699fab2022-09-08 08:46:06 -0600323 """Return whether proc is a parent (not forked) autoserv process."""
324 return _is_autoserv(proc) and not _is_autoserv(proc.parent())
Allen Li51bb6122017-06-21 12:04:13 -0700325
326
327def _is_autoserv(proc):
Alex Klein1699fab2022-09-08 08:46:06 -0600328 """Return whether proc is an autoserv process."""
329 # This relies on the autoserv script being run directly. The script should
330 # be named autoserv exactly and start with a shebang that is /usr/bin/python,
331 # NOT /bin/env
332 return _is_process_name("autoserv", proc)
Allen Li51bb6122017-06-21 12:04:13 -0700333
334
Allen Li3992c662018-01-05 15:26:36 -0800335def _is_python_module(module, proc):
Alex Klein1699fab2022-09-08 08:46:06 -0600336 """Return whether proc is a process running a Python module."""
337 cmdline = proc.cmdline()
338 return (
339 cmdline
340 and cmdline[0].endswith("python")
341 and cmdline[1:3] == ["-m", module]
342 )
Allen Li3992c662018-01-05 15:26:36 -0800343
344
Prathmesh Prabhu0b795f02018-05-07 13:12:37 -0700345def _is_process_name(name, proc):
Alex Klein1699fab2022-09-08 08:46:06 -0600346 """Return whether process proc is named name."""
347 return proc.name() == name
Congbin Guo17542e02022-06-29 13:48:15 -0700348
349
Congbin Guo4ccf0632023-02-12 00:01:14 -0800350def _is_recipe(proc):
351 """Return whether proc is a recipe process.
352
353 An example proc is like
354 '/home/.../bin/python -u -s
355 /home/.../kitchen-checkout/recipe_engine/recipe_engine/main.py ...'.
356 """
357 cmdline = proc.cmdline()
358 return (
359 len(cmdline) >= 4
360 and cmdline[0].endswith("/python")
361 and cmdline[3].endswith("/recipe_engine/main.py")
362 )
363
364
Congbin Guo17542e02022-06-29 13:48:15 -0700365def _is_swarming_bot(proc):
Alex Klein1699fab2022-09-08 08:46:06 -0600366 """Return whether proc is a Swarming bot.
Congbin Guo17542e02022-06-29 13:48:15 -0700367
Alex Klein1699fab2022-09-08 08:46:06 -0600368 A swarming bot process is like '/usr/bin/python3.8 <bot-zip-path> start_bot'.
369 """
370 cmdline = proc.cmdline()
371 return (
372 len(cmdline) == 3
373 and cmdline[0].split("/")[-1].startswith("python")
374 and cmdline[2] == "start_bot"
375 )
Congbin Guo17542e02022-06-29 13:48:15 -0700376
377
Congbin Guo4ccf0632023-02-12 00:01:14 -0800378def _is_swarming_sub_task(proc):
379 """Return whether proc is a Swarming bot sub task.
380
381 An example Swarming sub task:
382 /usr/bin/python3.8 -u /.../swarming_bot.2.zip run_isolated ...
383 """
384 cmdline = proc.cmdline()
385 return (
386 len(cmdline) >= 4
387 and cmdline[0].split("/")[-1].startswith("python")
388 and cmdline[2].split("/")[-1].startswith("swarming_bot.")
389 )
390
391
Congbin Guo17542e02022-06-29 13:48:15 -0700392def _is_gsutil(proc):
Alex Klein1699fab2022-09-08 08:46:06 -0600393 """Return whether proc is gsutil."""
394 cmdline = proc.cmdline()
395 return (
396 len(cmdline) >= 2
397 and cmdline[0] == "python"
398 and cmdline[1].endswith("gsutil")
399 )
Congbin Guo522cd982022-10-06 11:47:28 -0700400
401
Congbin Guo4ccf0632023-02-12 00:01:14 -0800402def _is_k8s_system(proc):
403 """Return whether proc is a k8s system process."""
404 return proc.name() in ("kubelet", "kube-proxy")
405
406
Congbin Guo522cd982022-10-06 11:47:28 -0700407def _is_tko_proxy(proc):
408 """Return whether proc is a tko proxy.
409
410 A tk proxy process is like
411 '/opt/cloud_sql_proxy -dir=<...>
412 -instances=google.com:chromeos-lab:us-central1:tko
413 -credential_file=<...>'.
414 """
415 cmdline = proc.cmdline()
416 return (
417 len(cmdline) == 4
Congbin Guofcb436b2023-01-23 20:36:01 -0800418 and cmdline[0].split("/")[-1] == "cloud_sql_proxy"
419 and cmdline[2] == "-instances=google.com:chromeos-lab:us-central1:tko"
Congbin Guo522cd982022-10-06 11:47:28 -0700420 )
Congbin Guoa8432502023-01-23 20:31:01 -0800421
422
Congbin Guo91355e12023-02-14 13:51:20 -0800423def _is_cmd_with_subcmd(cmd, subcmd, proc):
424 """Return whiter proc is a subcommand of a command process.
Congbin Guoa8432502023-01-23 20:31:01 -0800425
Congbin Guo91355e12023-02-14 13:51:20 -0800426 For example: 'podman pull image:tag' or `phosphorus run-test ...`.
Congbin Guoa8432502023-01-23 20:31:01 -0800427 """
428 cmdline = proc.cmdline()
Congbin Guo91355e12023-02-14 13:51:20 -0800429 return proc.name() == cmd and len(cmdline) > 1 and cmdline[1] == subcmd
Congbin Guocf2750c2023-02-11 21:25:16 -0800430
431
432class _CPUTimes(object):
433 """A container for CPU times metrics."""
434
435 def __init__(self, v=None):
436 self.system = v.system if v else 0
437 self.user = v.user if v else 0
438 self.iowait = v.iowait if v else 0
439 self.children_system = v.children_system if v else 0
440 self.children_user = v.children_user if v else 0
441
442 def __sub__(self, rhs):
443 if not rhs:
444 return self
445
446 r = _CPUTimes()
447 r.system = self.system - rhs.system
448 r.user = self.user - rhs.user
449 r.iowait = self.iowait - rhs.iowait
450 r.children_system = self.children_system - rhs.children_system
451 r.children_user = self.children_user - rhs.children_user
452 return r
453
454 def __iadd__(self, rhs):
455 if not rhs:
456 return self
457
458 self.system += rhs.system
459 self.user += rhs.user
460 self.iowait += rhs.iowait
461 self.children_system += rhs.children_system
462 self.children_user += rhs.children_user
463 return self
464
465 def asdict(self):
466 return {
467 "system": self.system,
468 "user": self.user,
469 "iowait": self.iowait,
470 "children_system": self.children_system,
471 "children_user": self.children_user,
472 }
473
474
475class _IOCounters(object):
476 """A container for I/O counter metrics."""
477
478 def __init__(self, v=None):
479 self.read_count = v.read_count if v else 0
480 self.read_bytes = v.read_bytes if v else 0
481 self.read_chars = v.read_chars if v else 0
482 self.write_count = v.write_count if v else 0
483 self.write_bytes = v.write_bytes if v else 0
484 self.write_chars = v.write_chars if v else 0
485
486 def __sub__(self, rhs):
487 if not rhs:
488 return self
489
490 r = _IOCounters()
491 r.read_count = self.read_count - rhs.read_count
492 r.read_bytes = self.read_bytes - rhs.read_bytes
493 r.read_chars = self.read_chars - rhs.read_chars
494 r.write_count = self.write_count - rhs.write_count
495 r.write_bytes = self.write_bytes - rhs.write_bytes
496 r.write_chars = self.write_chars - rhs.write_chars
497 return r
498
499 def __iadd__(self, rhs):
500 if not rhs:
501 return self
502
503 self.read_count += rhs.read_count
504 self.read_bytes += rhs.read_bytes
505 self.write_count += rhs.write_count
506 self.write_bytes += rhs.write_bytes
507 return self