Introduction
This is the last of a 3-part series about memory compaction in the Linux kernel. The previous two blogs in this series (Part 1, Part 2) covered how Linux memory compaction works internally and what diagnostic data the compaction subsystem exposes through tracepoints and other interfaces. Part-3 (this part) is the practical companion — it demonstrates how to use bpftrace, dtrace and shell scripts to collect and interpret that data when diagnosing real compaction problems. The scripts have been tested against the v6.18 kernel but can be used with other kernels too with minimal or no changes.
The scripts are organised into two sections, in the same order as Part 2’s two halves: the first covers bpftrace and dtrace scripts built on the compaction tracepoints, and the second covers shell scripts built on /proc, /sys and debugfs. This ordering mirrors Part 2, but it is not a workflow prescription. In practice, the shell scripts are usually the first thing to run on an unfamiliar system — they need no tracing setup, and they tell you quickly whether compaction is even the issue. The tracepoint-based scripts come in once compaction is confirmed as the suspect and further detail is needed: for example, not just that fragmentation exists, but exactly when and how many times compaction was attempted within a time window. There is no hard rule, though — if you already suspect compaction, starting with bpftrace or dtrace is perfectly reasonable. Between bpftrace and dtrace, the scripts are largely equivalent and can be converted from one form to other. I have given some examples in bpftrace and others in dtrace just to show that both options are usable. dtrace is available on Oracle Linux via dtrace-utils, while bpftrace works on both UEK and upstream kernels. For dtrace scripts, dtrace version 2.0.2 or later should be used on UEK7 and later kernels. The scripts may work with lower dtrace version and older kernels too, but have not been tested on such setups.
Tracepoint scripts: bpftrace and dtrace
The bpftrace and dtrace scripts in this section are built on the compaction tracepoints, and they are presented in the order you would ask questions when debugging: first, is compaction being attempted at all? Second, when it is attempted, is it actually doing work, or is it skipping or deferring zones most of the time? And third, when it does the work, how often does it succeed? If compaction is not relieving fragmentation, the reason will show up at one of these three stages, and the corresponding script narrows it down.
Is compaction being attempted
The bpftrace script shown below uses mm_compaction_try_to_compact_pages to periodically (given as argument in secs or 10 secs by default) provide information about direct compaction requests; it shows which paths are triggering direct compaction, how frequently, and for what order.
#!/usr/bin/env bpftrace
/*
* License: GPLv2
*/
BEGIN
{
@interval = $1 ? $1 : 10;
@_countdown = @interval;
@activity = 0;
printf("Starting compaction monitor to track direct compactions: %llu second interval (reports only on activity)\n", @interval);
}
tracepoint:compaction:mm_compaction_try_to_compact_pages
{
@direct_compaction_stack[kstack]++;
@direct_compaction_order[args->order]++;
@activity = 1;
}
interval:s:1
{
@_countdown--;
if (@_countdown == 0) {
@_countdown = @interval;
if (@activity) {
printf("\n[%s]\n", strftime("%Y-%m-%d %H:%M:%S", nsecs));
printf("--- Direct compaction call stacks ---\n");
print(@direct_compaction_stack);
printf("--- Direct compaction by allocation order ---\n");
print(@direct_compaction_order);
clear(@direct_compaction_stack);
clear(@direct_compaction_order);
@activity = 0;
}
}
}
END
{
clear(@interval); clear(@_countdown); clear(@activity);
clear(@direct_compaction_stack);
clear(@direct_compaction_order);
}
One sample output of this script is shown below. We can see it has 2 parts. The first part, under --- Direct compaction call stacks ---, shows which call paths triggered direct compaction and how many times during the last interval. The second part of the output — under --- Direct compaction by allocation order --- — shows how many times, during the last interval, different allocation orders triggered direct compactions. This data was collected while a request for 30000 2-MB huge pages was made through /proc/sys/vm/nr_hugepages:
[2026-07-04 12:28:18]
--- Direct compaction call stacks ---
@direct_compaction_stack[
try_to_compact_pages+701
__alloc_pages_direct_compact+146
__alloc_pages_slowpath.constprop.0+1081
__alloc_pages_noprof+801
__folio_alloc_noprof+20
alloc_buddy_hugetlb_folio.isra.0+101
alloc_pool_huge_folio+334
set_max_huge_pages+565
__nr_hugepages_store_common+88
hugetlb_sysctl_handler_common+244
proc_sys_call_handler+370
vfs_write+666
ksys_write+109
do_syscall_64+140
entry_SYSCALL_64_after_hwframe+118
]: 33
@direct_compaction_stack[
try_to_compact_pages+701
__alloc_pages_direct_compact+146
__alloc_pages_slowpath.constprop.0+581
__alloc_pages_noprof+801
__folio_alloc_noprof+20
alloc_buddy_hugetlb_folio.isra.0+101
alloc_pool_huge_folio+334
set_max_huge_pages+565
__nr_hugepages_store_common+88
hugetlb_sysctl_handler_common+244
proc_sys_call_handler+370
vfs_write+666
ksys_write+109
do_syscall_64+140
entry_SYSCALL_64_after_hwframe+118
]: 625
--- Direct compaction by allocation order ---
@direct_compaction_order[9]: 658
This shows 658 order-9 direct compaction attempts in that interval. The call path came from the hugetlb pool growth path, which is expected because a 2-MB huge page maps to order-9 on a system with 4-KB base pages.
Sometimes compaction needs are very minimal or at least not severe enough to need long cycles or even direct compaction. In such cases it’s handy to see how frequently the per-node kcompactdN thread is waking up and how long it’s running. This complements the direct-compaction script above: that script shows direct compaction attempts, while the following dtrace script shows background or indirect compaction activity through kcompactd.
#!/usr/sbin/dtrace -Cs
/*
* License: GPLv2
*/
#pragma D option quiet
#pragma D option defaultargs
dtrace:::BEGIN
{
interval = $1 ? $1 : 10;
printf("Monitoring kcompactd per-node activity, interval: %d seconds (reports only on activity)\n\n", interval);
printf("%-25s %-12s %10s %15s %15s\n",
"[Time]", "[Thread]", "Wakeups", "Total(us)", "Avg(us)");
}
sdt:compaction::mm_compaction_wakeup_kcompactd
{
this->key = strjoin("kcompactd", lltostr(arg0));
@wakeups[this->key] = count();
activity = 1;
}
sdt:compaction::mm_compaction_kcompactd_wake
{
start_ts[arg0] = timestamp;
this->key = strjoin("kcompactd", lltostr(arg0));
activity = 1;
}
sdt:compaction::mm_compaction_kcompactd_sleep
/start_ts[arg0]/
{
this->elapsed_us = (timestamp - start_ts[arg0]) / 1000;
this->key = strjoin("kcompactd", lltostr(arg0));
@runtime_us[this->key] = sum(this->elapsed_us);
@avgtime_us[this->key] = avg(this->elapsed_us);
start_ts[arg0] = 0;
}
tick-1s
/activity && (((timestamp / 1000000000) % interval) == 0)/
{
printf("%-25Y\n", walltimestamp);
printa(" %-12s %10@d %15@d %15@d\n",
@wakeups, @runtime_us, @avgtime_us);
activity = 0;
trunc(@wakeups);
trunc(@runtime_us);
trunc(@avgtime_us);
}
It produces an output like:
Monitoring kcompactd per-node activity, interval: 1 seconds (reports only on activity)
[Time] [Thread] Wakeups Total(us) Avg(us)
2026 Jul 4 12:43:26
kcompactd0 1 0 0
2026 Jul 4 12:43:42
kcompactd0 5 21 3
2026 Jul 4 12:43:48
kcompactd0 1 0 0
This shows that kcompactd0 was woken during the test window. The middle row recorded five wakeups and a total measured runtime of 21 microseconds, so the daemon had very little work to do on this system. Such output can show if indirect compaction is getting triggered frequently and this in turn can signal that memory is already quite fragmented — not severely enough to slow things down, but still worth checking. Since the data is shown individually for the kcompactdN thread of each node, it can show if one node is more fragmented than the other.
Once we know that direct or background compaction is being attempted, the next useful question is whether the kernel considered compaction suitable for the requested allocation order. The mm_compaction_suitable tracepoint answers that question before the scanner and migration tracepoints tell us how much work happened.
Is the compaction attempt doing any work or just skipping zones
The following bpftrace script answers this question using mm_compaction_suitable trace event:
#!/usr/bin/env bpftrace
/*
* Summarize mm_compaction_suitable decisions.
*
* Usage:
* sudo bpftrace compaction_suitability.bt
*
* Return value legend from the tracepoint format:
* 0 not_suitable_zone
* 1 skipped
* 2 deferred
* 3 no_suitable_page
* 4 continue
* 5 complete
* 6 partial_skipped
* 7 contended
* 8 success
*
* Zone legend:
* 0 DMA, 1 DMA32, 2 Normal, 3 Movable
*
* License: GPLv2
*/
BEGIN
{
printf("Tracing mm_compaction_suitable decisions. Hit Ctrl-C to stop.\n");
printf("Printing a summary every 5 seconds, only after activity.\n");
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 0/
{
@activity = 1;
@suitability_decision["not_suitable_zone"] = count();
@by_order_suitability_decision[args->order, "not_suitable_zone"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "not_suitable_zone"] = count();
@by_task_suitability_decision[comm, "not_suitable_zone"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 1/
{
@activity = 1;
@suitability_decision["skipped"] = count();
@by_order_suitability_decision[args->order, "skipped"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "skipped"] = count();
@by_task_suitability_decision[comm, "skipped"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 2/
{
@activity = 1;
@suitability_decision["deferred"] = count();
@by_order_suitability_decision[args->order, "deferred"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "deferred"] = count();
@by_task_suitability_decision[comm, "deferred"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 3/
{
@activity = 1;
@suitability_decision["no_suitable_page"] = count();
@by_order_suitability_decision[args->order, "no_suitable_page"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "no_suitable_page"] = count();
@by_task_suitability_decision[comm, "no_suitable_page"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 4/
{
@activity = 1;
@suitability_decision["continue"] = count();
@by_order_suitability_decision[args->order, "continue"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "continue"] = count();
@by_task_suitability_decision[comm, "continue"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 5/
{
@activity = 1;
@suitability_decision["complete"] = count();
@by_order_suitability_decision[args->order, "complete"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "complete"] = count();
@by_task_suitability_decision[comm, "complete"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 6/
{
@activity = 1;
@suitability_decision["partial_skipped"] = count();
@by_order_suitability_decision[args->order, "partial_skipped"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "partial_skipped"] = count();
@by_task_suitability_decision[comm, "partial_skipped"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 7/
{
@activity = 1;
@suitability_decision["contended"] = count();
@by_order_suitability_decision[args->order, "contended"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "contended"] = count();
@by_task_suitability_decision[comm, "contended"] = count();
}
tracepoint:compaction:mm_compaction_suitable
/args->ret == 8/
{
@activity = 1;
@suitability_decision["success"] = count();
@by_order_suitability_decision[args->order, "success"] = count();
@by_zone_suitability_decision[args->nid, args->idx, "success"] = count();
@by_task_suitability_decision[comm, "success"] = count();
}
interval:s:5
/@activity/
{
printf("\n%s summary for mm_compaction_suitable:\n",
strftime("%Y-%m-%d %H:%M:%S", nsecs));
printf("Counts by suitability decision:\n");
print(@suitability_decision);
printf("Counts by order and suitability decision:\n");
print(@by_order_suitability_decision);
printf("Counts by node, zone, and suitability decision:\n");
print(@by_zone_suitability_decision);
printf("Counts by task and suitability decision:\n");
print(@by_task_suitability_decision);
clear(@suitability_decision);
clear(@by_order_suitability_decision);
clear(@by_zone_suitability_decision);
clear(@by_task_suitability_decision);
@activity = 0;
}
END
{
printf("\nFinal summary for mm_compaction_suitable:\n");
printf("Counts by suitability decision:\n");
print(@suitability_decision);
printf("Counts by order and suitability decision:\n");
print(@by_order_suitability_decision);
printf("Counts by node, zone, and suitability decision:\n");
print(@by_zone_suitability_decision);
printf("Counts by task and suitability decision:\n");
print(@by_task_suitability_decision);
clear(@suitability_decision);
clear(@by_order_suitability_decision);
clear(@by_zone_suitability_decision);
clear(@by_task_suitability_decision);
clear(@activity);
}
The following output was collected while repeatedly requesting a hugetlb pool larger than what the system could fully satisfy:
2026-07-05 02:13:00 summary for mm_compaction_suitable:
Counts by suitability decision:
@suitability_decision[continue]: 4
@suitability_decision[skipped]: 16
Counts by order and suitability decision:
@by_order_suitability_decision[9, continue]: 4
@by_order_suitability_decision[9, skipped]: 16
Counts by node, zone, and suitability decision:
@by_zone_suitability_decision[0, ZONE_NORMAL, continue]: 4
@by_zone_suitability_decision[0, ZONE_DMA, skipped]: 8
@by_zone_suitability_decision[0, ZONE_DMA32, skipped]: 8
Counts by task and suitability decision:
@by_task_suitability_decision[kswapd0, continue]: 1
@by_task_suitability_decision[kswapd0, skipped]: 2
@by_task_suitability_decision[bash, continue]: 3
@by_task_suitability_decision[bash, skipped]: 14
2026-07-05 02:13:05 summary for mm_compaction_suitable:
Counts by suitability decision:
@suitability_decision[continue]: 81
@suitability_decision[skipped]: 119
Counts by order and suitability decision:
@by_order_suitability_decision[9, continue]: 81
@by_order_suitability_decision[9, skipped]: 119
Counts by node, zone, and suitability decision:
@by_zone_suitability_decision[0, ZONE_DMA32, skipped]: 58
@by_zone_suitability_decision[0, ZONE_DMA, skipped]: 61
@by_zone_suitability_decision[0, ZONE_NORMAL, continue]: 81
Counts by task and suitability decision:
@by_task_suitability_decision[kswapd0, continue]: 6
@by_task_suitability_decision[kcompactd0, continue]: 10
@by_task_suitability_decision[kswapd0, skipped]: 27
@by_task_suitability_decision[bash, continue]: 65
@by_task_suitability_decision[bash, skipped]: 92
All entries are for order 9, matching the 2-MB huge-page request on a 4-KB base-page system. In the busiest interval, continue occurred 81 times, all in ZONE_NORMAL, meaning the kernel considered compaction worth continuing there. skipped occurred 119 times, split across DMA and DMA32, meaning those zones were skipped for this high-order request. The task breakdown shows both direct pressure from the shell path and background reclaim/compaction context from kswapd0 and kcompactd0.
If compaction attempts are being skipped or throttled, the defer-state tracepoints can show whether the kernel has temporarily deferred compaction for a zone and order. The following bpftrace script tracks when defer state is raised, observed, and reset:
#!/usr/bin/env bpftrace
/*
* Track compaction defer state transitions.
*
* Usage:
* sudo bpftrace compaction_defer_state.bt
*
* Deferral event labels used in summary maps:
* defer_compaction - compaction failed and defer state was raised
* deferred - compaction was skipped because the defer state is active
* defer_reset - compaction made enough progress and defer state was reset
*
* Zone legend:
* 0 DMA, 1 DMA32, 2 Normal, 3 Movable
*
* limit = 2 ^ defer_shift, matching the tracepoint print format.
*
* License: GPLv2
*/
BEGIN
{
printf("Tracing compaction defer state. Hit Ctrl-C to stop.\n");
printf("Printing a summary every 5 seconds, only after activity.\n");
}
tracepoint:compaction:mm_compaction_defer_compaction
{
@activity = 1;
@deferral_event["defer_compaction"] = count();
@by_order_deferral_event[args->order, "defer_compaction"] = count();
@by_zone_deferral_event[args->nid, args->idx, "defer_compaction"] = count();
@by_task_deferral_event[comm, "defer_compaction"] = count();
@considered_max[args->order, "defer_compaction"] = max(args->considered);
@defer_shift_max[args->order, "defer_compaction"] = max(args->defer_shift);
@order_failed_max[args->order, "defer_compaction"] = max(args->order_failed);
}
tracepoint:compaction:mm_compaction_deferred
{
@activity = 1;
@deferral_event["deferred"] = count();
@by_order_deferral_event[args->order, "deferred"] = count();
@by_zone_deferral_event[args->nid, args->idx, "deferred"] = count();
@by_task_deferral_event[comm, "deferred"] = count();
@considered_max[args->order, "deferred"] = max(args->considered);
@defer_shift_max[args->order, "deferred"] = max(args->defer_shift);
@order_failed_max[args->order, "deferred"] = max(args->order_failed);
}
tracepoint:compaction:mm_compaction_defer_reset
{
@activity = 1;
@deferral_event["defer_reset"] = count();
@by_order_deferral_event[args->order, "defer_reset"] = count();
@by_zone_deferral_event[args->nid, args->idx, "defer_reset"] = count();
@by_task_deferral_event[comm, "defer_reset"] = count();
@considered_max[args->order, "defer_reset"] = max(args->considered);
@defer_shift_max[args->order, "defer_reset"] = max(args->defer_shift);
@order_failed_max[args->order, "defer_reset"] = max(args->order_failed);
}
interval:s:5
/@activity/
{
printf("\n%s summary for compaction defer state:\n",
strftime("%Y-%m-%d %H:%M:%S", nsecs));
printf("Counts by deferral event:\n");
print(@deferral_event);
printf("Counts by order and deferral event:\n");
print(@by_order_deferral_event);
printf("Counts by node, zone, and deferral event:\n");
print(@by_zone_deferral_event);
printf("Counts by task and deferral event:\n");
print(@by_task_deferral_event);
printf("Maximum considered counter, keyed by order and deferral event:\n");
print(@considered_max);
printf("Maximum defer_shift, keyed by order and deferral event:\n");
print(@defer_shift_max);
printf("Maximum order_failed, keyed by order and deferral event:\n");
print(@order_failed_max);
clear(@deferral_event);
clear(@by_order_deferral_event);
clear(@by_zone_deferral_event);
clear(@by_task_deferral_event);
clear(@considered_max);
clear(@defer_shift_max);
clear(@order_failed_max);
@activity = 0;
}
END
{
printf("\nFinal summary for compaction defer state:\n");
printf("Counts by deferral event:\n");
print(@deferral_event);
printf("Counts by order and deferral event:\n");
print(@by_order_deferral_event);
printf("Counts by node, zone, and deferral event:\n");
print(@by_zone_deferral_event);
printf("Counts by task and deferral event:\n");
print(@by_task_deferral_event);
printf("Maximum considered counter, keyed by order and deferral event:\n");
print(@considered_max);
printf("Maximum defer_shift, keyed by order and deferral event:\n");
print(@defer_shift_max);
printf("Maximum order_failed, keyed by order and deferral event:\n");
print(@order_failed_max);
clear(@deferral_event);
clear(@by_order_deferral_event);
clear(@by_zone_deferral_event);
clear(@by_task_deferral_event);
clear(@considered_max);
clear(@defer_shift_max);
clear(@order_failed_max);
clear(@activity);
}
The following data was collected while alternating between over-sized and successful huge-page requests:
2026-07-05 02:13:55 summary for compaction defer state:
Counts by deferral event:
@deferral_event[deferred]: 1
@deferral_event[defer_compaction]: 1
@deferral_event[defer_reset]: 143
Counts by order and deferral event:
@by_order_deferral_event[9, defer_compaction]: 1
@by_order_deferral_event[9, deferred]: 1
@by_order_deferral_event[9, defer_reset]: 143
Counts by node, zone, and deferral event:
@by_zone_deferral_event[0, ZONE_NORMAL, deferred]: 1
@by_zone_deferral_event[0, ZONE_NORMAL, defer_compaction]: 1
@by_zone_deferral_event[0, ZONE_NORMAL, defer_reset]: 143
Counts by task and deferral event:
@by_task_deferral_event[bash, defer_compaction]: 1
@by_task_deferral_event[kcompactd0, deferred]: 1
@by_task_deferral_event[kcompactd0, defer_reset]: 11
@by_task_deferral_event[bash, defer_reset]: 132
Maximum considered counter, keyed by order and deferral event:
@considered_max[9, defer_compaction]: 0
@considered_max[9, deferred]: 1
@considered_max[9, defer_reset]: 2
Maximum defer_shift, keyed by order and deferral event:
@defer_shift_max[9, defer_reset]: 1
@defer_shift_max[9, defer_compaction]: 1
@defer_shift_max[9, deferred]: 1
Maximum order_failed, keyed by order and deferral event:
@order_failed_max[9, defer_compaction]: 9
@order_failed_max[9, deferred]: 9
@order_failed_max[9, defer_reset]: 10
2026-07-05 02:14:00 summary for compaction defer state:
Counts by deferral event:
@deferral_event[deferred]: 1
@deferral_event[defer_reset]: 1
@deferral_event[defer_compaction]: 1
Counts by order and deferral event:
@by_order_deferral_event[9, defer_reset]: 1
@by_order_deferral_event[9, defer_compaction]: 1
@by_order_deferral_event[9, deferred]: 1
Counts by node, zone, and deferral event:
@by_zone_deferral_event[0, ZONE_NORMAL, deferred]: 1
@by_zone_deferral_event[0, ZONE_NORMAL, defer_compaction]: 1
@by_zone_deferral_event[0, ZONE_NORMAL, defer_reset]: 1
Counts by task and deferral event:
@by_task_deferral_event[bash, defer_compaction]: 1
@by_task_deferral_event[kcompactd0, deferred]: 1
@by_task_deferral_event[bash, defer_reset]: 1
Maximum considered counter, keyed by order and deferral event:
@considered_max[9, defer_reset]: 0
@considered_max[9, defer_compaction]: 0
@considered_max[9, deferred]: 1
Maximum defer_shift, keyed by order and deferral event:
@defer_shift_max[9, defer_reset]: 0
@defer_shift_max[9, defer_compaction]: 1
@defer_shift_max[9, deferred]: 1
Maximum order_failed, keyed by order and deferral event:
@order_failed_max[9, defer_compaction]: 9
@order_failed_max[9, deferred]: 9
@order_failed_max[9, defer_reset]: 10
All defer-state activity in this run was for order 9 in ZONE_NORMAL, which again matches the hugetlb workload. defer_compaction means the kernel raised or updated defer state after a failed compaction attempt. deferred means a later attempt was skipped because the zone/order pair was already deferred. defer_reset means later compaction progress was sufficient to clear that state. These tracepoints therefore explain why the kernel may avoid compaction even before scanner activity becomes visible.
When cmpaction does the work, how often does it succeed
After finding out that compaction attempts are being made and compaction code is not deferring or skipping it, the next logical step is to see how much success the compaction attempts are achieving. We may run into situations where due to scattered unmovable pages or due to other reasons like writeback or dirty pages, despite trying multiple times, compaction passes don’t yield enough large pages. In such cases it can be useful to see how individual passes are faring. The following bpftrace script can be used in such cases. It hooks into mm_compaction_begin and mm_compaction_end to cover one pass and within that pass it hooks into other isolation and migration events to record how many pages were isolated by scanners and how many could or could not be migrated.
#!/usr/bin/env bpftrace
/*
* License: GPLv2
*/
BEGIN
{
printf("Starting compaction monitor to summarise individual compaction cycles\n");
}
tracepoint:compaction:mm_compaction_begin
{
@start[tid] = nsecs;
@mig_scn[tid] = (uint64)0;
@mig_tkn[tid] = (uint64)0;
@ff_scn[tid] = (uint64)0;
@ff_tkn[tid] = (uint64)0;
@free_scn[tid] = (uint64)0;
@free_tkn[tid] = (uint64)0;
@migrated[tid] = (uint64)0;
@mig_fail[tid] = (uint64)0;
}
tracepoint:compaction:mm_compaction_isolate_migratepages
/@start[tid]/
{
@mig_scn[tid] += args->nr_scanned;
@mig_tkn[tid] += args->nr_taken;
}
tracepoint:compaction:mm_compaction_fast_isolate_freepages
/@start[tid]/
{
@ff_scn[tid] += args->nr_scanned;
@ff_tkn[tid] += args->nr_taken;
}
tracepoint:compaction:mm_compaction_isolate_freepages
/@start[tid]/
{
@free_scn[tid] += args->nr_scanned;
@free_tkn[tid] += args->nr_taken;
}
tracepoint:compaction:mm_compaction_migratepages
/@start[tid]/
{
@migrated[tid] += args->nr_migrated;
@mig_fail[tid] += args->nr_failed;
}
tracepoint:compaction:mm_compaction_end
/@start[tid]/
{
$dur_us = (nsecs - @start[tid]) / 1000;
printf("%s Compaction summary (%s):\n", strftime("%Y-%m-%d %H:%M:%S", nsecs), comm);
printf(" %-25s: %llu\n", "start time", @start[tid]);
printf(" %-25s: %llu\n", "end time", nsecs);
printf(" %-25s: %llu\n", "duration (usecs)", $dur_us);
printf(" %-25s: %llu\n", "migratable pages scanned", @mig_scn[tid]);
printf(" %-25s: %llu\n", "migratable pages taken", @mig_tkn[tid]);
printf(" %-25s: %llu\n", "fast free pages scanned", @ff_scn[tid]);
printf(" %-25s: %llu\n", "fast free pages taken", @ff_tkn[tid]);
printf(" %-25s: %llu\n", "free pages scanned", @free_scn[tid]);
printf(" %-25s: %llu\n", "free pages taken", @free_tkn[tid]);
printf(" %-25s: %llu\n", "successful migration", @migrated[tid]);
printf(" %-25s: %llu\n", "failed migration", @mig_fail[tid]);
delete(@start[tid]);
delete(@mig_scn[tid]); delete(@mig_tkn[tid]);
delete(@ff_scn[tid]); delete(@ff_tkn[tid]);
delete(@free_scn[tid]); delete(@free_tkn[tid]);
delete(@migrated[tid]); delete(@mig_fail[tid]);
}
END
{
clear(@start);
clear(@mig_scn); clear(@mig_tkn);
clear(@ff_scn); clear(@ff_tkn);
clear(@free_scn); clear(@free_tkn);
clear(@migrated); clear(@mig_fail);
}
The following output was collected while triggering manual compaction from a shell:
2026-07-04 12:10:21 Compaction summary (sh):
start time : 1716364815151467
end time : 1716364829475277
duration (usecs) : 14314
migratable pages scanned : 205344
migratable pages taken : 32
fast free pages scanned : 0
fast free pages taken : 0
free pages scanned : 538438
free pages taken : 18
successful migration : 18
failed migration : 14
Here sh appears as the task because the compaction was triggered by a shell write to /proc/sys/vm/compact_memory. The pass scanned 205344 migratable pages, isolated 32 of them, and successfully migrated 18. The remaining 14 isolated pages failed migration, which is the kind of signal this script is intended to expose.
Starting with oled-tools version 1.3.0.2 and later, oled-tools also ships a DTrace script named compaction_tracker.d for this kind of compaction tracking. This script can be run through the oled front end, as shown below:
# periodic mode, 10-second interval by default
sudo oled scripts run compaction_tracker.d -Duek8
# periodic mode, 1-second interval
sudo oled scripts run compaction_tracker.d -Duek8 0 1
# cyclic mode, one row per compaction cycle
sudo oled scripts run compaction_tracker.d -Duek8 1
The -Duek8 macro enables columns for the mm_compaction_fast_isolate_freepages tracepoint, which is available on newer UEK (UEK8 and later) kernels . The script can run in 2 modes, periodic and cyclic. Periodic mode aggregates scanner and migration counts by task at a fixed interval. Cyclic mode prints one row per mm_compaction_begin/mm_compaction_end pair and includes the cycle duration.
For example, the following oled cyclic-mode run was collected while manual compaction was triggered from a shell:
# sudo oled scripts run compaction_tracker.d -Duek8 1
2026-07-04 14:01:27.617 INFO - Running script '/usr/libexec/oled-tools/scripts.d/compaction_tracker.d -Duek8 1'...
Starting compaction monitor in cyclic mode
[Time] [TASK] MIso MScn MTkn FFIso FFScn FFTkn FIso FScn FTkn MigOK MigFail Dur(ms)
2026 Jul 4 14:01:29 sh 205 126514 437 0 0 0 16146 475844 416 416 21 25
2026 Jul 4 14:01:31 sh 205 126976 431 0 0 0 16148 475924 380 380 51 26
2026 Jul 4 14:01:32 sh 211 130048 559 0 0 0 16145 491444 377 377 182 26
2026 Jul 4 14:01:33 sh 381 245248 756 0 0 0 16078 474931 443 443 313 26
In the above output, MIso indicates how many times the migration scanner was invoked to isolate migratable pages. MScn and MTkn respectively indicate scanned and taken pages by the migration scanner. FFIso, FFScn, and FFTkn describe fast free-page isolation. FIso, FScn, and FTkn describe regular free-page isolation. MigOK is how many pages migrated successfully, and MigFail is how many could not be migrated. The task is sh because compaction was requested through /proc/sys/vm/compact_memory. The final row shows a 26 ms compaction cycle: 381 migration-isolation attempts scanned 245248 pages and took 756 pages. The free scanner took 443 pages; 443 pages migrated successfully and 313 failed migration.
The same cyclic mode can also catch kcompactd activity. The following short run was collected while temporarily growing the hugetlb pool:
# sudo oled scripts run compaction_tracker.d -Duek8 1
Starting compaction monitor in cyclic mode
[Time] [TASK] MIso MScn MTkn FFIso FFScn FFTkn FIso FScn FTkn MigOK MigFail Dur(ms)
2026 Jul 4 14:02:04 kcompactd0 2 512 128 1 258 256 0 0 0 128 0 0
This row shows a small kcompactd cycle during the huge-page request: 128 pages were taken for migration and all 128 migrated successfully.
Getting this summary for individual passes is useful, but one may prefer looking at the same scanner and migration counters at a set frequency. Since oled-tools already packages this logic in compaction_tracker.d, there is no need to carry a separate periodic DTrace script here. The full DTrace source can be inspected on a system with oled-tools installed:
rpm -ql oled-tools | grep compaction_tracker.d
less /usr/libexec/oled-tools/scripts.d/compaction_tracker.d
In periodic mode, compaction_tracker.d uses the same scanner and migration tracepoints and prints one set of task-keyed counters per interval.
The following one-second oled run was collected while manual compaction was triggered from a shell:
# sudo oled scripts run compaction_tracker.d -Duek8 0 1
2026-07-04 14:01:15.218 INFO - Running script '/usr/libexec/oled-tools/scripts.d/compaction_tracker.d -Duek8 0 1'...
Starting compaction monitor in periodic mode : 1 seconds interval (reports only on activity)
[Time] [TASK] MIso MScn MTkn FFIso FFScn FFTkn FIso FScn FTkn MigOK MigFail
2026 Jul 4 14:01:17
sh 1147 772656 32 0 0 0 8426 153094 0 0 0
2026 Jul 4 14:01:19
sh 1148 773119 389 0 0 0 16189 494176 68 68 321
2026 Jul 4 14:01:22
sh 1148 773119 334 0 0 0 16189 512637 201 201 133
During the 1 sec interval ending 14:01:19, the migration scanner was invoked 1148 times, scanned 773119 pages, and isolated 389 pages. Of those isolated pages, 68 migrated successfully and 321 failed migration. It must be noted that the script shows data only when some activity is seen, so time difference seen between successive rows should not be taken as length of duration.
A temporary request for 30000 2-MB huge pages gives a more demanding high-order allocation workload and shows both direct compaction from the shell path and background kcompactd activity:
# sudo oled scripts run compaction_tracker.d -Duek8 0 1
2026-07-04 14:01:40.25 INFO - Running script '/usr/libexec/oled-tools/scripts.d/compaction_tracker.d -Duek8 0 1'...
Starting compaction monitor in periodic mode : 1 seconds interval (reports only on activity)
[Time] [TASK] MIso MScn MTkn FFIso FFScn FFTkn FIso FScn FTkn MigOK MigFail
2026 Jul 4 14:01:42
kcompactd0 2 512 52 1 164 64 0 0 0 52 0
sh 1385 405395 121729 19481 178772 96055 1700 391568 32741 120898 369
2026 Jul 4 14:01:44
kcompactd0 397 145830 39746 682 16412 7446 586 236294 32573 39714 32
sh 997 260881 63299 3751 93203 33714 788 234427 32151 62698 569
2026 Jul 4 14:01:46
sh 163 70766 612 22 6043 372 6 111 0 248 364
kcompactd0 754 381440 2952 17 3821 1216 25596 13029600 1965 2952 0
The 14:01:42 sh row shows heavy fast-free isolation activity: 19481 fast free-page isolation attempts scanned 178772 pages and took 96055 pages. The same interval also shows 120898 successful migrations and 369 failed migrations. The kcompactd0 rows show background compaction work running alongside the direct hugetlb request. Because periodic mode cuts activity by wall-clock interval, scanner and migration-result counters can straddle interval boundaries. Use cyclic mode when exact per-pass accounting is needed.
In both cyclic and periodic mode outputs of compaction_tracker.d, a low value of MTkn/MScn indicates low yield of migration scanners i.e. most of the scanned pages could not be isolated for migration. Similarly a low FFTkn/FFScn and low FTkn/FScn indicate low yields of fast-free and free scanners. Since MTkn is number of pages isolated for migration, it should be sum total of MigOK and MigFail. Again a high value of MigFail would indicate that most of pages selected for migration could not be migrated.
bpftrace/dtrace scripts taken together
The above dtrace and bpftrace scripts show some examples of using compaction tracepoints for debugging purposes; one can always modify them or create new scripts based on the debugging need. As long as we are aware of what tracepoints are available and what information they provide, we can always come up with different scripts to debug different manifestations of compaction issues.
Shell scripts: /proc, /sys, and debugfs
As mentioned in the previous blog (Part 2 of this series), the kernel exposes a lot of information pertaining to compaction via proc, sys and debugfs interfaces. This information can also be used to first see whether compaction could be the cause of the issue or not.
For example, the following script dumps fragmentation and unusable indices at regular intervals:
#!/bin/bash
# extfrag_watch.sh
#
# Monitors /sys/kernel/debug/extfrag/extfrag_index and
# /sys/kernel/debug/extfrag/unusable_index continuously.
#
# Usage: ./extfrag_watch.sh [interval] [order]
# interval: polling interval in seconds (default 5)
# order: order to watch (default 9)
#
# License: GPLv2
#
INTERVAL="${1:-5}"
WATCH_ORDER="${2:-9}"
ORDER_COL=$(( WATCH_ORDER + 5 ))
EXTFRAG_THRESHOLD=$(cat /proc/sys/vm/extfrag_threshold 2>/dev/null || echo 500)
EXTFRAG_FILE=/sys/kernel/debug/extfrag/extfrag_index
UNUSABLE_FILE=/sys/kernel/debug/extfrag/unusable_index
if [ ! -r "$EXTFRAG_FILE" ]; then
echo "Cannot read $EXTFRAG_FILE — run as root and ensure debugfs is mounted"
echo " mount -t debugfs none /sys/kernel/debug"
exit 1
fi
echo "Watching fragmentation index for order-${WATCH_ORDER}"
echo "extfrag_threshold = ${EXTFRAG_THRESHOLD} (displayed as $(echo "scale=3; $EXTFRAG_THRESHOLD/1000" | bc))"
echo "Polling every ${INTERVAL}s — Ctrl-C to stop"
echo ""
printf "%-12s %-12s %-12s %-10s %-10s\n" \
"TIME" "NODE" "ZONE" \
"EXTFRAG" "UNUSABLE"
printf "%s\n" "$(printf '─%.0s' {1..70})"
while true; do
ts=$(date +%H:%M:%S)
while IFS= read -r line; do
zone=$(echo "$line" | awk '{print $4}')
node=$(echo "$line" | awk '{print $2}')
extfrag_val=$(echo "$line" | awk -v col="$ORDER_COL" '{print $col}')
unusable_val=$(awk -v node="$node" -v zone="$zone" \
-v col="$ORDER_COL" '
$2==node && $4==zone {print $col}
' "$UNUSABLE_FILE" 2>/dev/null || echo "n/a")
printf "%-12s %-12s %-12s %-10s %-10s\n" \
"$ts" "$node" "$zone" \
"$extfrag_val" "${unusable_val}"
done < <(awk '/Normal/' "$EXTFRAG_FILE")
sleep "$INTERVAL"
done
It produces output, like shown in the following sample:
Watching fragmentation index for order-9
extfrag_threshold = 500 (displayed as .500)
Polling every 1s — Ctrl-C to stop
TIME NODE ZONE EXTFRAG UNUSABLE
──────────────────────────────────────────────────────────────────────
12:09:44 0, Normal -1.000 0.008
12:09:45 0, Normal -1.000 0.008
12:09:46 0, Normal -1.000 0.008
12:09:47 0, Normal -1.000 0.008
In the above out, EXTFRAG stayed at -1.000, meaning an order-9 allocation could succeed without compaction. UNUSABLE stayed around 0.008, so less than one percent of the Normal zone was unsuitable for order-9 allocations during the sample.
One can also use the following script to periodically monitor scanner and migration yield and get a high-level view of whether fragmentation is worsening or not.
#!/bin/bash
# compaction_rate.sh
#
# Measures rate of change of compaction counters over time.
# Useful for monitoring compaction pressure on a running system
# without enabling tracepoints.
#
# Usage: ./compaction_rate.sh [interval_seconds] [count]
# interval: sampling interval in seconds (default 5)
# count: number of samples (default unlimited, 0 = unlimited)
#
# License: GPLv2
#
INTERVAL="${1:-5}"
COUNT="${2:-0}"
sample=0
get_counter() { awk "/^$1 /{print \$2}" /proc/vmstat 2>/dev/null || echo 0; }
print_header() {
printf "%-12s %-8s %-8s %-8s %-8s %-8s %-8s %-8s %-8s\n" \
"TIME" \
"STALL/s" "FAIL/s" "SUCC/s" \
"MIG_S/s" "FREE_S/s" "ISOL/s" \
"KCD_W/s" "SUCC%"
printf "%s\n" "$(printf '─%.0s' {1..96})"
}
declare -A prev
prev[stall]=$(get_counter compact_stall)
prev[fail]=$(get_counter compact_fail)
prev[success]=$(get_counter compact_success)
prev[mig]=$(get_counter compact_migrate_scanned)
prev[free]=$(get_counter compact_free_scanned)
prev[isol]=$(get_counter compact_isolated)
prev[wake]=$(get_counter compact_daemon_wake)
print_header
while true; do
sleep "$INTERVAL"
cur_stall=$(get_counter compact_stall)
cur_fail=$(get_counter compact_fail)
cur_success=$(get_counter compact_success)
cur_mig=$(get_counter compact_migrate_scanned)
cur_free=$(get_counter compact_free_scanned)
cur_isol=$(get_counter compact_isolated)
cur_wake=$(get_counter compact_daemon_wake)
d_stall=$(( (cur_stall - prev[stall]) / INTERVAL ))
d_fail=$(( (cur_fail - prev[fail]) / INTERVAL ))
d_succ=$(( (cur_success - prev[success]) / INTERVAL ))
d_mig=$(( (cur_mig - prev[mig]) / INTERVAL ))
d_free=$(( (cur_free - prev[free]) / INTERVAL ))
d_isol=$(( (cur_isol - prev[isol]) / INTERVAL ))
d_wake=$(( (cur_wake - prev[wake]) / INTERVAL ))
total=$(( d_fail + d_succ ))
succ_pct=0
[ "$total" -gt 0 ] && succ_pct=$(( d_succ * 100 / total ))
printf "%-12s %-8d %-8d %-8d %-8d %-8d %-8d %-8d %-8d\n" \
"$(date +%H:%M:%S)" \
"$d_stall" "$d_fail" "$d_succ" \
"$d_mig" "$d_free" "$d_isol" \
"$d_wake" "$succ_pct"
prev[stall]=$cur_stall
prev[fail]=$cur_fail
prev[success]=$cur_success
prev[mig]=$cur_mig
prev[free]=$cur_free
prev[isol]=$cur_isol
prev[wake]=$cur_wake
sample=$(( sample + 1 ))
[ "$COUNT" -gt 0 ] && [ "$sample" -ge "$COUNT" ] && break
# reprint header every 20 lines
[ $(( sample % 20 )) -eq 0 ] && print_header
done
The output from this script has been shown below:
TIME STALL/s FAIL/s SUCC/s MIG_S/s FREE_S/s ISOL/s KCD_W/s SUCC%
────────────────────────────────────────────────────────────────────────────────────────────────
12:09:49 0 0 0 701951 0 17563 0 0
12:09:50 0 0 0 198144 1014153 48153 0 0
12:09:51 0 0 0 904703 628852 3566 0 0
12:09:52 0 0 0 905727 599386 1868 0 0
The above data was collected while attempting manual compaction which drove the scanner counters, so MIG_S/s, FREE_S/s, and ISOL/s moved sharply. STALL/s, FAIL/s, and SUCC/s remained zero because those counters describe direct compaction attempts, not manual compaction attempts.
Sometimes by the time we get to take a first look at a system, it may already be badly fragmented. This is more likely for systems where proactive compaction is absent (kernels older than v5.9) or disabled. On such systems it may be worthwhile to trigger manual compaction and see if it helps. The following script does just that. It must be noted that manual compaction will comb through entire zones, irrespective of whether large continuous blocks become available or not, so it has enough overhead and must be tried mostly as a last resort to manually force compaction and reduce fragmentation.
#!/bin/bash
# compaction_effectiveness.sh
#
# Tests whether manual compaction can improve the free page
# distribution for a given order. Useful for determining whether
# fragmentation is structural (compaction cannot help) or
# transient (compaction can resolve it).
#
# Usage: ./compaction_effectiveness.sh [order] [node]
# order: target order to check (default 9)
# node: NUMA node to compact (default all)
#
# License: GPLv2
#
set -euo pipefail
TARGET_ORDER="${1:-9}"
TARGET_NODE="${2:-all}"
PAGE_SIZE=$(getconf PAGESIZE)
if [ "${EUID}" -ne 0 ]; then
echo "This script must be run as root." >&2
exit 1
fi
if [ ! -r /proc/buddyinfo ]; then
echo "Cannot read /proc/buddyinfo." >&2
exit 1
fi
# /proc/buddyinfo has four label fields followed by order-0..MAX_ORDER counts.
MAX_ORDER=$(awk '/ zone[[:space:]]+Normal / { print NF - 5; exit }' \
/proc/buddyinfo)
if [ -z "$MAX_ORDER" ]; then
echo "No Normal zone found in /proc/buddyinfo." >&2
exit 1
fi
if ! [[ "$TARGET_ORDER" =~ ^[0-9]+$ ]] || \
[ "$TARGET_ORDER" -gt "$MAX_ORDER" ]; then
echo "Order must be an integer between 0 and ${MAX_ORDER}." >&2
exit 2
fi
if [ "$TARGET_NODE" != "all" ]; then
if ! [[ "$TARGET_NODE" =~ ^[0-9]+$ ]] || \
[ ! -d "/sys/devices/system/node/node${TARGET_NODE}" ]; then
echo "Node must be 'all' or an existing NUMA node number." >&2
exit 2
fi
fi
BLOCK_PAGES=$(( 1 << TARGET_ORDER ))
BLOCK_KB=$(( BLOCK_PAGES * PAGE_SIZE / 1024 ))
# Count blocks at TARGET_ORDER plus the equivalent number obtainable by
# splitting higher-order blocks. Restrict the calculation to TARGET_NODE when
# a node was requested.
get_available_blocks() {
awk -v order="$TARGET_ORDER" -v node="$TARGET_NODE" '
$1 == "Node" && $3 == "zone" && $4 == "Normal" &&
(node == "all" || $2 == node ",") {
for (field = 5 + order; field <= NF; field++)
total += $field * (2 ^ (field - 5 - order))
}
END { print total + 0 }
' /proc/buddyinfo
}
show_buddyinfo() {
if [ "$TARGET_NODE" = "all" ]; then
sed 's/^/ /' /proc/buddyinfo
else
awk -v node="$TARGET_NODE," '$1 == "Node" && $2 == node' \
/proc/buddyinfo | sed 's/^/ /'
fi
}
echo "=== Compaction Effectiveness Test ==="
echo "Target order: ${TARGET_ORDER} (${BLOCK_PAGES} pages = ${BLOCK_KB}kB)"
echo "Target node: ${TARGET_NODE}"
echo ""
echo "--- Before compaction ---"
before_free=$(get_available_blocks)
echo "order-${TARGET_ORDER} available blocks/equivalents (Normal): ${before_free}"
echo ""
echo "buddyinfo:"
show_buddyinfo
echo ""
echo "--- Triggering compaction ---"
start_ts=$(date +%s%N)
if [ "$TARGET_NODE" = "all" ]; then
if [ ! -w /proc/sys/vm/compact_memory ]; then
echo "Cannot write /proc/sys/vm/compact_memory." >&2
exit 1
fi
echo "Writing to /proc/sys/vm/compact_memory ..."
echo 1 > /proc/sys/vm/compact_memory
else
node_path="/sys/devices/system/node/node${TARGET_NODE}/compact"
if [ ! -w "$node_path" ]; then
echo "Cannot write ${node_path}." >&2
exit 1
fi
echo "Writing to ${node_path} ..."
echo 1 > "$node_path"
fi
end_ts=$(date +%s%N)
elapsed_ms=$(( (end_ts - start_ts) / 1000000 ))
echo "Compaction completed in ${elapsed_ms}ms"
echo ""
echo "--- After compaction ---"
after_free=$(get_available_blocks)
echo "order-${TARGET_ORDER} available blocks/equivalents (Normal): ${after_free}"
echo ""
echo "buddyinfo:"
show_buddyinfo
echo ""
echo "--- Analysis ---"
delta_free=$(( after_free - before_free ))
echo "order-${TARGET_ORDER} available block change: ${before_free} → ${after_free} (delta: ${delta_free})"
echo ""
if [ "$delta_free" -gt 0 ]; then
echo "RESULT: Compaction IMPROVED the free page distribution at order-${TARGET_ORDER}."
echo " If fragmentation returns quickly, kcompactd is not keeping up."
echo " Consider checking workload or increasing vm.compaction_proactiveness."
elif [ "$delta_free" -eq 0 ] && [ "$before_free" -gt 0 ]; then
echo "RESULT: Compaction had no effect but order-${TARGET_ORDER} blocks already exist."
echo " The allocation issue may not be fragmentation."
else
echo "RESULT: Compaction could NOT improve the free page distribution at order-${TARGET_ORDER}."
echo " Likely cause: non-movable pages colonising movable pageblocks."
echo " Check /proc/pagetypeinfo Unmovable vs Movable free page counts."
echo " Check /proc/meminfo SUnreclaimable — high value = kernel slab probable cause."
fi
A sample output from this script is given below:
=== Compaction Effectiveness Test ===
Target order: 9 (512 pages = 2048kB)
Target node: all
--- Before compaction ---
order-9 available blocks/equivalents (Normal): 27529
buddyinfo:
Node 0, zone DMA 0 1 1 1 0 0 0 0 2 2 1
Node 0, zone DMA32 4 8 5 4 6 6 5 3 2 5 348
Node 0, zone Normal 4051 11093 3288 1986 676 431 252 73 41 215 13657
--- Triggering compaction ---
Writing to /proc/sys/vm/compact_memory ...
Compaction completed in 13ms
--- After compaction ---
order-9 available blocks/equivalents (Normal): 27532
buddyinfo:
Node 0, zone DMA 0 1 1 1 0 0 0 0 2 2 1
Node 0, zone DMA32 4 8 5 4 6 6 5 3 2 5 348
Node 0, zone Normal 5483 10952 3276 1962 670 429 251 71 40 216 13658
--- Analysis ---
order-9 available block change: 27529 → 27532 (delta: 3)
RESULT: Compaction IMPROVED the free page distribution at order-9.
If fragmentation returns quickly, kcompactd is not keeping up.
Consider checking workload or increasing vm.compaction_proactiveness.
Here the system was already healthy before compaction, with more than 27000 order-9-equivalent Normal-zone blocks. Manual compaction still improved the count slightly, from 27529 to 27532, which shows that the script can detect small changes as well as severe fragmentation cases.
The script compacts all nodes by default but can be given a node number as an argument. When a node is selected, both the compaction request and the /proc/buddyinfo calculations are restricted to that node. Based on the number of blocks available at the specified order (default 9), including higher-order blocks that can be split to satisfy it, the script treats its attempt as a success or failure.
Conclusion
This blog demonstrated different approaches to diagnosing Linux memory compaction problems using bpftrace, dtrace and shell scripts. These scripts were built on tracepoints and other interfaces described in the previous two parts of this series.
The shell scripts indicate whether compaction is a problem, how fast it is happening, and whether it can actually help. They require no tracing infrastructure and can be run on production systems at any time.
The bpftrace and dtrace scripts provide the per-event granularity needed to understand exactly what is happening inside the compaction pass — which scanner is struggling, why migration is failing, which processes are driving direct compaction, and whether kcompactdN is keeping up with demand.
Used together, the two layers of observability described across this series — from the internals in Part 1, through the data catalogue in Part 2, to the practical tools demonstrated here — give a complete toolkit for understanding and resolving Linux memory compaction problems on real systems.