// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:Memory; import std; import :Json; // Memory (2026-10-22, Jorijn: "make the concurrency limit from free memory, // instead of just guessing"): what the machine has free, what a pass costs, // and the budget the pool admits passes against. // // - Free: the kernel's MemAvailable, read at the start of every gate and // before every launch: the whole machine's, which is right for a budget and // wrong for a cost. // - Cost: the peak PSS of a pass's own process tree (claude, and for the // browser environments the Playwright server and Chromium under it), sampled // by the driver on its own passes and kept per repository and environment // (Calibration); a pass is admitted at the highest of its environment's // recent peaks, or at the measured default before any is recorded. // - Reserve: what is left for the desktop and the page cache — // REVIEW_GATES_MEM_RESERVE_MB, by default 10 % of MemTotal, at least 2 GB. // A policy, not a measurement. export namespace Crafter::Review::Memory { // Recent peaks kept per repository and environment. constexpr std::size_t Keep = 30; // MemAvailable in MB; nullopt without /proc/meminfo. std::optional Available(const std::filesystem::path& path = "/proc/meminfo"); // REVIEW_GATES_MEM_RESERVE_MB, else 10 % of MemTotal, at least 2048. // std::invalid_argument for a value that is no number. std::int64_t Reserve(const std::optional& configured, const std::filesystem::path& path = "/proc/meminfo"); std::int64_t Reserve(); // The peak tree PSS of every watched pass, on one thread. A sweep makes one // ppid map, then reads smaps_rollup for every process of every watched tree // — the costly read, it walks the page tables. The pause after a sweep is // `interval` or three times the sweep's own length, whichever is longer, so // sampling holds at most a quarter of one core whatever the number of // passes (2026-10-22). A pass shorter than one pause may go unmeasured. class Sampler { public: using Measure = std::function>& kids)>; using Scan = std::function>()>; explicit Sampler(double interval = 3.0, Measure measure = {}, Scan scan = {}); ~Sampler(); Sampler(const Sampler&) = delete; Sampler& operator=(const Sampler&) = delete; void Watch(const std::string& key, std::int64_t pid); // One sweep over every watched pass; its length in seconds. double Sample(); // Stop watching; the peak in MB, rounded to 0.1, or nullopt when // nothing was measured. std::optional Done(const std::string& key); static double Pause(double interval, double sweep); private: struct State; std::shared_ptr state; }; // Recent peaks per repository and environment, in one JSON file next to the // seat ledger: {repo: {env: [MB]}}. A record, never a dependency // (2026-10-22): a damaged file reads as empty, a part of the wrong shape as // absent, and a peak that cannot be written is dropped. class Calibration { public: Calibration(std::filesystem::path path, const std::filesystem::path& repo, std::map defaults); std::vector Peaks(std::string_view environment) const; struct Cost { // MB, an int or a float as the file holds it. Json megabytes; std::string source; }; Cost CostOf(std::string_view environment) const; // Keep a pass's peak; whether it was stored. The file is every // driver's on this machine: the re-read and the write are one section // under its lock (2026-10-31), so a peak another driver wrote between // them is kept. bool Record(std::string_view environment, double megabytes); private: std::filesystem::path path; std::string repo; std::map defaults; mutable std::mutex lock; Json Load() const; }; }