use super::reader::{GuestMem, WalkContext};
use crate::monitor::btf_offsets::RunnableScanOffsets;
const SCX_TASK_CURSOR: u32 = 1 << 31;
const SCX_TASK_QUEUED: u32 = 1 << 0;
const SCX_TASK_RESET_RUNNABLE_AT: u32 = 1 << 2;
const MAX_NODES: u32 = 65_536;
#[allow(clippy::too_many_arguments)]
pub fn max_runnable_age(
mem: &GuestMem,
scx_tasks_kva: u64,
rq_pas: &[u64],
offsets: &RunnableScanOffsets,
jiffies: u64,
walk: WalkContext,
watchdog_timestamp_pa: Option<u64>,
start_kernel_map: u64,
phys_base: u64,
) -> u64 {
let global = max_runnable_age_global(
mem,
scx_tasks_kva,
offsets,
jiffies,
walk,
start_kernel_map,
phys_base,
);
let mut per_rq_max: u64 = 0;
for &rq_pa in rq_pas {
let age = max_runnable_age_per_rq(mem, rq_pa, offsets, jiffies, walk);
if age > per_rq_max {
per_rq_max = age;
}
}
let watchdog_age = match watchdog_timestamp_pa {
Some(pa) => {
let timestamp = mem.read_u64(pa, 0);
const SANITY_CAP_JIFFIES: u64 = 86_400_000;
if timestamp == 0 {
0
} else {
let raw = jiffies.saturating_sub(timestamp);
if raw > SANITY_CAP_JIFFIES { 0 } else { raw }
}
}
None => 0,
};
let mut max_age = global;
if per_rq_max > max_age {
max_age = per_rq_max;
}
if watchdog_age > max_age {
max_age = watchdog_age;
}
max_age
}
pub fn max_runnable_age_global(
mem: &GuestMem,
scx_tasks_kva: u64,
offsets: &RunnableScanOffsets,
jiffies: u64,
walk: WalkContext,
start_kernel_map: u64,
phys_base: u64,
) -> u64 {
if scx_tasks_kva == 0 {
return 0;
}
let head_kva = scx_tasks_kva;
let head_pa =
super::symbols::text_kva_to_pa_with_base(scx_tasks_kva, start_kernel_map, phys_base);
let mut node_kva = mem.read_u64(head_pa, 0);
if node_kva == 0 {
return 0;
}
let tasks_node_off_in_task = offsets.task_struct_scx + offsets.sched_ext_entity_tasks_node;
let runnable_at_off_in_task = offsets.task_struct_scx + offsets.sched_ext_entity_runnable_at;
let flags_off_in_see = offsets.sched_ext_entity_flags;
let tasks_node_off_in_see = offsets.sched_ext_entity_tasks_node;
let mut max_age: u64 = 0;
let mut visited: u32 = 0;
while node_kva != head_kva {
if visited >= MAX_NODES {
return max_age;
}
visited += 1;
let see_kva = node_kva.wrapping_sub(tasks_node_off_in_see as u64);
let (cursor, queued, reset_runnable_at) = match super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
see_kva,
walk.l5,
walk.tcr_el1,
) {
Some(see_pa) => {
let flags = mem.read_u32(see_pa, flags_off_in_see);
(
flags & SCX_TASK_CURSOR != 0,
flags & SCX_TASK_QUEUED != 0,
flags & SCX_TASK_RESET_RUNNABLE_AT != 0,
)
}
None => (false, false, false),
};
if !cursor && queued && !reset_runnable_at {
let task_kva = node_kva.wrapping_sub(tasks_node_off_in_task as u64);
let runnable_at_kva = task_kva.wrapping_add(runnable_at_off_in_task as u64);
if let Some(runnable_at_pa) = super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
runnable_at_kva,
walk.l5,
walk.tcr_el1,
) {
let runnable_at = mem.read_u64(runnable_at_pa, 0);
if runnable_at != 0 {
let age = jiffies.saturating_sub(runnable_at);
if age > max_age {
max_age = age;
}
}
} else {
tracing::debug!(
task_kva = format_args!("{task_kva:#x}"),
"runnable_scan: task page untranslatable, skipping",
);
}
}
let node_pa = match super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
node_kva,
walk.l5,
walk.tcr_el1,
) {
Some(pa) => pa,
None => return max_age,
};
let next_kva = mem.read_u64(node_pa, 0);
if next_kva == 0 {
return max_age;
}
node_kva = next_kva;
}
max_age
}
pub fn max_runnable_age_per_rq(
mem: &GuestMem,
rq_pa: u64,
offsets: &RunnableScanOffsets,
jiffies: u64,
walk: WalkContext,
) -> u64 {
if rq_pa == 0 {
return 0;
}
let head_offset = offsets.rq_scx + offsets.scx_rq_runnable_list;
let head_pa = rq_pa.wrapping_add(head_offset as u64);
let mut node_kva = mem.read_u64(head_pa, 0);
if node_kva == 0 {
return 0;
}
let head_kva = head_pa.wrapping_add(walk.page_offset);
let runnable_node_off_in_task =
offsets.task_struct_scx + offsets.sched_ext_entity_runnable_node;
let runnable_at_off_in_task = offsets.task_struct_scx + offsets.sched_ext_entity_runnable_at;
let mut max_age: u64 = 0;
let mut visited: u32 = 0;
while node_kva != head_kva {
if visited >= MAX_NODES {
return max_age;
}
visited += 1;
let task_kva = node_kva.wrapping_sub(runnable_node_off_in_task as u64);
let runnable_at_kva = task_kva.wrapping_add(runnable_at_off_in_task as u64);
if let Some(runnable_at_pa) = super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
runnable_at_kva,
walk.l5,
walk.tcr_el1,
) {
let runnable_at = mem.read_u64(runnable_at_pa, 0);
if runnable_at != 0 {
let age = jiffies.saturating_sub(runnable_at);
if age > max_age {
max_age = age;
}
}
} else {
tracing::debug!(
task_kva = format_args!("{task_kva:#x}"),
"runnable_scan: per-rq task page untranslatable, skipping",
);
}
let node_pa = match super::idr::translate_any_kva(
mem,
walk.cr3_pa,
walk.page_offset,
node_kva,
walk.l5,
walk.tcr_el1,
) {
Some(pa) => pa,
None => return max_age,
};
let next_kva = mem.read_u64(node_pa, 0);
if next_kva == 0 {
return max_age;
}
node_kva = next_kva;
}
max_age
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::reader::GuestMem;
use crate::monitor::symbols::START_KERNEL_MAP;
fn test_offsets(
task_struct_scx: usize,
sched_ext_entity_tasks_node: usize,
sched_ext_entity_runnable_at: usize,
sched_ext_entity_flags: usize,
) -> RunnableScanOffsets {
RunnableScanOffsets {
task_struct_scx,
sched_ext_entity_tasks_node,
sched_ext_entity_flags,
sched_ext_entity_runnable_at,
sched_ext_entity_runnable_node: 0,
rq_scx: 0,
scx_rq_runnable_list: 0,
}
}
fn with_per_rq(
mut o: RunnableScanOffsets,
sched_ext_entity_runnable_node: usize,
rq_scx: usize,
scx_rq_runnable_list: usize,
) -> RunnableScanOffsets {
o.sched_ext_entity_runnable_node = sched_ext_entity_runnable_node;
o.rq_scx = rq_scx;
o.scx_rq_runnable_list = scx_rq_runnable_list;
o
}
#[test]
fn zero_scx_tasks_kva_returns_zero() {
let mut buf = vec![0u8; 4096];
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(0, 0, 16, 8);
let age = max_runnable_age_global(
&mem,
0,
&offsets,
1_000,
WalkContext::default(),
START_KERNEL_MAP,
0,
);
assert_eq!(age, 0);
}
#[test]
fn empty_list_returns_zero() {
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
buf[0..8].copy_from_slice(&head_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(0, 0, 16, 8);
let page_offset = 0xffff_8880_0000_0000u64;
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
10_000,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(age, 0, "empty list must return age 0");
}
#[test]
fn single_stalled_task_age() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_pa = 64u64;
let task_kva = page_offset.wrapping_add(task_pa);
let node_kva = task_kva + (task_scx + tasks_node_off) as u64;
buf[0..8].copy_from_slice(&node_kva.to_le_bytes());
buf[64..72].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at = jiffies - 50;
let runnable_at_pa = (task_pa as usize) + task_scx + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&runnable_at.to_le_bytes());
let flags_pa = (task_pa as usize) + task_scx + flags_off;
buf[flags_pa..flags_pa + 4].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(age, 50);
}
#[test]
fn future_runnable_at_treated_as_age_zero() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_pa = 64u64;
let task_kva = page_offset.wrapping_add(task_pa);
let node_kva = task_kva;
buf[0..8].copy_from_slice(&node_kva.to_le_bytes());
buf[64..72].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let future = jiffies + 100;
let runnable_at_pa = 64usize + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&future.to_le_bytes());
let flags_pa = 64usize + flags_off;
buf[flags_pa..flags_pa + 4].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(age, 0, "future runnable_at must saturate to age 0");
}
#[test]
fn zero_runnable_at_skipped() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_pa = 64u64;
let task_kva = page_offset.wrapping_add(task_pa);
let node_kva = task_kva;
buf[0..8].copy_from_slice(&node_kva.to_le_bytes());
buf[64..72].copy_from_slice(&head_kva.to_le_bytes());
let runnable_at = 0u64;
let runnable_at_pa = 64usize + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&runnable_at.to_le_bytes());
let flags_pa = 64usize + flags_off;
buf[flags_pa..flags_pa + 4].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let jiffies = 1_000u64;
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 0,
"runnable_at == 0 must be skipped, not treated as a 1000-jiffy stall"
);
}
#[test]
fn max_across_tasks() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_a_pa = 64u64;
let task_a_kva = page_offset.wrapping_add(task_a_pa);
let task_b_pa = 256u64;
let task_b_kva = page_offset.wrapping_add(task_b_pa);
buf[0..8].copy_from_slice(&task_a_kva.to_le_bytes());
buf[64..72].copy_from_slice(&task_b_kva.to_le_bytes());
buf[256..264].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at_a = jiffies - 30;
let runnable_at_b = jiffies - 70;
buf[(64 + runnable_at_off)..(64 + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_a.to_le_bytes());
buf[(256 + runnable_at_off)..(256 + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_b.to_le_bytes());
buf[(64 + flags_off)..(64 + flags_off + 4)].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
buf[(256 + flags_off)..(256 + flags_off + 4)]
.copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(age, 70, "scan must take the max across the global list");
}
#[test]
fn cursor_entry_skipped() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_a_pa = 64u64;
let task_a_kva = page_offset.wrapping_add(task_a_pa);
let cursor_pa = 256u64;
let cursor_kva = page_offset.wrapping_add(cursor_pa);
buf[0..8].copy_from_slice(&task_a_kva.to_le_bytes());
buf[64..72].copy_from_slice(&cursor_kva.to_le_bytes());
buf[256..264].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at_a = jiffies - 30;
buf[(64 + runnable_at_off)..(64 + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_a.to_le_bytes());
buf[(64 + flags_off)..(64 + flags_off + 4)].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let cursor_flags: u32 = 1 << 31;
buf[(256 + flags_off)..(256 + flags_off + 4)].copy_from_slice(&cursor_flags.to_le_bytes());
let cursor_runnable_at: u64 = 1; buf[(256 + runnable_at_off)..(256 + runnable_at_off + 8)]
.copy_from_slice(&cursor_runnable_at.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 30,
"cursor entry must be skipped — only task A's age 30 contributes"
);
}
#[test]
fn cycle_walker_terminates() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_pa = 64u64;
let task_kva = page_offset.wrapping_add(task_pa);
let node_kva = task_kva;
buf[0..8].copy_from_slice(&node_kva.to_le_bytes());
buf[64..72].copy_from_slice(&node_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at = jiffies - 5;
let runnable_at_pa = 64usize + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&runnable_at.to_le_bytes());
let flags_pa = 64usize + flags_off;
buf[flags_pa..flags_pa + 4].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(age, 5);
}
#[test]
fn zero_runnable_at_does_not_abort_walk() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_a_pa = 64u64;
let task_a_kva = page_offset.wrapping_add(task_a_pa);
let task_b_pa = 256u64;
let task_b_kva = page_offset.wrapping_add(task_b_pa);
buf[0..8].copy_from_slice(&task_a_kva.to_le_bytes());
buf[64..72].copy_from_slice(&task_b_kva.to_le_bytes());
buf[256..264].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at_b = jiffies - 80;
buf[(256 + runnable_at_off)..(256 + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_b.to_le_bytes());
buf[(64 + flags_off)..(64 + flags_off + 4)].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
buf[(256 + flags_off)..(256 + flags_off + 4)]
.copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 80,
"zero-runnable_at task must not abort the walk; \
real stall later in the list must still surface",
);
}
#[test]
fn nonzero_task_scx_offset() {
let task_scx = 0x300usize;
let tasks_node_off = 0x60usize;
let runnable_at_off = 0x18usize;
let flags_off = 0x44usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 0x1000];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_pa: u64 = 0x100;
let task_kva = page_offset.wrapping_add(task_pa);
let tasks_node_kva = task_kva + (task_scx + tasks_node_off) as u64;
buf[0..8].copy_from_slice(&tasks_node_kva.to_le_bytes());
let tasks_node_pa = task_pa as usize + task_scx + tasks_node_off;
buf[tasks_node_pa..tasks_node_pa + 8].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at = jiffies - 42;
let runnable_at_pa = task_pa as usize + task_scx + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&runnable_at.to_le_bytes());
let flags_pa = task_pa as usize + task_scx + flags_off;
buf[flags_pa..flags_pa + 4].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 42,
"container_of must subtract task.scx + see.tasks_node, \
not just see.tasks_node, to recover the right task_struct KVA"
);
}
#[test]
fn unqueued_task_with_stale_runnable_at_skipped() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_pa = 64u64;
let task_kva = page_offset.wrapping_add(task_pa);
let node_kva = task_kva;
buf[0..8].copy_from_slice(&node_kva.to_le_bytes());
buf[64..72].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let stale_runnable_at = jiffies - 900;
let runnable_at_pa = 64usize + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&stale_runnable_at.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 0,
"non-queued task must not contribute to max_age \
regardless of runnable_at value (kernel does not \
clear runnable_at on dequeue)"
);
}
#[test]
fn queued_and_unqueued_mixed_takes_queued_age() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = START_KERNEL_MAP + head_pa;
let task_a_pa = 64u64;
let task_a_kva = page_offset.wrapping_add(task_a_pa);
let task_b_pa = 256u64;
let task_b_kva = page_offset.wrapping_add(task_b_pa);
buf[0..8].copy_from_slice(&task_a_kva.to_le_bytes());
buf[64..72].copy_from_slice(&task_b_kva.to_le_bytes());
buf[256..264].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at_a = jiffies - 50;
let runnable_at_b = jiffies - 900;
buf[(64 + runnable_at_off)..(64 + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_a.to_le_bytes());
buf[(256 + runnable_at_off)..(256 + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_b.to_le_bytes());
buf[(64 + flags_off)..(64 + flags_off + 4)].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age_global(
&mem,
head_kva,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 50,
"queued task age must win over an unqueued task's \
stale runnable_at — the unqueued task's 900-jiffy \
value is a leftover stamp, not a current stall"
);
}
#[test]
fn per_rq_empty_list_returns_zero() {
let rq_scx_off = 32usize;
let scx_rq_runnable_list_off = 8usize;
let head_offset = rq_scx_off + scx_rq_runnable_list_off;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let rq_pa: u64 = 64;
let head_pa = rq_pa + head_offset as u64;
let head_kva = head_pa + page_offset;
buf[head_pa as usize..head_pa as usize + 8].copy_from_slice(&head_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = with_per_rq(
test_offsets(0, 0, 16, 8),
24,
rq_scx_off,
scx_rq_runnable_list_off,
);
let age = max_runnable_age_per_rq(
&mem,
rq_pa,
&offsets,
1_000,
WalkContext {
page_offset,
..Default::default()
},
);
assert_eq!(age, 0, "empty per-rq runnable_list must return 0");
}
#[test]
fn per_rq_single_stalled_task_age() {
let task_scx = 0usize;
let runnable_node_off = 24usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let rq_scx_off = 32usize;
let scx_rq_runnable_list_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let rq_pa: u64 = 64;
let head_offset = rq_scx_off + scx_rq_runnable_list_off;
let head_pa = rq_pa + head_offset as u64;
let head_kva = head_pa + page_offset;
let task_pa: u64 = 256;
let task_kva = page_offset + task_pa;
let node_kva = task_kva + (task_scx + runnable_node_off) as u64;
buf[head_pa as usize..head_pa as usize + 8].copy_from_slice(&node_kva.to_le_bytes());
let node_pa = task_pa as usize + task_scx + runnable_node_off;
buf[node_pa..node_pa + 8].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at = jiffies - 75;
let ra_pa = task_pa as usize + task_scx + runnable_at_off;
buf[ra_pa..ra_pa + 8].copy_from_slice(&runnable_at.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = with_per_rq(
test_offsets(task_scx, 0, runnable_at_off, flags_off),
runnable_node_off,
rq_scx_off,
scx_rq_runnable_list_off,
);
let age = max_runnable_age_per_rq(
&mem,
rq_pa,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
);
assert_eq!(
age, 75,
"per-rq walker must compose rq.scx + scx_rq.runnable_list to find the head, \
then container_of through runnable_node to reach the task"
);
}
#[test]
fn per_rq_zero_rq_pa_returns_zero() {
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let bogus_head_kva = page_offset + 0xff_ff_ff_00u64;
buf[40..48].copy_from_slice(&bogus_head_kva.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = with_per_rq(test_offsets(0, 0, 16, 8), 24, 32, 8);
let age = max_runnable_age_per_rq(
&mem,
0,
&offsets,
1_000,
WalkContext {
page_offset,
..Default::default()
},
);
assert_eq!(age, 0, "rq_pa == 0 must short-circuit the per-rq walker");
}
#[test]
fn max_runnable_age_takes_max_across_walks() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_node_off = 24usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let rq_scx_off = 32usize;
let scx_rq_runnable_list_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 0x2000];
let head_pa = 0u64;
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + head_pa;
let task_a_pa = 64u64;
let task_a_kva = page_offset + task_a_pa;
let node_a_kva = task_a_kva;
buf[head_pa as usize..head_pa as usize + 8].copy_from_slice(&node_a_kva.to_le_bytes());
buf[task_a_pa as usize..task_a_pa as usize + 8].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at_a = jiffies - 30;
buf[(task_a_pa as usize + runnable_at_off)..(task_a_pa as usize + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_a.to_le_bytes());
buf[(task_a_pa as usize + flags_off)..(task_a_pa as usize + flags_off + 4)]
.copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let rq_pa: u64 = 1024;
let prq_head_pa = rq_pa + (rq_scx_off + scx_rq_runnable_list_off) as u64;
let prq_head_kva = prq_head_pa + page_offset;
let task_b_pa: u64 = 1280;
let task_b_kva = page_offset + task_b_pa;
let node_b_kva = task_b_kva + (task_scx + runnable_node_off) as u64;
buf[prq_head_pa as usize..prq_head_pa as usize + 8]
.copy_from_slice(&node_b_kva.to_le_bytes());
let node_b_pa = task_b_pa as usize + task_scx + runnable_node_off;
buf[node_b_pa..node_b_pa + 8].copy_from_slice(&prq_head_kva.to_le_bytes());
let runnable_at_b = jiffies - 70;
buf[(task_b_pa as usize + runnable_at_off)..(task_b_pa as usize + runnable_at_off + 8)]
.copy_from_slice(&runnable_at_b.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = with_per_rq(
test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off),
runnable_node_off,
rq_scx_off,
scx_rq_runnable_list_off,
);
let rq_pas = vec![rq_pa];
let age = max_runnable_age(
&mem,
head_kva,
&rq_pas,
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
None,
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 70,
"wrapper must take max(global, per-rq) — per-rq's 70 > global's 30"
);
}
#[test]
fn max_runnable_age_empty_rq_pas_uses_global() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + head_pa;
let task_pa = 64u64;
let task_kva = page_offset.wrapping_add(task_pa);
let node_kva = task_kva;
buf[0..8].copy_from_slice(&node_kva.to_le_bytes());
buf[64..72].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let runnable_at = jiffies - 42;
let runnable_at_pa = 64usize + runnable_at_off;
buf[runnable_at_pa..runnable_at_pa + 8].copy_from_slice(&runnable_at.to_le_bytes());
let flags_pa = 64usize + flags_off;
buf[flags_pa..flags_pa + 4].copy_from_slice(&SCX_TASK_QUEUED.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age(
&mem,
head_kva,
&[],
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
None,
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 42,
"empty rq_pas slice must defer to the global walk's age"
);
}
#[test]
fn max_runnable_age_uses_watchdog_timestamp() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + head_pa;
buf[0..8].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let stale_timestamp = jiffies - 800;
let watchdog_pa: u64 = 256;
buf[256..264].copy_from_slice(&stale_timestamp.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age(
&mem,
head_kva,
&[],
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
Some(watchdog_pa),
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 800,
"watchdog_timestamp must contribute when both per-task walks are empty",
);
}
#[test]
fn max_runnable_age_zero_watchdog_timestamp_skipped() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + head_pa;
buf[0..8].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let watchdog_pa: u64 = 256;
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age(
&mem,
head_kva,
&[],
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
Some(watchdog_pa),
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 0,
"zero watchdog_timestamp must be skipped, not synthesise jiffies of age",
);
}
#[test]
fn max_runnable_age_pre_attach_watchdog_capped() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + head_pa;
buf[0..8].copy_from_slice(&head_kva.to_le_bytes());
let timestamp = 1u64;
let jiffies = 100_000_000u64;
let watchdog_pa: u64 = 256;
buf[256..264].copy_from_slice(×tamp.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age(
&mem,
head_kva,
&[],
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
Some(watchdog_pa),
START_KERNEL_MAP,
0,
);
assert_eq!(
age, 0,
"implausibly large watchdog age (pre-attach boot window) must be capped to 0",
);
}
#[test]
fn max_runnable_age_future_watchdog_timestamp_saturates() {
let task_scx = 0usize;
let tasks_node_off = 0usize;
let runnable_at_off = 16usize;
let flags_off = 8usize;
let page_offset = 0xffff_8880_0000_0000u64;
let mut buf = vec![0u8; 4096];
let head_pa = 0u64;
let head_kva = crate::monitor::symbols::START_KERNEL_MAP + head_pa;
buf[0..8].copy_from_slice(&head_kva.to_le_bytes());
let jiffies = 1_000u64;
let future_timestamp = jiffies + 100;
let watchdog_pa: u64 = 256;
buf[256..264].copy_from_slice(&future_timestamp.to_le_bytes());
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let offsets = test_offsets(task_scx, tasks_node_off, runnable_at_off, flags_off);
let age = max_runnable_age(
&mem,
head_kva,
&[],
&offsets,
jiffies,
WalkContext {
page_offset,
..Default::default()
},
Some(watchdog_pa),
START_KERNEL_MAP,
0,
);
assert_eq!(age, 0, "future watchdog_timestamp must saturate to age 0",);
}
}