Make NOHZ cpulist checks more uniform

Signed-off-by: Marco Crivellari <marco.crivellari@suse.com>
diff --git a/check_configs_lib.py b/check_configs.py
similarity index 95%
rename from check_configs_lib.py
rename to check_configs.py
index 32b8f25..124f289 100644
--- a/check_configs_lib.py
+++ b/check_configs.py
@@ -3,7 +3,8 @@
 import os, re, sys, gzip
 from collections import defaultdict
 from dynticks_testing_lib import (parse_cpulist, get_cmdline_param, read_file,
-								  read_int, read_cpulist, safe_parse_cpulist)
+								  read_int, read_cpulist, safe_parse_cpulist,
+								  get_nohz_full_cpus)
 from log_output import Log
 
 # max number of items (IRQ vectors, workqueues, ...) listed in a single message
@@ -138,10 +139,6 @@
 
 	return _kernel_config
 
-def check_nohz_full_mask(cpulist):
-	Log.header("NOHZ_FULL / ISOLATION MASK")
-
-
 REQUIRED_CONFIGS = [
 	("CONFIG_NO_HZ_FULL", "full dynticks support"),
 	("CONFIG_HIGH_RES_TIMERS", "high resolution timers (needed by nohz_full)"),
@@ -868,13 +865,27 @@
 	else:
 		Log.print(Log.Status.OK, "ISOLCPUS_MATCH", "nohz_full is part of isolcpus. Ok!")
 
+# nohz_full= and isolcpus=nohz,<list> are equivalent ways to request full
+# dynticks: either is authoritative, nohz_full= takes precedence if both are set
+def get_requested_nohz_full():
+	requested = get_cmdline_param("nohz_full")
+	if requested is not None:
+		return expand_cpu_spec(requested)
+
+	isolcpus = get_cmdline_param("isolcpus")
+	if isolcpus is None:
+		return None
+
+	flags, cpus = split_isolcpus(isolcpus)
+	return cpus if "nohz" in flags else None
+
 def check_isolcpus_cpuset(cpulist):
 	Log.header("ISOLCPUS / CPUSET AND NOHZ_FULL")
 
 	# the applied mask can differ from the requested one: CPU0 is always kept
 	# as housekeeping and the mask is clamped to CONFIG_NR_CPUS
-	effective = read_cpulist("/sys/devices/system/cpu/nohz_full")
-	requested = expand_cpu_spec(get_cmdline_param("nohz_full"))
+	effective = get_nohz_full_cpus()
+	requested = get_requested_nohz_full()
 
 	if not effective:
 		Log.print(Log.Status.FAIL, "NOHZ_FULL_MASK",
@@ -883,7 +894,8 @@
 		Log.print(Log.Status.INFO, "NOHZ_FULL_MASK", f"Effective nohz_full: {fmt_cpus(effective)}")
 
 	if requested is None:
-		Log.print(Log.Status.WARN, "NOHZ_FULL_CMDLINE", "No nohz_full= found in /proc/cmdline")
+		Log.print(Log.Status.WARN, "NOHZ_FULL_CMDLINE",
+				  "No nohz_full= or isolcpus=nohz found in /proc/cmdline")
 	elif effective and sorted(requested) != sorted(effective):
 		dropped = sorted(set(requested) - set(effective))
 		Log.print(Log.Status.FAIL, "NOHZ_FULL_MISMATCH",
@@ -927,16 +939,15 @@
 		Log.print(Log.Status.WARN, "CGROUP_VERSION", "No cgroup hierarchy found under /sys/fs/cgroup")
 		covered = set()
 
-	# the single partitions have been reported above: the nohz_full CPUs can be
-	# spread over several of them, so the verdict is on their union
-	if not covered:
-		Log.print(Log.Status.FAIL, "CPUSET_MISSING",
-				  f"nohz_full specified {fmt_cpus(cpulist)} but no isolated partitions (isolcpus | cpusets) found!")
-	elif set(cpulist) - covered:
-		err_nohz_isolated(cpulist, sorted(covered))
-	else:
-		Log.print(Log.Status.OK, "CPUSET_MATCH",
-				  f"every nohz_full CPU ({fmt_cpus(cpulist)}) is in an isolated partition. Ok!")
+	if isolcpus is None:
+		if not covered:
+			Log.print(Log.Status.FAIL, "CPUSET_MISSING",
+					  f"nohz_full specified {fmt_cpus(cpulist)} but no isolated partitions (isolcpus | cpusets) found!")
+		elif set(cpulist) - covered:
+			err_nohz_isolated(cpulist, sorted(covered))
+		else:
+			Log.print(Log.Status.OK, "CPUSET_MATCH",
+					  f"every nohz_full CPU ({fmt_cpus(cpulist)}) is in an isolated partition. Ok!")
 
 def check_configs(cpulist):
 	if not cpulist:
@@ -948,8 +959,9 @@
 	# one and SMT is expected to be *disabled* on a tuned nohz_full machine
 	check_isolcpus_cpuset(cpulist)
 
-	Log.header("CPU TOPOLOGY / SMT")
 	smt = is_smt_enabled()
+	if smt:
+		check_siblings_thread(cpulist)
 
 	check_cpu_governor(cpulist)
 	check_irq_affinity(cpulist)
@@ -960,5 +972,4 @@
 	check_noise_knobs(cpulist)
 	check_net_rps_xps(cpulist)
 
-	if smt:
-		check_siblings_thread(cpulist)
+	print(" ");
diff --git a/dynticks-testing.py b/dynticks-testing.py
index d9a10e7..884ec93 100755
--- a/dynticks-testing.py
+++ b/dynticks-testing.py
@@ -1,11 +1,10 @@
 #!/usr/bin/python3
 
-from dynticks_testing_lib import (parse_cpulist, read_file, find_isolated_partition,
-								  cgroup_attach, to_str_range)
+from dynticks_testing_lib import (read_file, find_isolated_partition,
+								  cgroup_attach, to_str_range, get_nohz_full_cpus)
 import os, sys, subprocess, time
 
 TRACING_PATH = "/sys/kernel/tracing"
-NOHZ_FULL_PATH = "/sys/devices/system/cpu/nohz_full"
 
 # how long the user loops are left spinning while the trace is collected
 TRACE_DURATION_S = 10
@@ -43,19 +42,13 @@
 		except OSError:
 			pass
 
-# Parse nohz_full=
-cpulist = read_file(NOHZ_FULL_PATH)
-if cpulist is None:
-	print("Can't find %s" % NOHZ_FULL_PATH)
-	sys.exit(-1)
-
-if cpulist in ("(null)", ""):
+# nohz_full CPUs, however they were requested (nohz_full= or isolcpus=nohz)
+nohz_full = get_nohz_full_cpus()
+if not nohz_full:
 	print("nohz_full= not set ?")
 	sys.exit(-1)
 
-print("nohz_full=%s" % cpulist)
-
-nohz_full = parse_cpulist(cpulist)
+print("nohz_full=%s" % to_str_range(nohz_full))
 
 # Return the set of CPUs the calling thread of the current process is restricted to
 allowed_cpus = os.sched_getaffinity(0)
diff --git a/dynticks_testing_lib.py b/dynticks_testing_lib.py
index 52d0b86..53c1131 100755
--- a/dynticks_testing_lib.py
+++ b/dynticks_testing_lib.py
@@ -4,6 +4,7 @@
 from itertools import groupby
 
 CGROUP_ROOT = "/sys/fs/cgroup"
+NOHZ_FULL_PATH = "/sys/devices/system/cpu/nohz_full"
 
 def parse_cpulist(s):
 	cpulist = []
@@ -65,6 +66,12 @@
 
 	return safe_parse_cpulist(value) or []
 
+# The single source of truth for "which CPUs are running full dynticks": the
+# kernel populates this file the same way whether it was requested via
+# nohz_full= or via the equivalent isolcpus=nohz,<list> flag.
+def get_nohz_full_cpus():
+	return read_cpulist(NOHZ_FULL_PATH)
+
 # Return the cgroup v2 isolated partition owning 'cpu', or None
 def find_isolated_partition(cpu):
 	for root, dirs, files in os.walk(CGROUP_ROOT):
diff --git a/noise_parse.py b/noise_parse.py
index 9e2ce25..9a6dfe4 100755
--- a/noise_parse.py
+++ b/noise_parse.py
@@ -3,8 +3,8 @@
 import re, sys, statistics , os
 from optparse import OptionParser
 from collections import defaultdict
-from dynticks_testing_lib import parse_cpulist, get_cmdline_param
-from check_configs_lib import check_configs
+from dynticks_testing_lib import parse_cpulist, get_nohz_full_cpus
+from check_configs import check_configs
 
 class Noise:
 	def __init__(self, ts):
@@ -774,12 +774,11 @@
 
 (options, trace_file) = parse_arguments()
 
-# if --cpulist is not set, read nohz_full
+# if --cpulist is not set, read the nohz_full CPUs from sysfs: this covers
+# both nohz_full= and the equivalent isolcpus=nohz,<list> boot parameter
 if cpulist is None:
-	print("INFO: --cpulist param. not set: reading nohz_full from /proc/cmdline")
-	nohz_list = get_cmdline_param("nohz_full")
-	if nohz_list:
-		cpulist = parse_cpulist(nohz_list)
+	print("INFO: --cpulist param. not set: reading nohz_full from sysfs")
+	cpulist = get_nohz_full_cpus()
 
 
 # Check system configurations...