@samitouri / QOSAMI-WSL / commits / 49f841cc

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, &parameter);
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()