Share memory reduction logic and enable compaction for WSLC (#40376)

* Share memory reduction logic between WSL2 and WSLC Extract the memory compaction and cache reclaim thread into StartMemoryReductionThread() in util.cpp. WSLC now runs the same idle-based compaction and drop_caches logic that WSL2 uses, improving page reporting efficiency and reducing host memory pressure. - Add StartMemoryReductionThread() and MemoryReductionMode to util.h/cpp - WSL2 main.cpp delegates to shared implementation - WSLC WSLCInit.cpp calls StartMemoryReductionThread(DropCache) - Remove duplicated GetUserCpuTime/GetMemoryInUse from main.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove MemoryReductionMode enum, use LX_MINI_INIT_MEMORY_RECLAIM_MODE directly Address PR review feedback: - Remove redundant MemoryReductionMode enum and use the existing wire enum LX_MINI_INIT_MEMORY_RECLAIM_MODE directly, eliminating the unsafe static_cast that could have swapped Gradual/DropCache behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Start memory reduction thread after WSLC chroot ProcessMessages may MS_MOVE /proc into the chroot target before invoking chroot(); starting the thread before that window risked failing path lookups under /proc. Defer the thread start until the first successful chroot, guarded by a once_flag so subsequent mount messages don't re-start. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback on memory reduction helpers * GetUserCpuTime: harden /proc/stat parsing. read() returning 0 left Buffer empty (Result <= 0 was treated as success), and either strtok_r call could return nullptr on a short/malformed first line, causing strtoll(nullptr, ...) to crash. Now check both Result > 0 and that each token is non-null, returning -1 on any anomaly. * RECLAIM_PATH: build from CGROUP_MOUNTPOINT (already defined in util.h) so the cgroup root is defined in one place. Drop the dead duplicate #define from main.cpp now that the only consumer lives in util.cpp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Pooja Trivedi <poojatrivedi@gmail.com>

Ben Hillis committed May 21, 2026 at 09:18 UTC 98b175b5d9ea549d95173b799442c6589c54804d
4 files changed +252 -234
src/linux/init/WSLCInit.cpp
+5
@@ -30,6 +30,7 @@ Abstract:
30 #include <arpa/inet.h>
31
32 #include <pty.h>
33 +#include <mutex>
34 #include "mountutilcpp.h"
35 #include <filesystem>
36
@@ -621,6 +622,10 @@ void HandleMessageImpl(
622
623 // Recreate the crash dump symlink inside the new root.
624 CreateCaptureCrashSymlink();
625 +
626 + // Start the memory reduction thread now that procfs is in its final location.
627 + static std::once_flag memoryReductionFlag;
628 + std::call_once(memoryReductionFlag, [] { StartMemoryReductionThread(LxMiniInitMemoryReclaimModeDropCache); });
629 }
630
631 response.Result = 0;
src/linux/init/main.cpp
+1 -233
@@ -89,7 +89,6 @@ Abstract:
89 #define PROCFS_PATH "/proc"
90 #define RESOLV_CONF_FILE "resolv.conf"
91 #define RESOLV_CONF_PATH ETC_PATH "/" RESOLV_CONF_FILE
92 -#define RECLAIM_PATH "/sys/fs/cgroup/memory.reclaim"
92 #define SCSI_DEVICE_PATH "/sys/bus/scsi/devices"
93 #define SCSI_DEVICE_NAME_PREFIX "0:0:0:"
94 #define SCSI_DEVICE_PREFIX SCSI_DEVICE_PATH "/" SCSI_DEVICE_NAME_PREFIX
@@ -122,8 +121,6 @@ std::optional<bool> g_EnableSocketLogging;
121
122 int Chroot(const char* Target);
123
125 -void ConfigureMemoryReduction(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode);
126 -
124 void CreateSwap(unsigned int Lun);
125
126 int CreateTempDirectory(const char* ParentPath, std::string& Path);
@@ -146,10 +143,6 @@ int GetDiskPartitionIndex(const char* DiskPath, const char* PartitionName);
143
144 std::string GetMountTarget(const char* Name);
145
149 -long long int GetUserCpuTime(void);
150 -
151 -ssize_t GetMemoryInUse(void);
152 -
146 int ImportFromSocket(const char* Destination, int Socket, int ErrorSocket, unsigned int Flags);
147
148 int Initialize(const char* Hostname);
@@ -265,160 +258,6 @@ Return Value:
258 return 0;
259 }
260
268 -void ConfigureMemoryReduction(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode)
269 -
270 -/*++
271 -
272 -Routine Description:
273 -
274 - This routine configures memory reduction behavior including memory reclaim and compaction.
275 -
276 -Arguments:
277 -
278 - Mode - Supplies the memory reclaim mode.
279 -
280 -Return Value:
281 -
282 - None.
283 -
284 ---*/
285 -
286 -try
287 -{
288 - //
289 - // Create a worker thread to periodically check if the VM is idle and performs memory compaction
290 - // and memory reclaim. This ensures that the maximum number of pages can be discarded to the host.
291 - //
292 -
293 - std::thread([Mode]() mutable {
294 - try
295 - {
296 - //
297 - // Set the thread's scheduling policy to idle.
298 - //
299 -
300 - sched_param Parameter{};
301 - Parameter.sched_priority = 0;
302 - THROW_LAST_ERROR_IF(pthread_setschedparam(pthread_self(), SCHED_IDLE, &Parameter) != 0);
303 -
304 - //
305 - // Periodically check if the machine is idle by querying procfs for CPU usage.
306 - // Memory compaction will occur if both of the following conditions are true:
307 - // 1. The CPU time since the last check is greater than the idle threshold.
308 - // 2. The current CPU usage is below the idle threshold. This is measured by taking two readings one second apart.
309 - //
310 -
311 - double MemoryLow = 1024 * 1024 * 1024;
312 - double MemoryHigh = 1.1 * 1024.0 * 1024.0 * 1024.0;
313 - const int IdleThreshold = get_nprocs(); // Change math to adjust if sysconf(_SC_CLK_TCK) != 100? Is 1%
314 - long long int Start, Stop = 0;
315 - auto constexpr SleepDuration = std::chrono::seconds(30);
316 - size_t ReclaimIndex = 0;
317 - long long int const ReclaimThreshold = (get_nprocs() * sysconf(_SC_CLK_TCK) * SleepDuration / std::chrono::seconds(1)) / 200; // 0.5%
318 - long long int ReclaimWindow[20] = {}; // 10 minutes
319 - long long int ReclaimWindowLength = COUNT_OF(ReclaimWindow);
320 - bool ReclaimIdling = false;
321 -
322 - //
323 - // Fall back to drop cache if the required cgroup path is not present.
324 - //
325 -
326 - if (Mode == LxMiniInitMemoryReclaimModeGradual && access(RECLAIM_PATH, W_OK) < 0)
327 - {
328 - LOG_WARNING("access({}, W_OK) failed {}, falling back to autoMemoryReclaim = dropcache", RECLAIM_PATH, errno);
329 - Mode = LxMiniInitMemoryReclaimModeDropCache;
330 - }
331 -
332 - if (Mode == LxMiniInitMemoryReclaimModeGradual)
333 - {
334 - static_assert(COUNT_OF(ReclaimWindow) >= 6);
335 - ReclaimWindowLength = 6; // Set to 3 minutes.
336 - }
337 -
338 - for (auto i = 1; i < ReclaimWindowLength; i++)
339 - {
340 - ReclaimWindow[i] = LLONG_MIN;
341 - }
342 -
343 - std::this_thread::sleep_for(SleepDuration);
344 - for (;;)
345 - {
346 - auto const Target = std::chrono::steady_clock::now() + SleepDuration;
347 - Start = GetUserCpuTime();
348 - THROW_LAST_ERROR_IF(Start == -1);
349 -
350 - if (Mode != LxMiniInitMemoryReclaimModeDisabled)
351 - {
352 - //
353 - // Ensure that utilization is below 0.5% from the last 30 seconds, and last n minutes, of usage.
354 - //
355 -
356 - size_t const LastIndex = (ReclaimIndex + 1) % ReclaimWindowLength;
357 - if ((ReclaimWindow[LastIndex] > Start - ReclaimThreshold * (ReclaimWindowLength + 1)) &&
358 - (ReclaimWindow[ReclaimIndex] > Start - ReclaimThreshold))
359 - {
360 - if (Mode == LxMiniInitMemoryReclaimModeGradual)
361 - {
362 - double MemorySize = GetMemoryInUse();
363 - THROW_LAST_ERROR_IF(MemorySize < 0);
364 -
365 - if (MemorySize > MemoryHigh)
366 - {
367 - ReclaimIdling = false;
368 - }
369 -
370 - if (!ReclaimIdling && MemorySize > MemoryLow)
371 - {
372 - double MemoryTargetSize = MemorySize * 0.97;
373 - std::string MemoryToFree = std::to_string(size_t(MemorySize - MemoryTargetSize));
374 - // EAGAIN Means that it attempted, but was unable to evict sufficient pages.
375 - THROW_LAST_ERROR_IF(WriteToFile(RECLAIM_PATH, MemoryToFree.c_str()) < 0 && errno != EAGAIN);
376 -
377 - if (MemoryTargetSize < MemoryLow)
378 - {
379 - ReclaimIdling = true;
380 - }
381 - }
382 - }
383 - else if (!ReclaimIdling)
384 - {
385 - ReclaimIdling = true;
386 - THROW_LAST_ERROR_IF(WriteToFile(PROCFS_PATH "/sys/vm/drop_caches", "1\n") < 0);
387 - }
388 - }
389 - else
390 - {
391 - ReclaimIdling = false;
392 - }
393 -
394 - ReclaimIndex = LastIndex;
395 - ReclaimWindow[ReclaimIndex] = Start;
396 - }
397 -
398 - //
399 - // Perform memory compaction if the VM is idle.
400 - // This coalesces free pages into larger blocks for more efficient page reporting.
401 - //
402 -
403 - if ((Start - Stop) > IdleThreshold)
404 - {
405 - std::this_thread::sleep_for(std::chrono::seconds(1));
406 - Stop = GetUserCpuTime();
407 - THROW_LAST_ERROR_IF(Stop == -1);
408 - if ((Stop - Start) < IdleThreshold)
409 - {
410 - THROW_LAST_ERROR_IF(WriteToFile(PROCFS_PATH "/sys/vm/compact_memory", "1\n") < 0);
411 - }
412 - }
413 -
414 - std::this_thread::sleep_until(Target);
415 - }
416 - }
417 - CATCH_LOG()
418 - }).detach();
419 -}
420 -CATCH_LOG()
421 -
261 wil::unique_fd CreateNetlinkSocket(void)
262
263 /*++
@@ -1031,77 +870,6 @@ try
870 }
871 CATCH_RETURN_ERRNO()
872
1034 -long long int GetUserCpuTime(void)
1035 -
1036 -/*++
1037 -
1038 -Routine Description:
1039 -
1040 - This routine parses /proc/stat to query a summary of all user CPU time.
1041 -
1042 -Arguments:
1043 -
1044 - None.
1045 -
1046 -Return Value:
1047 -
1048 - The current user CPU counter for all cores.
1049 -
1050 ---*/
1051 -
1052 -{
1053 - wil::unique_fd Fd{open(PROCFS_PATH "/stat", O_RDONLY)};
1054 - if (!Fd)
1055 - {
1056 - LOG_ERROR("open failed {}", errno);
1057 - return -1;
1058 - }
1059 -
1060 - char Buffer[32];
1061 - int Result = TEMP_FAILURE_RETRY(read(Fd.get(), Buffer, (sizeof(Buffer) - 1)));
1062 - if (Result < 0)
1063 - {
1064 - LOG_ERROR("read failed {}", errno);
1065 - return -1;
1066 - }
1067 -
1068 - //
1069 - // Parse the first line of /proc/stat which is in the format
1070 - // "cpu <counter>".
1071 - //
1072 -
1073 - Buffer[Result] = '\0';
1074 - char* Sp1;
1075 - char* Info = strtok_r(Buffer, " \n", &Sp1);
1076 - Info = strtok_r(nullptr, " \n", &Sp1);
1077 - return strtoll(Info, nullptr, 10);
1078 -}
1079 -
1080 -ssize_t GetMemoryInUse(void)
1081 -
1082 -/*++
1083 -
1084 -Routine Description:
1085 -
1086 - This routine returns the amount memory in use in bytes.
1087 -
1088 -Arguments:
1089 -
1090 - None.
1091 -
1092 -Return Value:
1093 -
1094 - Total memory - Free memory. Includes that used by cache and buffers.
1095 -
1096 ---*/
1097 -try
1098 -{
1099 - struct sysinfo Info = {};
1100 - THROW_LAST_ERROR_IF(sysinfo(&Info) < 0);
1101 - return Info.totalram - Info.freeram;
1102 -}
1103 -CATCH_RETURN_ERRNO()
1104 -
873 int ImportFromSocket(const char* Destination, int Socket, int ErrorSocket, unsigned int Flags)
874
875 /*++
@@ -3267,7 +3035,7 @@ try
3035 // Configure memory reclamation.
3036 //
3037
3270 - ConfigureMemoryReduction(EarlyConfig->MemoryReclaimMode);
3038 + StartMemoryReductionThread(EarlyConfig->MemoryReclaimMode);
3039
3040 //
3041 // Initialize system distro if supported.
src/linux/init/util.cpp
+243 -1
@@ -17,6 +17,7 @@ Abstract:
17 #include <sys/epoll.h>
18 #include <sys/utsname.h>
19 #include <sys/types.h>
20 +#include <sys/sysinfo.h>
21 #include <grp.h>
22 #include <unistd.h>
23 #include <sys/prctl.h>
@@ -26,6 +27,10 @@ Abstract:
27 #include <iostream>
28 #include <sstream>
29 #include <regex>
30 +#include <thread>
31 +#include <chrono>
32 +#include <climits>
33 +#include <pthread.h>
34 #include "common.h"
35 #include "wslpath.h"
36 #include "util.h"
@@ -3463,4 +3468,241 @@ int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span
3468 }
3469
3470 return 0;
3466 -}
\ No newline at end of file
3471 +}
3472 +
3473 +#define RECLAIM_PATH CGROUP_MOUNTPOINT "/memory.reclaim"
3474 +
3475 +static long long int GetUserCpuTime()
3476 +
3477 +/*++
3478 +
3479 +Routine Description:
3480 +
3481 + This routine parses /proc/stat to query a summary of all user CPU time.
3482 +
3483 +Arguments:
3484 +
3485 + None.
3486 +
3487 +Return Value:
3488 +
3489 + The current user CPU counter for all cores.
3490 +
3491 +--*/
3492 +
3493 +{
3494 + wil::unique_fd Fd{open("/proc/stat", O_RDONLY)};
3495 + if (!Fd)
3496 + {
3497 + LOG_ERROR("open failed {}", errno);
3498 + return -1;
3499 + }
3500 +
3501 + char Buffer[32];
3502 + int Result = TEMP_FAILURE_RETRY(read(Fd.get(), Buffer, (sizeof(Buffer) - 1)));
3503 + if (Result <= 0)
3504 + {
3505 + LOG_ERROR("read failed {}", errno);
3506 + return -1;
3507 + }
3508 +
3509 + //
3510 + // Parse the first line of /proc/stat which is in the format
3511 + // "cpu <counter>".
3512 + //
3513 +
3514 + Buffer[Result] = '\0';
3515 + char* Sp1;
3516 + char* Info = strtok_r(Buffer, " \n", &Sp1);
3517 + if (Info == nullptr)
3518 + {
3519 + LOG_ERROR("/proc/stat first line missing cpu label");
3520 + return -1;
3521 + }
3522 +
3523 + Info = strtok_r(nullptr, " \n", &Sp1);
3524 + if (Info == nullptr)
3525 + {
3526 + LOG_ERROR("/proc/stat first line missing cpu counter");
3527 + return -1;
3528 + }
3529 +
3530 + return strtoll(Info, nullptr, 10);
3531 +}
3532 +
3533 +static ssize_t GetMemoryInUse()
3534 +
3535 +/*++
3536 +
3537 +Routine Description:
3538 +
3539 + This routine returns the amount memory in use in bytes.
3540 +
3541 +Arguments:
3542 +
3543 + None.
3544 +
3545 +Return Value:
3546 +
3547 + Total memory - Free memory. Includes that used by cache and buffers.
3548 +
3549 +--*/
3550 +
3551 +try
3552 +{
3553 + struct sysinfo Info = {};
3554 + THROW_LAST_ERROR_IF(sysinfo(&Info) < 0);
3555 + return (Info.totalram - Info.freeram) * Info.mem_unit;
3556 +}
3557 +CATCH_RETURN_ERRNO()
3558 +
3559 +void StartMemoryReductionThread(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode)
3560 +
3561 +/*++
3562 +
3563 +Routine Description:
3564 +
3565 + This routine starts a background thread that performs memory compaction and optional cache/memory
3566 + reclaim when the VM is idle. This ensures that the maximum number of pages can be discarded to the host.
3567 +
3568 +Arguments:
3569 +
3570 + Mode - Supplies the memory reclaim mode.
3571 +
3572 +Return Value:
3573 +
3574 + None.
3575 +
3576 +--*/
3577 +
3578 +try
3579 +{
3580 + std::thread([Mode = Mode]() mutable {
3581 + try
3582 + {
3583 + //
3584 + // Set the thread's scheduling policy to idle.
3585 + //
3586 +
3587 + sched_param Parameter{};
3588 + Parameter.sched_priority = 0;
3589 + const int Result = pthread_setschedparam(pthread_self(), SCHED_IDLE, &Parameter);
3590 + THROW_ERRNO_IF(Result, Result != 0);
3591 +
3592 + //
3593 + // Periodically check if the machine is idle by querying procfs for CPU usage.
3594 + // Memory compaction will occur if both of the following conditions are true:
3595 + // 1. The CPU time since the last check is greater than the idle threshold.
3596 + // 2. The current CPU usage is below the idle threshold. This is measured by taking two readings one second apart.
3597 + //
3598 +
3599 + double MemoryLow = 1024 * 1024 * 1024;
3600 + double MemoryHigh = 1.1 * 1024.0 * 1024.0 * 1024.0;
3601 + const int IdleThreshold = get_nprocs(); // Change math to adjust if sysconf(_SC_CLK_TCK) != 100? Is 1%
3602 + long long int Start, Stop = 0;
3603 + auto constexpr SleepDuration = std::chrono::seconds(30);
3604 + size_t ReclaimIndex = 0;
3605 + long long int const ReclaimThreshold = (get_nprocs() * sysconf(_SC_CLK_TCK) * SleepDuration / std::chrono::seconds(1)) / 200; // 0.5%
3606 + long long int ReclaimWindow[20] = {}; // 10 minutes
3607 + long long int ReclaimWindowLength = COUNT_OF(ReclaimWindow);
3608 + bool ReclaimIdling = false;
3609 +
3610 + //
3611 + // Fall back to drop cache if the required cgroup path is not present.
3612 + //
3613 +
3614 + if (Mode == LxMiniInitMemoryReclaimModeGradual && access(RECLAIM_PATH, W_OK) < 0)
3615 + {
3616 + LOG_WARNING("access({}, W_OK) failed {}, falling back to drop_caches", RECLAIM_PATH, errno);
3617 + Mode = LxMiniInitMemoryReclaimModeDropCache;
3618 + }
3619 +
3620 + if (Mode == LxMiniInitMemoryReclaimModeGradual)
3621 + {
3622 + static_assert(COUNT_OF(ReclaimWindow) >= 6);
3623 + ReclaimWindowLength = 6; // Set to 3 minutes.
3624 + }
3625 +
3626 + for (auto i = 1; i < ReclaimWindowLength; i++)
3627 + {
3628 + ReclaimWindow[i] = LLONG_MIN;
3629 + }
3630 +
3631 + std::this_thread::sleep_for(SleepDuration);
3632 + for (;;)
3633 + {
3634 + auto const Target = std::chrono::steady_clock::now() + SleepDuration;
3635 + Start = GetUserCpuTime();
3636 + THROW_LAST_ERROR_IF(Start == -1);
3637 +
3638 + if (Mode != LxMiniInitMemoryReclaimModeDisabled)
3639 + {
3640 + //
3641 + // Ensure that utilization is below 0.5% from the last 30 seconds, and last n minutes, of usage.
3642 + //
3643 +
3644 + size_t const LastIndex = (ReclaimIndex + 1) % ReclaimWindowLength;
3645 + if ((ReclaimWindow[LastIndex] > Start - ReclaimThreshold * (ReclaimWindowLength + 1)) &&
3646 + (ReclaimWindow[ReclaimIndex] > Start - ReclaimThreshold))
3647 + {
3648 + if (Mode == LxMiniInitMemoryReclaimModeGradual)
3649 + {
3650 + double MemorySize = GetMemoryInUse();
3651 + THROW_LAST_ERROR_IF(MemorySize < 0);
3652 +
3653 + if (MemorySize > MemoryHigh)
3654 + {
3655 + ReclaimIdling = false;
3656 + }
3657 +
3658 + if (!ReclaimIdling && MemorySize > MemoryLow)
3659 + {
3660 + double MemoryTargetSize = MemorySize * 0.97;
3661 + std::string MemoryToFree = std::to_string(size_t(MemorySize - MemoryTargetSize));
3662 + // EAGAIN Means that it attempted, but was unable to evict sufficient pages.
3663 + THROW_LAST_ERROR_IF(WriteToFile(RECLAIM_PATH, MemoryToFree.c_str()) < 0 && errno != EAGAIN);
3664 +
3665 + if (MemoryTargetSize < MemoryLow)
3666 + {
3667 + ReclaimIdling = true;
3668 + }
3669 + }
3670 + }
3671 + else if (!ReclaimIdling)
3672 + {
3673 + ReclaimIdling = true;
3674 + THROW_LAST_ERROR_IF(WriteToFile("/proc/sys/vm/drop_caches", "1\n") < 0);
3675 + }
3676 + }
3677 + else
3678 + {
3679 + ReclaimIdling = false;
3680 + }
3681 +
3682 + ReclaimIndex = LastIndex;
3683 + ReclaimWindow[ReclaimIndex] = Start;
3684 + }
3685 +
3686 + //
3687 + // Perform memory compaction if the VM is idle.
3688 + // This coalesces free pages into larger blocks for more efficient page reporting.
3689 + //
3690 +
3691 + if ((Start - Stop) > IdleThreshold)
3692 + {
3693 + std::this_thread::sleep_for(std::chrono::seconds(1));
3694 + Stop = GetUserCpuTime();
3695 + THROW_LAST_ERROR_IF(Stop == -1);
3696 + if ((Stop - Start) < IdleThreshold)
3697 + {
3698 + THROW_LAST_ERROR_IF(WriteToFile("/proc/sys/vm/compact_memory", "1\n") < 0);
3699 + }
3700 + }
3701 +
3702 + std::this_thread::sleep_until(Target);
3703 + }
3704 + }
3705 + CATCH_LOG()
3706 + }).detach();
3707 +}
3708 +CATCH_LOG()
src/linux/init/util.h
+3
@@ -315,4 +315,7 @@ uint16_t UtilWinAfToLinuxAf(uint16_t AddressFamily);
315
316 int WriteToFile(const char* Path, const char* Content, int permissions = 0644);
317
318 +// Starts a background thread that performs memory compaction and optional cache reclaim when the VM is idle.
319 +void StartMemoryReductionThread(LX_MINI_INIT_MEMORY_RECLAIM_MODE Mode);
320 +
321 int ProcessCreateProcessMessage(wsl::shared::Transaction& Transaction, gsl::span<gsl::byte> Buffer);
\ No newline at end of file