perf sched stats: Reject mismatched or incomplete snapshots

The before and after records are paired by list position. If a CPU or
domain disappears, counters can be subtracted from a different record
or left as absolute values. An extra record can also advance the cursor
past the list.

Identify the second snapshot by its timestamp or CPU ordering, and
require matching CPU/domain IDs and versions before subtracting. Check
that every record has a counterpart before printing, and propagate
errors from either input of diff.

Add a shell test with synthetic snapshots, including equal timestamps,
CPU filtering, and missing or reordered CPU/domain records.

Fixes: 5a357ae6ad ("perf sched stats: Add support for report subcommand")
Reviewed-by: Swapnil Sapkal <swapnil.sapkal@amd.com>
Assisted-by: LLM
Signed-off-by: Tianyi Chen <hi@tychen.cc>
Tested-by: Swapnil Sapkal <swapnil.sapkal@amd.com>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
This commit is contained in:
Tianyi Chen 2026-09-17 00:11:25 +08:00 committed by Arnaldo Carvalho de Melo
parent ccccf88a19
commit 9910ee15c9
3 changed files with 286 additions and 70 deletions

View File

@ -95,8 +95,10 @@ There are several variants of 'perf sched':
events, ``try_to_wakeup()`` call among others. This is useful in understanding the
scheduler behavior for the workload.
Note: The tool will not give correct results if there is topological reordering or
online/offline of cpus in between capturing snapshots of `/proc/schedstat`.
Note: Reports reject incomplete snapshots and mismatched CPU or domain IDs.
Topology changes that retain the same IDs, including CPUs going offline
and returning online between snapshots, cannot be detected and may still
produce incorrect results.
Example usage:
perf sched stats record -- sleep 1

View File

@ -4281,18 +4281,18 @@ struct schedstat_cpu {
};
static struct list_head cpu_head = LIST_HEAD_INIT(cpu_head);
static struct schedstat_cpu *cpu_second_pass;
static struct schedstat_domain *domain_second_pass;
static struct list_head *cpu_second_pass;
static struct list_head *domain_second_pass;
static u64 schedstat_timestamp;
static bool after_workload_flag;
static bool verbose_field;
static void free_schedstat(struct list_head *head);
static void store_schedstat_cpu_diff(struct schedstat_cpu *after_workload)
static void store_schedstat_cpu_diff(struct perf_record_schedstat_cpu *before,
struct perf_record_schedstat_cpu *after)
{
struct perf_record_schedstat_cpu *before = cpu_second_pass->cpu_data;
struct perf_record_schedstat_cpu *after = after_workload->cpu_data;
__u16 version = after_workload->cpu_data->version;
__u16 version = after->version;
#define CPU_FIELD(_type, _name, _desc, _format, _is_pct, _pct_of, _ver) \
(before->_ver._name = after->_ver._name - before->_ver._name)
@ -4308,11 +4308,10 @@ static void store_schedstat_cpu_diff(struct schedstat_cpu *after_workload)
#undef CPU_FIELD
}
static void store_schedstat_domain_diff(struct schedstat_domain *after_workload)
static void store_schedstat_domain_diff(struct perf_record_schedstat_domain *before,
struct perf_record_schedstat_domain *after)
{
struct perf_record_schedstat_domain *before = domain_second_pass->domain_data;
struct perf_record_schedstat_domain *after = after_workload->domain_data;
__u16 version = after_workload->domain_data->version;
__u16 version = after->version;
#define DOMAIN_FIELD(_type, _name, _desc, _format, _is_jiffies, _ver) \
(before->_ver._name = after->_ver._name - before->_ver._name)
@ -4814,12 +4813,35 @@ static int show_schedstat_data(struct list_head *head1, struct cpu_domain_map **
* other after completion of the workload. The above linked list stores the diff of the cpu and
* domain statistics.
*/
static int schedstat_snapshot_error(void)
{
pr_err("Incompatible or incomplete schedstat snapshots\n");
return -EINVAL;
}
static bool schedstat_domains_complete(void)
{
struct schedstat_cpu *cpu;
if (!domain_second_pass)
return true;
cpu = list_entry(cpu_second_pass, struct schedstat_cpu, cpu_list);
return domain_second_pass == &cpu->domain_head;
}
static int schedstat_snapshots_complete(void)
{
if (!after_workload_flag || !cpu_second_pass ||
cpu_second_pass->next != &cpu_head || !schedstat_domains_complete())
return schedstat_snapshot_error();
return 0;
}
static int perf_sched__process_schedstat(const struct perf_tool *tool __maybe_unused,
struct perf_session *session __maybe_unused,
union perf_event *event)
{
struct perf_cpu this_cpu;
static __u32 initial_cpu;
switch (event->header.type) {
case PERF_RECORD_SCHEDSTAT_CPU:
@ -4836,63 +4858,91 @@ static int perf_sched__process_schedstat(const struct perf_tool *tool __maybe_un
return 0;
if (event->header.type == PERF_RECORD_SCHEDSTAT_CPU) {
struct schedstat_cpu *temp = zalloc(sizeof(*temp));
struct perf_record_schedstat_cpu *data = &event->schedstat_cpu;
struct schedstat_cpu *cpu;
if (!temp)
return -ENOMEM;
temp->cpu_data = zalloc(sizeof(*temp->cpu_data));
if (!temp->cpu_data)
return -ENOMEM;
memcpy(temp->cpu_data, &event->schedstat_cpu, sizeof(*temp->cpu_data));
if (!list_empty(&cpu_head) && temp->cpu_data->cpu == initial_cpu)
after_workload_flag = true;
if (!after_workload_flag) {
if (list_empty(&cpu_head))
initial_cpu = temp->cpu_data->cpu;
list_add_tail(&temp->cpu_list, &cpu_head);
INIT_LIST_HEAD(&temp->domain_head);
} else {
if (temp->cpu_data->cpu == initial_cpu) {
cpu_second_pass = list_first_entry(&cpu_head, struct schedstat_cpu,
cpu_list);
cpu_second_pass->cpu_data->timestamp =
temp->cpu_data->timestamp - cpu_second_pass->cpu_data->timestamp;
} else {
cpu_second_pass = list_next_entry(cpu_second_pass, cpu_list);
if (list_empty(&cpu_head)) {
after_workload_flag = false;
cpu_second_pass = &cpu_head;
domain_second_pass = NULL;
schedstat_timestamp = data->timestamp;
} else if (!after_workload_flag) {
cpu = list_last_entry(&cpu_head, struct schedstat_cpu, cpu_list);
/* Snapshots share a timestamp and list CPUs in increasing order. */
if (data->timestamp != schedstat_timestamp ||
data->cpu <= cpu->cpu_data->cpu) {
after_workload_flag = true;
schedstat_timestamp = data->timestamp;
}
domain_second_pass = list_first_entry(&cpu_second_pass->domain_head,
struct schedstat_domain, domain_list);
store_schedstat_cpu_diff(temp);
free(temp->cpu_data);
free(temp);
}
} else if (event->header.type == PERF_RECORD_SCHEDSTAT_DOMAIN) {
struct schedstat_cpu *cpu_tail;
struct schedstat_domain *temp = zalloc(sizeof(*temp));
if (!temp)
return -ENOMEM;
temp->domain_data = zalloc(sizeof(*temp->domain_data));
if (!temp->domain_data)
return -ENOMEM;
memcpy(temp->domain_data, &event->schedstat_domain, sizeof(*temp->domain_data));
if (!after_workload_flag) {
cpu_tail = list_last_entry(&cpu_head, struct schedstat_cpu, cpu_list);
list_add_tail(&temp->domain_list, &cpu_tail->domain_head);
} else {
store_schedstat_domain_diff(temp);
domain_second_pass = list_next_entry(domain_second_pass, domain_list);
free(temp->domain_data);
free(temp);
if (after_workload_flag) {
if (data->timestamp != schedstat_timestamp || !schedstat_domains_complete())
return schedstat_snapshot_error();
cpu_second_pass = cpu_second_pass->next;
if (cpu_second_pass == &cpu_head)
return schedstat_snapshot_error();
cpu = list_entry(cpu_second_pass, struct schedstat_cpu, cpu_list);
if (data->cpu != cpu->cpu_data->cpu ||
data->version != cpu->cpu_data->version ||
data->timestamp < cpu->cpu_data->timestamp)
return schedstat_snapshot_error();
cpu->cpu_data->timestamp = data->timestamp - cpu->cpu_data->timestamp;
store_schedstat_cpu_diff(cpu->cpu_data, data);
domain_second_pass = cpu->domain_head.next;
return 0;
}
cpu = zalloc(sizeof(*cpu));
if (!cpu)
return -ENOMEM;
cpu->cpu_data = memdup(data, sizeof(*data));
if (!cpu->cpu_data) {
free(cpu);
return -ENOMEM;
}
INIT_LIST_HEAD(&cpu->domain_head);
list_add_tail(&cpu->cpu_list, &cpu_head);
} else {
struct perf_record_schedstat_domain *data = &event->schedstat_domain;
struct schedstat_domain *domain;
struct schedstat_cpu *cpu;
if (list_empty(&cpu_head) || data->timestamp != schedstat_timestamp)
return schedstat_snapshot_error();
if (after_workload_flag) {
cpu = list_entry(cpu_second_pass, struct schedstat_cpu, cpu_list);
if (domain_second_pass == &cpu->domain_head)
return schedstat_snapshot_error();
domain = list_entry(domain_second_pass, struct schedstat_domain,
domain_list);
if (data->cpu != domain->domain_data->cpu ||
data->domain != domain->domain_data->domain ||
data->version != domain->domain_data->version)
return schedstat_snapshot_error();
store_schedstat_domain_diff(domain->domain_data, data);
domain_second_pass = domain_second_pass->next;
return 0;
}
cpu = list_last_entry(&cpu_head, struct schedstat_cpu, cpu_list);
if (data->cpu != cpu->cpu_data->cpu || data->version != cpu->cpu_data->version)
return schedstat_snapshot_error();
if (!list_empty(&cpu->domain_head)) {
domain = list_last_entry(&cpu->domain_head, struct schedstat_domain,
domain_list);
if (data->domain <= domain->domain_data->domain)
return schedstat_snapshot_error();
}
domain = zalloc(sizeof(*domain));
if (!domain)
return -ENOMEM;
domain->domain_data = memdup(data, sizeof(*data));
if (!domain->domain_data) {
free(domain);
return -ENOMEM;
}
list_add_tail(&domain->domain_list, &cpu->domain_head);
}
return 0;
@ -4947,6 +4997,8 @@ static int perf_sched__schedstat_report(struct perf_sched *sched)
user_requested_cpus = evlist__core(session->evlist)->user_requested_cpus;
err = perf_session__process_events(session);
if (!err)
err = schedstat_snapshots_complete();
if (!err) {
setup_pager();
@ -4976,7 +5028,7 @@ static int perf_sched__schedstat_diff(struct perf_sched *sched,
struct list_head cpu_head_ses0, cpu_head_ses1;
struct perf_session *session[2];
struct perf_data data[2] = {0};
int ret = 0, err = 0;
int ret = 0;
static const char *defaults[] = {
"perf.data.old",
"perf.data",
@ -5009,8 +5061,10 @@ static int perf_sched__schedstat_diff(struct perf_sched *sched,
goto out_delete_ses0;
}
err = perf_session__process_events(session[0]);
if (err) {
ret = perf_session__process_events(session[0]);
if (!ret)
ret = schedstat_snapshots_complete();
if (ret) {
free_schedstat(&cpu_head);
goto out_delete_ses0;
}
@ -5028,8 +5082,10 @@ static int perf_sched__schedstat_diff(struct perf_sched *sched,
goto out_delete_ses1;
}
err = perf_session__process_events(session[1]);
if (err) {
ret = perf_session__process_events(session[1]);
if (!ret)
ret = schedstat_snapshots_complete();
if (ret) {
free_schedstat(&cpu_head);
goto out_delete_ses1;
}
@ -5152,6 +5208,9 @@ static int perf_sched__schedstat_live(struct perf_sched *sched,
user_requested_cpus);
if (err)
goto out;
err = schedstat_snapshots_complete();
if (err)
goto out;
setup_pager();

View File

@ -0,0 +1,155 @@
#!/bin/sh
# Validate CPU and domain pairing in perf sched stats snapshots
# SPDX-License-Identifier: GPL-2.0
set -e
# shellcheck source=lib/setup_python.sh
. "$(dirname "$0")/lib/setup_python.sh"
if ! perf version --build-options | grep -q 'libtraceevent:.*on'; then
echo "[Skip] perf sched requires libtraceevent"
exit 2
fi
$PYTHON - <<'PY'
import os
import re
import struct
import subprocess
import sys
import tempfile
# Native-endian perf.data with only NRCPUS and CPU_DOMAIN_INFO features.
endian = '<' if sys.byteorder == 'little' else '>'
def pack(fmt, *values):
return struct.pack(endian + fmt, *values)
def string(value):
data = value.encode() + b'\0'
return pack('I', len(data)) + data
def cpu(cpu_id, timestamp, value, version):
return pack('IHHQIHH6I3Q', 85, 0, 72, timestamp, cpu_id, version, 0,
*([value] * 9))
def domain(cpu_id, domain_id, timestamp, value, version):
# All supported versions use the largest union member's record size.
return pack('IHHQIHH45I4x', 86, 0, 208, timestamp, cpu_id, version,
domain_id, *([value] * 45))
def snapshot(timestamp, value, version=17, cpus=(0, 1, 2), domains=(0, 1)):
records = []
for cpu_id in cpus:
records.append(cpu(cpu_id, timestamp, value + cpu_id * 100, version))
for domain_id in domains:
records.append(domain(cpu_id, domain_id, timestamp,
value + cpu_id * 100, version))
return records
def write_file(path, records, version=17):
metadata = pack('II', version, 2)
for cpu_id in range(3):
metadata += pack('II', cpu_id, 2)
for domain_id in range(2):
metadata += pack('I', domain_id)
if version >= 17:
metadata += string('SMT' if domain_id == 0 else 'MC')
metadata += string('7') + string('0-2')
features = [pack('II', 3, 3), metadata]
data = b''.join(records)
offset = 104 + len(data) + 16 * len(features)
sections = b''
for feature in features:
sections += pack('QQ', offset, len(feature))
offset += len(feature)
header = pack('13Q', 0x32454c4946524550, 104, 144, 104, 0,
104, len(data), 0, 0, (1 << 7) | (1 << 32), 0, 0, 0)
with open(path, 'wb') as output:
output.write(header + data + sections + b''.join(features))
def run(args, valid, domains=True):
result = subprocess.run(['perf', 'sched', 'stats'] + args,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, timeout=10)
if valid:
assert result.returncode == 0, result.stderr
assert re.search(r'^yld_count\s+:\s+10\b', result.stdout, re.M), result.stdout
if domains:
assert re.search(r'^busy_lb_count\s+:\s+10\b', result.stdout, re.M), result.stdout
else:
assert 'busy_lb_count' not in result.stdout, result.stdout
else:
assert result.returncode > 0, (args, result.returncode, result.stdout)
assert 'Incompatible or incomplete schedstat snapshots' in result.stderr
assert not result.stdout, result.stdout
assert 'Sanitizer' not in result.stderr, result.stderr
with tempfile.TemporaryDirectory(prefix='perf-schedstat-') as directory:
good = os.path.join(directory, 'good.data')
test = os.path.join(directory, 'test.data')
before = snapshot(100, 1000)
after = snapshot(200, 1010)
write_file(good, before + after)
for version in (15, 16, 17):
for timestamp in (100, 200):
write_file(test, snapshot(100, 1000, version) +
snapshot(timestamp, 1010, version), version)
run(['report', '-C', '0,1,2', '-i', test], True)
run(['diff', test, test], True)
print('Matching snapshots, including equal timestamps: [Success]')
write_file(test, snapshot(100, 1000, domains=()) +
snapshot(200, 1010, domains=()))
run(['report', '-C', '0,1,2', '-i', test], True, domains=False)
run(['diff', test, test], True, domains=False)
print('CPUs without domains: [Success]')
write_file(test, before + snapshot(200, 1010, cpus=(0, 1)))
run(['report', '-C', '0,1', '-i', test], True)
run(['report', '-C', '1', '-i', good], True)
write_file(test, snapshot(100, 1000, cpus=(0, 1)) +
snapshot(200, 1010, cpus=(0, 1)))
run(['diff', good, test], True)
print('CPU filtering and different CPU sets across files: [Success]')
write_file(test, snapshot(100, 0xfffffffa, cpus=(0,)) +
snapshot(200, 4, cpus=(0,)))
run(['report', '-C', '0', '-i', test], True)
print('Wrapping 32-bit counters: [Success]')
cases = {
'missing first CPU': before + snapshot(200, 1010, cpus=(1, 2)),
'missing middle CPU': before + snapshot(200, 1010, cpus=(0, 2)),
'missing last CPU': before + snapshot(200, 1010, cpus=(0, 1)),
'added CPU': snapshot(100, 1000, cpus=(0, 1)) + after,
'reordered CPUs': before + snapshot(200, 1010, cpus=(0, 2, 1)),
'equal timestamp, missing first CPU': before + snapshot(100, 1010, cpus=(1, 2)),
'missing first domain': before + snapshot(200, 1010, domains=(1,)),
'missing last domain': before + after[:-1],
'added domain': snapshot(100, 1000, domains=(0,)) + after,
'reordered domains': before + snapshot(200, 1010, domains=(1, 0)),
'domain without CPU': before[1:] + after,
'wrong domain CPU': before + [after[0], domain(1, 0, 200, 1010, 17)] + after[2:],
'changed version': before + snapshot(200, 1010, version=16),
'backwards timestamp': before + snapshot(50, 1010),
'third snapshot': before + after + snapshot(300, 1020),
'missing second snapshot': before,
}
for name, records in cases.items():
write_file(test, records)
run(['report', '-C', '0,1,2', '-i', test], False)
run(['diff', test, good], False)
run(['diff', good, test], False)
print(name + ': [Success]')
PY