[RFC PATCH] tools/accounting: add lock_contention.py tool

From: 何惟禹(百奎)
Date: Thu Aug 05 2021 - 08:54:13 EST


Add a drgn-based tool to count some spinlocks(eg. runqueues.lock, task->pi_lock) contention

Currently supports locks:
1. rq_lock
2. task->pi_lock
3. zone->lock
4. pgdat->lru_lock
5. cfs_b->lock

Output example:
$ ./tools/accounting/lock_contention.py lock rq_lock
17:52:23 rq_lock contention: 3/52 [A]
17:52:24 rq_lock contention: 1/52 [A]
17:52:25 rq_lock contention: 0/52 [A]
17:52:26 rq_lock contention: 1/52 [A]
17:52:27 rq_lock contention: 1/52 [A]
17:52:28 rq_lock contention: 2/52 [A]
17:52:29 rq_lock contention: 1/52 [A]
17:52:30 rq_lock contention: 1/52 [A]
17:52:32 rq_lock contention: 0/52 [A]
17:52:33 rq_lock contention: 1/52 [A]
17:52:34 rq_lock contention: 2/52 [A]
17:52:35 rq_lock contention: 2/52 [A]
17:52:36 rq_lock contention: 1/52 [A]
17:52:37 rq_lock contention: 0/52 [A]
17:52:38 rq_lock contention: 0/52 [A]
17:52:39 rq_lock contention: 0/52 [A]
17:52:40 rq_lock contention: 0/52 [A]
17:52:41 rq_lock contention: 0/52 [A]
17:52:42 rq_lock contention: 0/52 [A]
17:52:43 rq_lock contention: 0/52 [A]
17:52:44 rq_lock contention: 0/52 [A]
17:52:45 rq_lock contention: 0/52 [A]
17:52:46 rq_lock contention: 0/52 [A]
17:52:47 rq_lock contention: 0/52 [A]
17:52:48 rq_lock contention: 0/52 [A]
17:52:49 rq_lock contention: 0/52 [A]
17:52:50 rq_lock contention: 0/52 [A]
17:52:51 rq_lock contention: 1/52 [A]
17:52:52 rq_lock contention: 0/52 [A]
17:52:53 rq_lock contention: 1/52 [A]
17:52:54 rq_lock contention: 0/52 [A]
17:52:55 rq_lock contention: 1/52 [A]
17:52:56 rq_lock contention: 2/52 [A]

Signed-off-by: Weiyu <heweiyu.hwy@xxxxxxxxxxxxxxx>
---
tools/accounting/lock_contention.py | 214 ++++++++++++++++++++++++++++
1 file changed, 214 insertions(+)
create mode 100755 tools/accounting/lock_contention.py

diff --git a/tools/accounting/lock_contention.py b/tools/accounting/lock_contention.py
new file mode 100755
index 000000000000..984cb415cee3
--- /dev/null
+++ b/tools/accounting/lock_contention.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env drgn
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2021 Weiyu He <heweiyu.hwy@xxxxxxxxxxxxxxx>
+# Copyright (C) 2021 Alibaba Inc
+
+from collections import namedtuple
+import argparse
+import sys
+import time
+import multiprocessing
+
+from drgn.helpers.linux import css_for_each_descendant_pre, per_cpu, for_each_task
+from drgn import container_of, sizeof
+
+
+DESC = """
+This is a drgn script to provide lock contention statistics for spinlock.
+"""
+
+def err(s):
+ print('lock_contention.py: error: %s' % s, file=sys.stderr, flush=True)
+ sys.exit(1)
+
+def for_each_set_bit(bitmap, size):
+ """
+ Iterate over all set (one) bits in a bitmap.
+ :param bitmap: ``unsigned long *``
+ :param size: Size of *bitmap* in bits.
+ """
+ size = int(size)
+ word_bits = 8 * sizeof(bitmap.type_.type)
+ for i in range((size + word_bits - 1) // word_bits):
+ word = bitmap[i].value_()
+ for j in range(min(word_bits, size - word_bits * i)):
+ if word & (1 << j):
+ yield (word_bits * i) + j
+
+def for_each_online_node(prog):
+ """Iterate over all online NUMA nodes."""
+ mask = prog["node_states"][prog["N_ONLINE"]]
+ try:
+ nr_node_ids = mask.prog_["nr_node_ids"].value_()
+ except KeyError:
+ nr_node_ids = 1
+ return for_each_set_bit(mask.bits, nr_node_ids)
+
+def is_vaild_bit(bits):
+ return any(int(x) for x in bits)
+
+def split_counter(counter_bits):
+ if sys.byteorder == "little":
+ locked = counter_bits[24:]
+ pending = counter_bits[16:24]
+ tail = counter_bits[:16]
+ else:
+ locked = counter_bits[:8]
+ pending = counter_bits[8:16]
+ tail = counter_bits[16:]
+ return (locked, pending, tail)
+
+def computer_spinlock_waiter(val):
+ """
+ @Return: (is_accurate[Bool], n_locked + n_waiter [int])
+
+ counter: 32 bits {31, 30, ...., 2, 1, 0}
+ locked: 8bits [0, 7]
+ pending: 8 bits [8, 15]
+ tail: 16bits [16, 32]
+ """
+ counter = val.counter.value_()
+ counter_bits = f'{counter:032b}'
+ locked, pending, tail = split_counter(counter_bits)
+
+ Result = namedtuple('waiter', ['ac', 'num'])
+
+ res = Result(True, 0)
+
+ if is_vaild_bit(tail):
+ res = Result(False, 3) # scenario.1 more than two waiter
+ elif is_vaild_bit(pending):
+ if not is_vaild_bit(locked):
+ res = Result(True, 1.5) # special: no locked, but one pending is regarded as 1.5
+ else:
+ res = Result(True, 2) # scenario.2 only one waiter
+ elif is_vaild_bit(locked):
+ res = Result(True, 1) # scenario.1 only one locked
+
+ return res
+
+def get_raw_spinlock_waiter(raw_spinlock):
+ return computer_spinlock_waiter(raw_spinlock.raw_lock.val)
+
+def get_spinlock_waiter(spinlock):
+ return get_raw_spinlock_waiter(spinlock.rlock)
+
+def for_each_task_group(prog):
+ """
+ bug:
+ An error may be reported if the cgroup is deleted during the traversal
+ """
+ cpu_cgrp_id = prog["cpu_cgrp_id"]
+ root_tg = prog["root_task_group"].address_of_()
+ for pos in css_for_each_descendant_pre(root_tg.css.address_of_()):
+ cgroup = pos.cgroup
+ task_group = container_of(cgroup.subsys[cpu_cgrp_id], "struct task_group", "css")
+ yield task_group
+
+def lock_contention_show(name, ac, sum):
+ now = time.localtime(time.time())
+ now_str = "{}:{}:{}".format(now.tm_hour, now.tm_min, now.tm_sec)
+
+ flag = "A"
+ if not ac:
+ flag = "NA"
+ counter_msg = "{}/{} [{}]".format(sum, NR_CPUS, flag)
+
+ print("{:10}\t{} contention: {:^30}".format(now_str, name, counter_msg))
+
+def for_each_rq_lock():
+ for cpu in range(NR_CPUS):
+ yield get_raw_spinlock_waiter(per_cpu(prog["runqueues"], cpu).lock)
+
+def for_each_cb_lock():
+ for tg in for_each_task_group(prog):
+ yield get_raw_spinlock_waiter(tg.cfs_bandwidth.lock)
+
+def for_each_pi_lock():
+ """
+ Performance issues:
+ Problem with too many tasks (too slow)
+ """
+ for task in for_each_task(prog):
+ yield get_raw_spinlock_waiter(task.pi_lock)
+
+def for_each_zone_lock():
+ node_data = prog["node_data"]
+ for i in for_each_online_node(prog):
+ zone_num = prog["__MAX_NR_ZONES"].value_()
+ for j in range(zone_num):
+ yield get_spinlock_waiter(node_data[i].node_zones[j].lock)
+
+def for_each_lru_lock():
+ node_data = prog["node_data"]
+ for i in for_each_online_node(prog):
+ yield get_spinlock_waiter(node_data[i].lru_lock)
+
+def for_each_lock(lock_name):
+ return LOCKS[lock_name]()
+
+def lock_list_show(args):
+ for i, lock_name in enumerate(LOCKS.keys()):
+ print("{}. {:20}".format(i+1, lock_name))
+
+def lock_contention(args):
+ lock_name = args.lock
+ interval = int(args.interval)
+ count = int(args.count)
+
+ if not lock_name in LOCKS:
+ err("this \"{}\" lock is not support".format(lock_name))
+
+ exiting = 0
+ while(1):
+ try:
+ time.sleep(float(interval))
+
+ is_accurate = True
+ contention_sum = 0
+ for lock_info in for_each_lock(lock_name):
+ is_accurate &= lock_info.ac
+ contention_sum += lock_info.num
+ lock_contention_show(lock_name, is_accurate, contention_sum)
+
+ except KeyboardInterrupt:
+ exiting = 1
+
+ count -= 1
+ if exiting or count == 0:
+ exit()
+
+LOCKS = {
+ "rq_lock" : for_each_rq_lock,
+ "pi_lock" : for_each_pi_lock,
+ "zone_lock" : for_each_zone_lock,
+ "lru_lock" : for_each_lru_lock,
+ "cb_lock" : for_each_cb_lock,
+}
+
+NR_CPUS = multiprocessing.cpu_count()
+
+def main():
+ parser = argparse.ArgumentParser(description=DESC,
+ formatter_class=
+ argparse.RawTextHelpFormatter)
+
+ subparsers = parser.add_subparsers()
+
+ lock_parser = subparsers.add_parser("lock", help="statistics lock contention")
+ lock_parser.add_argument('lock', metavar='lockname',
+ help='Target lock name (e.g. rq_lock)')
+ lock_parser.add_argument("interval", nargs="?", default=1,
+ help="Output interval, in seconds")
+ lock_parser.add_argument("count", nargs="?", default=99999999,
+ help="Number of outputs")
+ lock_parser.set_defaults(func=lock_contention)
+
+ list_parser = subparsers.add_parser("list", help="list support locks")
+ list_parser.set_defaults(func=lock_list_show)
+
+
+ args = parser.parse_args()
+ args.func(args)
+
+main()
\ No newline at end of file
--
2.27.0