qcode/ui/workspace/freezing.rs
1//! The screen's side of freezing: when the workspace screen looks at what its profiles are doing,
2//! and what it does with what it finds.
3//!
4//! [`freeze`] says when a profile's container may be frozen and what freezing one is. This is
5//! everything around that question: the once-asked question of whether this machine can freeze at
6//! all, the minute that brings the screen back to look, the readings a look is a difference from,
7//! and the containers QCode has frozen and not woken yet.
8//!
9//! Nothing here decides on its own what a profile is doing. A look reads the machine's side of
10//! [`freeze::Signals`] on a background thread and the screen builds the rest of them when the
11//! answer comes, so what is judged is the screen as it is then rather than as it was when the
12//! reading went out. A reading that cannot be had is a no rather than a maybe: a container whose
13//! control group or whose `/proc` cannot be read is left exactly as it is, which is the whole
14//! point of the rules in [`freeze`].
15//!
16//! The lengths the rules are judged by — [`freeze::QUIET`], [`freeze::CPU_WINDOW`] and the minute
17//! between looks — and where the machine's control groups are read from are the screen's own
18//! [`Where`], so that a test may shorten the first and point the last at a folder of its own. The
19//! product uses the measured values; nothing else may.
20
21use std::collections::{HashMap, HashSet};
22use std::path::{Path, PathBuf};
23use std::time::{Duration, Instant};
24
25use qframe::prelude::*;
26use qframe::runtime::Task;
27
28use crate::engine::{Engine, EngineCommand};
29use crate::profile::Profile;
30
31use super::freeze::{self, CPU_WINDOW, Lengths, QUIET, Signals};
32use super::{Msg, OpenWorkspace, Tab, TabKey, TabKind, WorkspaceScreen, relay, shared};
33
34/// How often the screen looks at what its profiles are doing.
35///
36/// A minute is short enough that a container which grows quiet is frozen soon after the last thing
37/// in it finished, and long enough that a screen left open all day is not asking the engine and
38/// the kernel about itself hundreds of times. The rules are measured in tens of minutes, so the
39/// finest this reading has to be is whether a moment of work is behind it. It is shorter than
40/// [`CPU_WINDOW`] on purpose: the CPU is then read from a reading kept across looks (see
41/// `Freezing::spent`), so the window is judged every second look rather than never.
42pub(super) const EVERY: Duration = Duration::from_secs(60);
43
44/// What one look's reading of a container found, which is what [`Msg::Looked`] carries.
45///
46/// `None` for either field is the engine or the kernel refusing, and either way it is not a yes: a
47/// container whose control group or whose `/proc` cannot be read is not judged on what it might
48/// have been doing.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Read {
51 /// The microseconds of CPU everything in the container had used when the reading was taken.
52 pub used: Option<u64>,
53 /// The command line of every process in the container.
54 pub processes: Option<Vec<String>>,
55}
56
57/// A container's CPU as of a moment: what it had used, and when that was read. Kept per container
58/// between looks, since the reading is only a difference against the one before it.
59#[derive(Debug, Clone, Copy)]
60struct Taken {
61 used: u64,
62 at: Instant,
63}
64
65/// The lengths a look is judged by and the folder the machine's control groups are read from.
66///
67/// The product's are the measured [`freeze`] constants and the machine's own cgroup root; a test
68/// shortens the first and points the last at a folder of its own, since a test cannot wait ten
69/// minutes and cannot write to the real one.
70#[derive(Debug, Clone)]
71pub(super) struct Where {
72 /// How long every tab of a profile must have written nothing.
73 pub(super) quiet: Duration,
74 /// The shortest window the container's CPU use is read over.
75 pub(super) window: Duration,
76 /// How often the screen looks.
77 pub(super) every: Duration,
78 /// Where this machine keeps every control group, which is the only place a frozen container's
79 /// pages can be handed back from.
80 pub(super) cgroups: PathBuf,
81}
82
83impl Default for Where {
84 fn default() -> Self {
85 Self { quiet: QUIET, window: CPU_WINDOW, every: EVERY, cgroups: PathBuf::from(freeze::CGROUP_ROOT) }
86 }
87}
88
89impl From<&Where> for Lengths {
90 fn from(where_: &Where) -> Self {
91 Self { quiet: where_.quiet, window: where_.window }
92 }
93}
94
95/// What the screen knows about freezing: what this machine can do, what has been frozen, and where
96/// each container's CPU was last read.
97#[derive(Debug, Default)]
98pub(super) struct Freezing {
99 /// Whether this machine can freeze a container at all, asked once when the screen got its
100 /// engine and never again: a session does not change its engine or its swap underneath it.
101 /// `None` until the answer comes, and `None` means no timer runs.
102 pub(super) supported: Option<bool>,
103 /// The containers QCode has frozen and not woken since, by name. A container here is one the
104 /// look leaves alone and the panel shows as frozen.
105 pub(super) frozen: HashSet<String>,
106 /// The last CPU reading of each container this screen has read, and when it was taken.
107 readings: HashMap<String, Taken>,
108 /// Whether the next look is timed, so that one is timed at a time.
109 pub(super) timed: bool,
110 /// The containers the engine has been asked to wake and has not answered for yet. A second
111 /// wake of one of them is not asked: the engine refuses to unpause a container that is awake,
112 /// and that refusal, coming after the first wake's success, would have the screen call an awake
113 /// container frozen again — and hold every message for it for ever.
114 waking: HashSet<String>,
115}
116
117impl Freezing {
118 /// Whether `container` is one QCode has frozen and not woken since.
119 #[must_use]
120 pub(super) fn is_frozen(&self, container: &str) -> bool {
121 self.frozen.contains(container)
122 }
123
124 /// Notes that `container` is frozen, or that it is not.
125 ///
126 /// A freeze that failed leaves nothing behind: no container noted, and nothing said to the
127 /// person, so the next look reads the container again and tries once more. A thaw that failed
128 /// leaves the container in the set, since it is still frozen and every later thing that needs
129 /// it must ask the engine about it again.
130 pub(super) fn note(&mut self, container: &str, frozen: bool) {
131 if frozen {
132 self.frozen.insert(container.to_owned());
133 } else {
134 self.frozen.remove(container);
135 }
136 }
137
138 /// Forgets where the CPU of `container` was last read: a woken container's next reading is a
139 /// difference from the moment it woke, not from the reading taken before it was put to sleep.
140 pub(super) fn forgot(&mut self, container: &str) {
141 self.readings.remove(container);
142 }
143
144 /// The CPU of `container` as a difference against its last reading, over the window between
145 /// the two, and keeps this one. `None` for a container seen for the first time, which is not
146 /// frozen on this look: there is nothing to take a difference from yet.
147 /// Notes that the engine answered for the wake of `container`, whatever it said.
148 pub(super) fn woke(&mut self, container: &str) {
149 self.waking.remove(container);
150 }
151
152 /// Takes out of the frozen set every container the engine has just listed as anything but
153 /// paused. The panel's own Stop and Restart, a crash of the engine or the person's own `podman
154 /// unpause` all wake or end a container without the screen hearing of it, and a container the
155 /// screen still called frozen would never be looked at again.
156 pub(super) fn listed(&mut self, containers: &[crate::engine::Container]) {
157 for container in containers {
158 if !matches!(container.state, crate::engine::ContainerState::Paused) {
159 self.frozen.remove(&container.name);
160 }
161 }
162 }
163
164 /// The CPU `container` has used since the reading this one is measured from, and over how long.
165 ///
166 /// That reading is kept until it is `window` old. The screen looks more often than the window
167 /// is long, so measuring from the look before would give a window one look long on every look,
168 /// shorter than the rule asks for, and no container would ever be frozen. Kept, the reading
169 /// gives a window that grows look by look until it is long enough to be judged on, and only
170 /// then does the new reading take its place.
171 fn spent(&mut self, container: &str, used: u64, at: Instant, window: Duration) -> Option<(u64, Duration)> {
172 let since = |before: &Taken| (used.saturating_sub(before.used), at.saturating_duration_since(before.at));
173 match self.readings.get(container) {
174 Some(before) if at.saturating_duration_since(before.at) < window => Some(since(before)),
175 _ => self.readings.insert(container.to_owned(), Taken { used, at }).map(|before| since(&before)),
176 }
177 }
178}
179
180/// Forgets where the CPU of every container of the workspace at `index` was last read, which is
181/// leaving the rail: a container of a workspace that is not on the screen is not looked at again,
182/// and one that comes back is read from this moment rather than from a reading taken before its
183/// tabs were closed.
184pub(super) fn left(screen: &mut WorkspaceScreen, index: usize) {
185 let Some(workspace) = screen.workspaces.get(index) else { return };
186 let containers: Vec<String> =
187 workspace.profiles.iter().filter_map(|profile| container_of(workspace, profile)).collect();
188 for container in containers {
189 screen.freezing.forgot(&container);
190 }
191}
192
193/// Asks once, on a background thread, whether this machine can freeze a container at all, and keeps
194/// the answer: podman, running as the person who started it, and with swap. Nothing else about
195/// freezing happens without it, and no timer is run at all when it says no.
196///
197/// Runs the engine, so it belongs on a background thread.
198pub(super) fn ask(engine: Engine) -> Command<Msg> {
199 Command::perform(move || Msg::Freezable(freeze::supported(&engine)))
200}
201
202/// The next look, timed once, while there is something to look at: the person wants quiet profiles
203/// frozen, this machine can freeze one, and a workspace is open. One timer at a time, so a look
204/// that is already timed is not timed again.
205pub(super) fn again(screen: &mut WorkspaceScreen) -> Command<Msg> {
206 if !may_look(screen) || screen.freezing.timed {
207 return Command::none();
208 }
209 screen.freezing.timed = true;
210 let every = screen.where_.every;
211 Command::task(Task::new(t!("workspace.freezing.looking"), move |cx| {
212 if cx.sleep(every) { Ok(Msg::Look) } else { Err(String::new()) }
213 }))
214}
215
216/// Whether there is anything for a look to be about: the person turned freezing on, this machine can
217/// do it, and there is an engine to ask.
218#[must_use]
219pub(super) fn may_look(screen: &WorkspaceScreen) -> bool {
220 screen.freezes_idle() && screen.freezing.supported == Some(true) && screen.engine().is_some()
221}
222
223/// One look: for every open workspace, every profile of it that has a terminal tab and whose
224/// container is not frozen already is read out of its container and its `/proc`.
225///
226/// The reading is what the engine and the kernel are asked, and it goes to a background thread of
227/// its own for each profile, so a slow one holds up nothing but the profile it is about and no
228/// part of the screen waits for it. What the profile's tabs are doing is not read here: it is built
229/// when the answer comes, so what is judged is the screen as it is then.
230///
231/// A look that was timed before the person turned freezing off, or before this machine turned out
232/// not to be able to freeze anything, reads nothing when it comes round: the person is not waiting
233/// for a timer to finish before their choice takes hold, and no timer is timed again either.
234pub(super) fn look(screen: &mut WorkspaceScreen) -> Command<Msg> {
235 if !may_look(screen) {
236 return Command::none();
237 }
238 let Some(engine) = screen.engine.clone() else { return Command::none() };
239 let cgroups = screen.where_.cgroups.clone();
240 let mut commands = Vec::new();
241 for (index, workspace) in screen.workspaces.iter().enumerate() {
242 for profile in &workspace.profiles {
243 let Some(container) = container_of(workspace, profile) else { continue };
244 if screen.freezing.is_frozen(&container) {
245 continue;
246 }
247 let (id, name) = (workspace.id().to_owned(), profile.name.to_string());
248 let (engine, container) = (engine.clone(), container.clone());
249 let cgroups = cgroups.clone();
250 commands.push(Command::perform(move || {
251 Msg::Looked(index, id, name, read(&engine, &container, &cgroups), Instant::now())
252 }));
253 }
254 }
255 Command::batch(commands)
256}
257
258/// What came of a look's reading: the signals the screen builds now for the profile, the CPU it
259/// spent since the reading before it, and whether the container may be frozen.
260///
261/// A profile whose workspace has closed, or which the store no longer carries, is left alone: there
262/// is no container of it on the screen to do anything with. A reading that could not be had carries
263/// no `cpu` at all, so a container whose control group would not say is never frozen on a reading
264/// that says nothing.
265pub(super) fn looked(
266 screen: &mut WorkspaceScreen,
267 index: usize,
268 id: &str,
269 name: &str,
270 read: Read,
271 at: Instant,
272) -> Command<Msg> {
273 let Some(workspace) = screen.workspaces.get(index).filter(|workspace| workspace.id() == id) else {
274 return Command::none();
275 };
276 let Some(profile) = workspace.profiles.iter().find(|profile| profile.name.as_str() == name) else {
277 return Command::none();
278 };
279 let Some(container) = container_of(workspace, profile) else { return Command::none() };
280 let mut signals = signals(screen, index, workspace, profile);
281 let window = screen.where_.window;
282 signals.cpu = read.used.and_then(|used| screen.freezing.spent(&container, used, at, window));
283 signals.processes = read.processes;
284 if !freeze::may_freeze(&signals, profile.harness, Lengths::from(&screen.where_)) {
285 return Command::none();
286 }
287 let Some(engine) = screen.engine.clone() else { return Command::none() };
288 let cgroups = screen.where_.cgroups.clone();
289 Command::perform(move || {
290 // Paused is frozen, whatever came of the reclaim after it: a container the engine paused and
291 // the kernel took nothing back from is still one whose tabs answer nothing until it is woken,
292 // and the screen has to know so to wake it.
293 let paused = !matches!(freeze::freeze(&engine, &container, &cgroups), Err(freeze::FreezeTrouble::Engine(_)));
294 Msg::Frozen(container, paused)
295 })
296}
297
298/// Wakes the container of the tab the person has just brought into view, when QCode froze it: a
299/// paused container answers nothing to the harness in it and refuses every command the screen runs
300/// there, and the person has come back to this tab.
301///
302/// Every place the screen changes which tab is shown comes through here, so there is one answer to
303/// "the shown tab is a profile's, and that profile's container is frozen" rather than one for each
304/// way of showing a tab. A tab that is still starting is left to the engine bringing it up, which
305/// wakes a frozen container itself: asking twice would have one of the two refused, and a refusal
306/// is what a tab shows as its own failure.
307#[must_use]
308pub(super) fn shown(screen: &mut WorkspaceScreen) -> Command<Msg> {
309 let Some(tab) = screen.workspace().and_then(OpenWorkspace::active_tab) else { return Command::none() };
310 if tab.state().is_starting() {
311 return Command::none();
312 }
313 let Some(workspace) = screen.workspace() else { return Command::none() };
314 let Some(container) = container_of_tab(workspace, tab) else { return Command::none() };
315 thawing(screen, container)
316}
317
318/// The container of the tab `key` being closed, when QCode froze it, and `None` for every other
319/// tab: what the command that ends that tab's work runs in has to be awake for, since a paused
320/// container refuses it and a harness nobody is left to read goes on working.
321#[must_use]
322pub(super) fn closing(screen: &WorkspaceScreen, key: TabKey) -> Option<String> {
323 let workspace = screen.owner(key)?;
324 let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
325 let container = container_of_tab(workspace, tab)?;
326 screen.freezing.is_frozen(&container).then_some(container)
327}
328
329/// The container of `tab`, when QCode froze it and a message is waiting in the tab to be typed into
330/// its harness, and `None` for every other case: a paused container would take the paste into a
331/// program that is not running, and a message is better a while in the tab where the person can
332/// read it. A tab with no message waiting is not a reason to wake anything.
333#[must_use]
334pub(super) fn waiting_in(screen: &WorkspaceScreen, workspace: &OpenWorkspace, tab: &Tab) -> Option<String> {
335 if tab.letters().is_empty() {
336 return None;
337 }
338 let container = container_of_tab(workspace, tab)?;
339 screen.freezing.is_frozen(&container).then_some(container)
340}
341
342/// Notes that the container of the tab `key` is up, which is what the engine's own bringing a tab
343/// up does to a container QCode froze: the screen stops calling it frozen, and the CPU it had used
344/// before it slept says nothing about what it has done since.
345pub(super) fn up(screen: &mut WorkspaceScreen, key: TabKey) {
346 let container = screen.owner(key).and_then(|workspace| {
347 let tab = workspace.tabs.iter().find(|tab| tab.key() == key)?;
348 container_of_tab(workspace, tab)
349 });
350 if let Some(container) = container {
351 screen.freezing.note(&container, false);
352 screen.freezing.forgot(&container);
353 }
354}
355
356/// The waking of `container`, which the screen has frozen, on a background thread. A container
357/// whose wake is already on its way is not asked for again.
358#[must_use]
359pub(super) fn thawing(screen: &mut WorkspaceScreen, container: String) -> Command<Msg> {
360 if !screen.freezing.is_frozen(&container) || screen.freezing.waking.contains(&container) {
361 return Command::none();
362 }
363 let Some(engine) = screen.engine.clone() else { return Command::none() };
364 screen.freezing.waking.insert(container.clone());
365 Command::perform(move || Msg::Thawed(container.clone(), awake(&engine, &container)))
366}
367
368/// Wakes `container` and answers whether it is awake now. A wake the engine refused is asked about
369/// once more, since the refusal may only mean that something else woke it first — the engine
370/// bringing a tab up, the panel's own Stop, the person's own `podman unpause` — and an awake
371/// container the screen still called frozen would hold every message for it.
372///
373/// Runs engine commands, so it belongs on a background thread.
374fn awake(engine: &Engine, container: &str) -> bool {
375 freeze::thaw(engine, container).is_ok()
376 || crate::engine::run::capture(&engine.container_state(container)).is_ok_and(|word| {
377 !matches!(crate::engine::ContainerState::parse(&word), crate::engine::ContainerState::Paused)
378 })
379}
380
381/// The waking of `container` on the screen's own account followed by the work that has to run in it,
382/// both on one background thread and in that order: a command run in a paused container is refused,
383/// and a second command would not wait for the first.
384///
385/// A container the engine would not wake refuses the second command as well, which costs nothing:
386/// there was nothing running in it to end, and the next thing that needs it asks again.
387pub(super) fn thawing_then(screen: &mut WorkspaceScreen, container: String, then: EngineCommand) -> Command<Msg> {
388 let Some(engine) = screen.engine.clone() else { return Command::none() };
389 screen.freezing.waking.insert(container.clone());
390 Command::perform(move || {
391 let awake = awake(&engine, &container);
392 let _ = crate::engine::run::capture(&then);
393 Msg::Thawed(container, awake)
394 })
395}
396
397/// The engine commands that wake every container the screen has frozen, one name each and in an
398/// order of their own, for the moment QCode is going.
399///
400/// A container left paused is one the next QCode finds asleep with every tab in it still holding
401/// what it held, and one the reaper cannot stop, since a stop of a paused container is refused.
402/// QCode waits for the engine over these, since there is no later to run them in.
403#[must_use]
404pub fn waking_on_the_way_out(screen: &WorkspaceScreen) -> Vec<EngineCommand> {
405 let Some(engine) = screen.engine.as_ref() else { return Vec::new() };
406 let mut names: Vec<&str> = screen.freezing.frozen.iter().map(String::as_str).collect();
407 names.sort_unstable();
408 names.into_iter().map(|name| engine.unpause_container(name)).collect()
409}
410
411/// Builds what the screen knows about `profile` of `workspace`, which is every rule but the two that
412/// are a difference over time and the two the machine is asked about.
413///
414/// A profile that opens a window is never frozen: its window is a container of its own and the
415/// person is looking at it. A profile with no terminal tab is not judged either, since
416/// [`Signals::quiet_for`] is `None` without one, which is the rules' own answer.
417fn signals(screen: &WorkspaceScreen, index: usize, workspace: &OpenWorkspace, profile: &Profile) -> Signals {
418 let now = Instant::now();
419 let tabs: Vec<&Tab> = workspace.tabs.iter().filter(|tab| owns(tab, profile)).collect();
420 let quiet_for = tabs
421 .iter()
422 .filter_map(|tab| tab.session())
423 .map(|session| now.saturating_duration_since(session.last_output()))
424 .min();
425 let (relay_busy, relay_quiet_for) = relay_of(workspace, profile, now);
426 Signals {
427 // Only the tab the person is looking at is on screen, and only the open workspace has one:
428 // a tab of a workspace behind another is not on the screen, and a profile of the same name in
429 // another workspace is a container of its own that nothing of this one can be on the screen
430 // of.
431 on_screen: index == screen.active
432 && screen.workspace().and_then(OpenWorkspace::active_tab).is_some_and(|tab| owns(tab, profile)),
433 // A line the person has not sent, a message being pasted, or a message waiting to be
434 // delivered: each is work in a tab of this profile whatever its container is doing.
435 typing: tabs.iter().any(|tab| tab.person_typed() || tab.owed().is_some() || !tab.letters().is_empty()),
436 quiet_for,
437 relay_busy,
438 relay_quiet_for,
439 cpu: None,
440 processes: None,
441 desktop: profile.harness.desktop().is_some(),
442 }
443}
444
445/// The profile whose container `tab` runs in, when it runs in one: a harness in a terminal, or the
446/// administrator's shell, which is a terminal in the same container. A window's tab is not one — it
447/// has a container of its own, which is never frozen — and neither is a file, a shell or a blank
448/// tab.
449#[must_use]
450fn of(tab: &Tab) -> Option<&str> {
451 match tab.kind() {
452 TabKind::Profile(name) | TabKind::Admin(name) => Some(name.as_str()),
453 _ => None,
454 }
455}
456
457/// Whether `tab` is a terminal tab of `profile`, which is what a profile's container is entered
458/// with.
459#[must_use]
460fn owns(tab: &Tab, profile: &Profile) -> bool {
461 of(tab).is_some_and(|name| name == profile.name.as_str())
462}
463
464/// The container `profile`'s terminal tabs of `workspace` run in, and `None` when it has none: a
465/// profile nobody opened a terminal tab of has no container to read, and a window's own container is
466/// a thing of its own that is never frozen.
467#[must_use]
468fn container_of(workspace: &OpenWorkspace, profile: &Profile) -> Option<String> {
469 workspace
470 .tabs
471 .iter()
472 .any(|tab| owns(tab, profile))
473 .then(|| crate::engine::names::profile_container(workspace.id(), profile.name.as_str()))
474}
475
476/// The container `tab` runs in, and `None` for a tab that runs in none or in a window's own.
477#[must_use]
478fn container_of_tab(workspace: &OpenWorkspace, tab: &Tab) -> Option<String> {
479 let name = of(tab)?;
480 let profile = workspace.profiles.iter().find(|profile| profile.name.as_str() == name)?;
481 container_of(workspace, profile)
482}
483
484/// What the relay is doing for `profile`'s tokens, and how long ago the last request of one of them
485/// finished.
486///
487/// The tokens are each tab's own, and, for opencode's shared server, the server's own token as
488/// well: a request of a shared server is carried by the server's token, not by the tab that asked
489/// for it. A profile that does not run on a provider carries `None` for both, which skips the
490/// relay's rules rather than failing them, and so does one whose relay is not listening, since
491/// nothing of it can be on its way through a relay that is not there.
492fn relay_of(workspace: &OpenWorkspace, profile: &Profile, now: Instant) -> (Option<bool>, Option<Duration>) {
493 if profile.provider.is_none() {
494 return (None, None);
495 }
496 let relay::Link::On { activity, .. } = &workspace.relay else { return (None, None) };
497 let mut tokens: Vec<String> =
498 workspace.tabs.iter().filter(|tab| owns(tab, profile)).map(|tab| tab.token().to_owned()).collect();
499 if shared::shares(profile) {
500 tokens.push(workspace.servers.token(profile.name.as_str()));
501 }
502 let busy = tokens.iter().any(|token| activity.busy(token));
503 let quiet_for = tokens
504 .iter()
505 .filter_map(|token| activity.last_finished(token))
506 .map(|at| now.saturating_duration_since(at))
507 .min();
508 (Some(busy), quiet_for)
509}
510
511/// The microseconds of CPU everything in `container` has used, read out of the control group the
512/// engine says it has under `cgroups`, and the command line of every process in it. Either may be
513/// missing when the engine or the kernel would not say, and either way it is not a yes.
514fn read(engine: &Engine, container: &str, cgroups: &Path) -> Read {
515 Read {
516 used: freeze::cgroup(engine, container, cgroups).as_deref().and_then(freeze::cpu_used),
517 processes: freeze::processes(engine, container),
518 }
519}