Improve WSL2 guest memory reclaim (#41096)
* Improve WSL2 guest memory reclaim behavior * Use rolling CPU window for memory reclaim Copilot-Session: ccc721ed-5fa2-41b2-a5f9-5a8502cdf364 * Harden memory reclaim sampling Copilot-Session: ccc721ed-5fa2-41b2-a5f9-5a8502cdf364 --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Copilot-Session: ccc721ed-5fa2-41b2-a5f9-5a8502cdf364
Ben Hillis committed
Jul 24, 2026 at 15:20 UTC
49f841cc5e759079a860e090a752ab060a5795d0
1 file changed
+352
-118
src/linux/init/util.cpp
+352
-118
@@ -3531,89 +3531,270 @@ int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span
3531
3532
#define RECLAIM_PATH CGROUP_MOUNTPOINT "/memory.reclaim"
3533
3534
-static long long int GetUserCpuTime()
3534
+namespace {
3535
+
3536
+class CpuIdleTracker
3537
+{
3538
+public:
3539
+ struct State
3540
+ {
3541
+ bool IntervalIdle;
3542
+ bool WindowIdle;
3543
+ };
3544
+
3545
+ State AddSample(unsigned long long Busy, unsigned long long Total)
3546
+ {
3547
+ m_windowBusy -= m_busyWindow[m_windowIndex];
3548
+ m_windowTotal -= m_totalWindow[m_windowIndex];
3549
+ m_busyWindow[m_windowIndex] = Busy;
3550
+ m_totalWindow[m_windowIndex] = Total;
3551
+ m_windowBusy += Busy;
3552
+ m_windowTotal += Total;
3553
+ m_windowIndex = (m_windowIndex + 1) % c_windowIntervals;
3554
+ if (m_windowSamples < c_windowIntervals)
3555
+ {
3556
+ m_windowSamples += 1;
3557
+ }
3558
+
3559
+ return {
3560
+ .IntervalIdle = IsIdle(Busy, Total),
3561
+ .WindowIdle = m_windowSamples == c_windowIntervals && IsIdle(m_windowBusy, m_windowTotal),
3562
+ };
3563
+ }
3564
+
3565
+ void Reset()
3566
+ {
3567
+ m_busyWindow.fill(0);
3568
+ m_totalWindow.fill(0);
3569
+ m_windowBusy = 0;
3570
+ m_windowTotal = 0;
3571
+ m_windowIndex = 0;
3572
+ m_windowSamples = 0;
3573
+ }
3574
+
3575
+private:
3576
+ static bool IsIdle(unsigned long long Busy, unsigned long long Total)
3577
+ {
3578
+ return Total == 0 || Busy * 1000 <= Total * c_busyThresholdPerMille;
3579
+ }
3580
+
3581
+ static constexpr size_t c_windowIntervals = 12; // 2 minutes
3582
+ static constexpr unsigned long long c_busyThresholdPerMille = 5; // 0.5%
3583
+
3584
+ std::array<unsigned long long, c_windowIntervals> m_busyWindow{};
3585
+ std::array<unsigned long long, c_windowIntervals> m_totalWindow{};
3586
+ unsigned long long m_windowBusy = 0;
3587
+ unsigned long long m_windowTotal = 0;
3588
+ size_t m_windowIndex = 0;
3589
+ size_t m_windowSamples = 0;
3590
+};
3591
+
3592
+} // namespace
3593
+
3594
+static bool ReadCpuBusyIdle(unsigned long long& Busy, unsigned long long& Idle)
3595
3596
/*++
3597
3598
Routine Description:
3599
3540
- This routine parses /proc/stat to query a summary of all user CPU time.
3600
+ This routine parses the aggregate "cpu" line of /proc/stat and splits the cumulative jiffies into
3601
+ busy and idle buckets. Idle time is idle + iowait; everything else (user, nice, system, irq,
3602
+ softirq, steal) counts as busy, so kernel-bound work keeps the VM out of the idle state rather than
3603
+ looking at user time alone.
3604
3605
Arguments:
3606
3544
- None.
3607
+ Busy - Receives the cumulative busy jiffies across all cores.
3608
+
3609
+ Idle - Receives the cumulative idle jiffies (idle + iowait) across all cores.
3610
3611
Return Value:
3612
3548
- The current user CPU counter for all cores.
3613
+ true on success, false on failure.
3614
3615
--*/
3616
3617
{
3553
- wil::unique_fd Fd{open("/proc/stat", O_RDONLY)};
3554
- if (!Fd)
3618
+ wil::unique_fd fd{TEMP_FAILURE_RETRY(open("/proc/stat", O_RDONLY | O_CLOEXEC))};
3619
+ if (!fd)
3620
{
3556
- LOG_ERROR("open failed {}", errno);
3557
- return -1;
3621
+ LOG_ERROR("open(/proc/stat) failed {}", errno);
3622
+ return false;
3623
}
3624
3560
- char Buffer[32];
3561
- int Result = TEMP_FAILURE_RETRY(read(Fd.get(), Buffer, (sizeof(Buffer) - 1)));
3562
- if (Result <= 0)
3625
+ char buffer[256];
3626
+ const ssize_t result = TEMP_FAILURE_RETRY(read(fd.get(), buffer, sizeof(buffer) - 1));
3627
+ if (result <= 0)
3628
{
3564
- LOG_ERROR("read failed {}", errno);
3565
- return -1;
3629
+ LOG_ERROR("read(/proc/stat) failed {}", errno);
3630
+ return false;
3631
}
3632
3633
+ buffer[result] = '\0';
3634
+
3635
//
3569
- // Parse the first line of /proc/stat which is in the format
3570
- // "cpu <counter>".
3636
+ // Format: "cpu user nice system idle iowait irq softirq steal ...". Fields after steal are ignored.
3637
//
3638
3573
- Buffer[Result] = '\0';
3574
- char* Sp1;
3575
- char* Info = strtok_r(Buffer, " \n", &Sp1);
3576
- if (Info == nullptr)
3639
+ if (strncmp(buffer, "cpu ", 4) != 0)
3640
{
3641
LOG_ERROR("/proc/stat first line missing cpu label");
3642
+ return false;
3643
+ }
3644
+
3645
+ unsigned long long fields[8] = {};
3646
+ const char* cursor = buffer + 3;
3647
+ int parsed = 0;
3648
+ for (; parsed < static_cast<int>(COUNT_OF(fields)); parsed += 1)
3649
+ {
3650
+ char* end = nullptr;
3651
+ const unsigned long long value = strtoull(cursor, &end, 10);
3652
+ if (end == cursor)
3653
+ {
3654
+ break;
3655
+ }
3656
+
3657
+ fields[parsed] = value;
3658
+ cursor = end;
3659
+ }
3660
+
3661
+ if (parsed < 5)
3662
+ {
3663
+ LOG_ERROR("failed to parse /proc/stat cpu line (parsed {})", parsed);
3664
+ return false;
3665
+ }
3666
+
3667
+ Idle = fields[3] + fields[4];
3668
+ Busy = 0;
3669
+ for (int index = 0; index < parsed; index += 1)
3670
+ {
3671
+ if (index != 3 && index != 4)
3672
+ {
3673
+ Busy += fields[index];
3674
+ }
3675
+ }
3676
+
3677
+ return true;
3678
+}
3679
+
3680
+static long long GetReclaimableCacheBytes()
3681
+
3682
+/*++
3683
+
3684
+Routine Description:
3685
+
3686
+ This routine returns the amount of reclaimable file-backed page cache (in bytes) by parsing
3687
+ /proc/meminfo. It counts only memory that cache reclaim can actually return to the host:
3688
+ Active(file) + Inactive(file) + SReclaimable. Anonymous memory is excluded because reclaim of clean
3689
+ cache cannot free it.
3690
+
3691
+Arguments:
3692
+
3693
+ None.
3694
+
3695
+Return Value:
3696
+
3697
+ Reclaimable cache in bytes, or -1 on failure.
3698
+
3699
+--*/
3700
+
3701
+{
3702
+ std::ifstream memInfo("/proc/meminfo");
3703
+ if (!memInfo)
3704
+ {
3705
+ LOG_ERROR("failed to open /proc/meminfo");
3706
+ return -1;
3707
+ }
3708
+
3709
+ // /proc/meminfo values are in kB.
3710
+ long long activeFileKb = 0;
3711
+ long long inactiveFileKb = 0;
3712
+ long long reclaimableSlabKb = 0;
3713
+ bool foundActiveFile = false;
3714
+ bool foundInactiveFile = false;
3715
+ bool foundReclaimableSlab = false;
3716
+ std::string line;
3717
+ while (std::getline(memInfo, line))
3718
+ {
3719
+ std::istringstream stream(line);
3720
+ std::string name;
3721
+ long long value = 0;
3722
+ if (!(stream >> name >> value))
3723
+ {
3724
+ continue;
3725
+ }
3726
+
3727
+ if (name == "Active(file):")
3728
+ {
3729
+ activeFileKb = value;
3730
+ foundActiveFile = true;
3731
+ }
3732
+ else if (name == "Inactive(file):")
3733
+ {
3734
+ inactiveFileKb = value;
3735
+ foundInactiveFile = true;
3736
+ }
3737
+ else if (name == "SReclaimable:")
3738
+ {
3739
+ reclaimableSlabKb = value;
3740
+ foundReclaimableSlab = true;
3741
+ }
3742
+ }
3743
+
3744
+ if (memInfo.bad())
3745
+ {
3746
+ LOG_ERROR("failed to read /proc/meminfo");
3747
return -1;
3748
}
3749
3582
- Info = strtok_r(nullptr, " \n", &Sp1);
3583
- if (Info == nullptr)
3750
+ if (!foundActiveFile || !foundInactiveFile || !foundReclaimableSlab)
3751
{
3585
- LOG_ERROR("/proc/stat first line missing cpu counter");
3752
+ LOG_ERROR("failed to find reclaimable cache counters in /proc/meminfo");
3753
return -1;
3754
}
3755
3589
- return strtoll(Info, nullptr, 10);
3756
+ return (activeFileKb + inactiveFileKb + reclaimableSlabKb) * 1024;
3757
}
3758
3592
-static ssize_t GetMemoryInUse()
3759
+static bool RequestReclaim(long long Bytes)
3760
3761
/*++
3762
3763
Routine Description:
3764
3598
- This routine returns the amount memory in use in bytes.
3765
+ Best-effort write of a byte count to the cgroup memory.reclaim knob. EAGAIN is an expected outcome
3766
+ (the kernel freed some, but not all, of the requested pages) and is treated as success without
3767
+ logging, so the long-lived reduction thread does not error out every interval. A transient failure
3768
+ never throws.
3769
3770
Arguments:
3771
3602
- None.
3772
+ Bytes - Supplies the number of bytes to request the kernel reclaim.
3773
3774
Return Value:
3775
3606
- Total memory - Free memory. Includes that used by cache and buffers.
3776
+ true if pages were reclaimed (full success or EAGAIN), false otherwise.
3777
3778
--*/
3779
3610
-try
3780
{
3612
- struct sysinfo Info = {};
3613
- THROW_LAST_ERROR_IF(sysinfo(&Info) < 0);
3614
- return (Info.totalram - Info.freeram) * Info.mem_unit;
3781
+ wil::unique_fd fd{TEMP_FAILURE_RETRY(open(RECLAIM_PATH, O_WRONLY | O_CLOEXEC))};
3782
+ if (!fd)
3783
+ {
3784
+ LOG_ERROR("open({}) failed {}", RECLAIM_PATH, errno);
3785
+ return false;
3786
+ }
3787
+
3788
+ const std::string request = std::to_string(Bytes) + " swappiness=0";
3789
+ const ssize_t result = UtilWriteStringView(fd.get(), request);
3790
+ if (result == static_cast<ssize_t>(request.size()) || (result < 0 && errno == EAGAIN))
3791
+ {
3792
+ return true;
3793
+ }
3794
+
3795
+ LOG_ERROR("write({}, {}) failed {}", RECLAIM_PATH, request, errno);
3796
+ return false;
3797
}
3616
-CATCH_RETURN_ERRNO()
3798
3799
void StartMemoryReductionThread(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode)
3800
@@ -3621,8 +3802,13 @@ void StartMemoryReductionThread(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode)
3802
3803
Routine Description:
3804
3624
- This routine starts a background thread that performs memory compaction and optional cache/memory
3625
- reclaim when the VM is idle. This ensures that the maximum number of pages can be discarded to the host.
3805
+ This routine starts a background thread that reclaims cold page cache and compacts free pages while
3806
+ the VM is idle, so the maximum number of pages can be discarded back to the host.
3807
+
3808
+ Reclaim is gated on CPU idle using a rolling window of all non-idle CPU time, not just user time.
3809
+ Gradual mode reclaims cold file-backed page cache above a small floor via the cgroup memory.reclaim
3810
+ knob, falling back to drop_caches when the knob is unavailable. DropCache mode uses drop_caches
3811
+ directly. Freed pages are compacted so free-page reporting can hand back large blocks.
3812
3813
Arguments:
3814
@@ -3636,129 +3822,177 @@ Return Value:
3822
3823
try
3824
{
3639
- std::thread([Mode = Mode]() mutable {
3825
+ if (Mode == LxMiniInitMemoryReclaimModeDisabled)
3826
+ {
3827
+ return;
3828
+ }
3829
+
3830
+ std::thread([Mode]() {
3831
try
3832
{
3833
//
3643
- // Set the thread's scheduling policy to idle.
3834
+ // Run at idle scheduling priority so reclaim never competes with real work.
3835
//
3836
3646
- sched_param Parameter{};
3647
- Parameter.sched_priority = 0;
3648
- const int Result = pthread_setschedparam(pthread_self(), SCHED_IDLE, &Parameter);
3649
- THROW_ERRNO_IF(Result, Result != 0);
3837
+ sched_param parameter{};
3838
+ parameter.sched_priority = 0;
3839
+ const int result = pthread_setschedparam(pthread_self(), SCHED_IDLE, ¶meter);
3840
+ THROW_ERRNO_IF(result, result != 0);
3841
3842
//
3652
- // Periodically check if the machine is idle by querying procfs for CPU usage.
3653
- // Memory compaction will occur if both of the following conditions are true:
3654
- // 1. The CPU time since the last check is greater than the idle threshold.
3655
- // 2. The current CPU usage is below the idle threshold. This is measured by taking two readings one second apart.
3843
+ // Gradual mode uses cgroup memory.reclaim and falls back to drop_caches when unavailable.
3844
//
3845
3658
- double MemoryLow = 1024 * 1024 * 1024;
3659
- double MemoryHigh = 1.1 * 1024.0 * 1024.0 * 1024.0;
3660
- const int IdleThreshold = get_nprocs(); // Change math to adjust if sysconf(_SC_CLK_TCK) != 100? Is 1%
3661
- long long int Start, Stop = 0;
3662
- auto constexpr SleepDuration = std::chrono::seconds(30);
3663
- size_t ReclaimIndex = 0;
3664
- long long int const ReclaimThreshold = (get_nprocs() * sysconf(_SC_CLK_TCK) * SleepDuration / std::chrono::seconds(1)) / 200; // 0.5%
3665
- long long int ReclaimWindow[20] = {}; // 10 minutes
3666
- long long int ReclaimWindowLength = COUNT_OF(ReclaimWindow);
3667
- bool ReclaimIdling = false;
3668
-
3669
- //
3670
- // Fall back to drop cache if the required cgroup path is not present.
3671
- //
3672
-
3673
- if (Mode == LxMiniInitMemoryReclaimModeGradual && access(RECLAIM_PATH, W_OK) < 0)
3846
+ bool useReclaim = Mode != LxMiniInitMemoryReclaimModeDropCache;
3847
+ if (useReclaim && access(RECLAIM_PATH, W_OK) < 0)
3848
{
3849
LOG_WARNING("access({}, W_OK) failed {}, falling back to drop_caches", RECLAIM_PATH, errno);
3676
- Mode = LxMiniInitMemoryReclaimModeDropCache;
3850
+ useReclaim = false;
3851
}
3852
3679
- if (Mode == LxMiniInitMemoryReclaimModeGradual)
3853
+ constexpr auto c_pollInterval = std::chrono::seconds(10);
3854
+
3855
+ // Reclaimable cache below this floor is always retained to protect a minimal working set.
3856
+ constexpr long long c_floorBytes = 128ll * 1024 * 1024;
3857
+
3858
+ // Scale reclaim requests with the VM size while keeping individual operations bounded.
3859
+ constexpr long long c_minReclaimBytes = 256ll * 1024 * 1024;
3860
+ constexpr long long c_maxReclaimBytes = 1024ll * 1024 * 1024;
3861
+
3862
+ struct sysinfo info = {};
3863
+ THROW_LAST_ERROR_IF(sysinfo(&info) < 0);
3864
+
3865
+ long long reclaimStepBytes = (static_cast<long long>(info.totalram) * info.mem_unit) / 32;
3866
+ if (reclaimStepBytes < c_minReclaimBytes)
3867
{
3681
- static_assert(COUNT_OF(ReclaimWindow) >= 6);
3682
- ReclaimWindowLength = 6; // Set to 3 minutes.
3868
+ reclaimStepBytes = c_minReclaimBytes;
3869
}
3684
-
3685
- for (auto i = 1; i < ReclaimWindowLength; i++)
3870
+ else if (reclaimStepBytes > c_maxReclaimBytes)
3871
{
3687
- ReclaimWindow[i] = LLONG_MIN;
3872
+ reclaimStepBytes = c_maxReclaimBytes;
3873
}
3874
3690
- std::this_thread::sleep_for(SleepDuration);
3875
+ unsigned long long previousBusy = 0;
3876
+ unsigned long long previousIdle = 0;
3877
+ bool havePreviousSample = false;
3878
+
3879
+ CpuIdleTracker idleTracker;
3880
+
3881
+ bool droppedThisIdlePeriod = false;
3882
+ bool compactedThisIdlePeriod = false;
3883
+
3884
for (;;)
3885
{
3693
- auto const Target = std::chrono::steady_clock::now() + SleepDuration;
3694
- Start = GetUserCpuTime();
3695
- THROW_LAST_ERROR_IF(Start == -1);
3886
+ std::this_thread::sleep_for(c_pollInterval);
3887
3697
- if (Mode != LxMiniInitMemoryReclaimModeDisabled)
3888
+ unsigned long long busy = 0;
3889
+ unsigned long long idle = 0;
3890
+ if (!ReadCpuBusyIdle(busy, idle))
3891
{
3699
- //
3700
- // Ensure that utilization is below 0.5% from the last 30 seconds, and last n minutes, of usage.
3701
- //
3892
+ continue;
3893
+ }
3894
+
3895
+ if (!havePreviousSample)
3896
+ {
3897
+ previousBusy = busy;
3898
+ previousIdle = idle;
3899
+ havePreviousSample = true;
3900
+ continue;
3901
+ }
3902
+
3903
+ //
3904
+ // Guard against non-monotonic counters (should not happen, but resample if it does).
3905
+ //
3906
+
3907
+ if (busy < previousBusy || idle < previousIdle)
3908
+ {
3909
+ previousBusy = busy;
3910
+ previousIdle = idle;
3911
+ idleTracker.Reset();
3912
+ droppedThisIdlePeriod = false;
3913
+ compactedThisIdlePeriod = false;
3914
+ continue;
3915
+ }
3916
+
3917
+ const unsigned long long busyDelta = busy - previousBusy;
3918
+ const unsigned long long totalDelta = busyDelta + (idle - previousIdle);
3919
+ previousBusy = busy;
3920
+ previousIdle = idle;
3921
+
3922
+ const auto idleState = idleTracker.AddSample(busyDelta, totalDelta);
3923
+ if (!idleState.WindowIdle)
3924
+ {
3925
+ droppedThisIdlePeriod = false;
3926
+ compactedThisIdlePeriod = false;
3927
+ continue;
3928
+ }
3929
+
3930
+ //
3931
+ // A short burst blocks this tick but does not discard the preceding idle history.
3932
+ //
3933
+
3934
+ if (!idleState.IntervalIdle)
3935
+ {
3936
+ continue;
3937
+ }
3938
3703
- size_t const LastIndex = (ReclaimIndex + 1) % ReclaimWindowLength;
3704
- if ((ReclaimWindow[LastIndex] > Start - ReclaimThreshold * (ReclaimWindowLength + 1)) &&
3705
- (ReclaimWindow[ReclaimIndex] > Start - ReclaimThreshold))
3939
+ //
3940
+ // The VM is idle: reclaim cold cache and compact.
3941
+ //
3942
+
3943
+ bool reclaimed = false;
3944
+ if (useReclaim)
3945
+ {
3946
+ const long long cache = GetReclaimableCacheBytes();
3947
+ if (cache > c_floorBytes)
3948
{
3707
- if (Mode == LxMiniInitMemoryReclaimModeGradual)
3949
+ long long bytes = cache - c_floorBytes;
3950
+ if (bytes > reclaimStepBytes)
3951
{
3709
- double MemorySize = GetMemoryInUse();
3710
- THROW_LAST_ERROR_IF(MemorySize < 0);
3711
-
3712
- if (MemorySize > MemoryHigh)
3713
- {
3714
- ReclaimIdling = false;
3715
- }
3716
-
3717
- if (!ReclaimIdling && MemorySize > MemoryLow)
3718
- {
3719
- double MemoryTargetSize = MemorySize * 0.97;
3720
- std::string MemoryToFree = std::to_string(size_t(MemorySize - MemoryTargetSize));
3721
- // EAGAIN Means that it attempted, but was unable to evict sufficient pages.
3722
- THROW_LAST_ERROR_IF(WriteToFile(RECLAIM_PATH, MemoryToFree.c_str()) < 0 && errno != EAGAIN);
3723
-
3724
- if (MemoryTargetSize < MemoryLow)
3725
- {
3726
- ReclaimIdling = true;
3727
- }
3728
- }
3729
- }
3730
- else if (!ReclaimIdling)
3731
- {
3732
- ReclaimIdling = true;
3733
- THROW_LAST_ERROR_IF(WriteToFile("/proc/sys/vm/drop_caches", "1\n") < 0);
3952
+ bytes = reclaimStepBytes;
3953
}
3954
+
3955
+ reclaimed = RequestReclaim(bytes);
3956
}
3736
- else
3957
+ }
3958
+ else if (!droppedThisIdlePeriod)
3959
+ {
3960
+ if (WriteToFile("/proc/sys/vm/drop_caches", "1\n") == 0)
3961
{
3738
- ReclaimIdling = false;
3962
+ droppedThisIdlePeriod = true;
3963
+ reclaimed = true;
3964
}
3740
-
3741
- ReclaimIndex = LastIndex;
3742
- ReclaimWindow[ReclaimIndex] = Start;
3965
}
3966
3967
//
3746
- // Perform memory compaction if the VM is idle.
3747
- // This coalesces free pages into larger blocks for more efficient page reporting.
3968
+ // Coalesce freed pages into larger blocks for efficient page reporting.
3969
//
3970
3750
- if ((Start - Stop) > IdleThreshold)
3971
+ bool memoryOperation = reclaimed;
3972
+ if (!compactedThisIdlePeriod || reclaimed)
3973
{
3752
- std::this_thread::sleep_for(std::chrono::seconds(1));
3753
- Stop = GetUserCpuTime();
3754
- THROW_LAST_ERROR_IF(Stop == -1);
3755
- if ((Stop - Start) < IdleThreshold)
3974
+ if (WriteToFile("/proc/sys/vm/compact_memory", "1\n") == 0)
3975
{
3757
- THROW_LAST_ERROR_IF(WriteToFile("/proc/sys/vm/compact_memory", "1\n") < 0);
3976
+ compactedThisIdlePeriod = true;
3977
+ memoryOperation = true;
3978
}
3979
}
3980
3761
- std::this_thread::sleep_until(Target);
3981
+ //
3982
+ // Exclude the reclaim/compaction work from the next utilization interval so it does not
3983
+ // restart the grace period itself.
3984
+ //
3985
+
3986
+ if (memoryOperation)
3987
+ {
3988
+ if (!ReadCpuBusyIdle(previousBusy, previousIdle))
3989
+ {
3990
+ havePreviousSample = false;
3991
+ idleTracker.Reset();
3992
+ droppedThisIdlePeriod = false;
3993
+ compactedThisIdlePeriod = false;
3994
+ }
3995
+ }
3996
}
3997
}
3998
CATCH_LOG()