Skip to content

Commit 0e4311f

Browse files
committed
feat(reaper): rework reaper logic to make it faster
This commit changes the way the reaper works. First this commit changed the `known_hwnds` held by the `WindowManager` to be a HashMap of window handles (isize) to a pair of monitor_idx, workspace_idx (usize, usize). This commit then changes the reaper to have a cache of hwnds which is updated by the `WindowManager` when they change. The reaper has a thread that is continuously checking this cache to see if there is any window handle that no longer exists. When it finds them, the thread sends a notification to a channel which is then received by the reaper on another thread that actually does the work on the `WindowManager` by removing said windows. This means that the reaper no longer tries to access and lock the `WindowManager` every second like it used to, but instead it only does it when it actually needs, when a window actually needs to be reaped. This means that we can make the thread that checks for orphan windows run much more frequently since it won't influence the rest of komorebi.
1 parent 30c22f5 commit 0e4311f

File tree

6 files changed

+211
-61
lines changed

6 files changed

+211
-61
lines changed

komorebi/src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ fn main() -> Result<()> {
291291
transparency_manager::listen_for_notifications(wm.clone());
292292
workspace_reconciliator::listen_for_notifications(wm.clone());
293293
monitor_reconciliator::listen_for_notifications(wm.clone())?;
294-
reaper::watch_for_orphans(wm.clone());
294+
reaper::listen_for_notifications(wm.clone());
295295
focus_manager::listen_for_notifications(wm.clone());
296296
theme_manager::listen_for_notifications();
297297

komorebi/src/monitor_reconciliator/mod.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
385385
}
386386

387387
// Update known_hwnds
388-
wm.known_hwnds.retain(|i| !windows_to_remove.contains(i));
388+
wm.known_hwnds.retain(|i, _| !windows_to_remove.contains(i));
389389

390390
if !newly_removed_displays.is_empty() {
391391
// After we have cached them, remove them from our state
@@ -481,7 +481,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
481481
{
482482
container.windows_mut().retain(|window| {
483483
window.exe().is_ok()
484-
&& !known_hwnds.contains(&window.hwnd)
484+
&& !known_hwnds.contains_key(&window.hwnd)
485485
});
486486

487487
if container.windows().is_empty() {
@@ -519,7 +519,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
519519

520520
if let Some(window) = workspace.maximized_window() {
521521
if window.exe().is_err()
522-
|| known_hwnds.contains(&window.hwnd)
522+
|| known_hwnds.contains_key(&window.hwnd)
523523
{
524524
workspace.set_maximized_window(None);
525525
} else if is_focused_workspace {
@@ -530,7 +530,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
530530
if let Some(container) = workspace.monocle_container_mut() {
531531
container.windows_mut().retain(|window| {
532532
window.exe().is_ok()
533-
&& !known_hwnds.contains(&window.hwnd)
533+
&& !known_hwnds.contains_key(&window.hwnd)
534534
});
535535

536536
if container.windows().is_empty() {
@@ -552,7 +552,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
552552
}
553553

554554
workspace.floating_windows_mut().retain(|window| {
555-
window.exe().is_ok() && !known_hwnds.contains(&window.hwnd)
555+
window.exe().is_ok()
556+
&& !known_hwnds.contains_key(&window.hwnd)
556557
});
557558

558559
if is_focused_workspace {

komorebi/src/process_event.rs

+26-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::fs::OpenOptions;
23
use std::sync::atomic::Ordering;
34
use std::sync::Arc;
@@ -717,41 +718,51 @@ impl WindowManager {
717718
}
718719

719720
tracing::trace!("updating list of known hwnds");
720-
let mut known_hwnds = vec![];
721-
for monitor in self.monitors() {
722-
for workspace in monitor.workspaces() {
721+
let mut known_hwnds = HashMap::new();
722+
for (m_idx, monitor) in self.monitors().iter().enumerate() {
723+
for (w_idx, workspace) in monitor.workspaces().iter().enumerate() {
723724
for container in workspace.containers() {
724725
for window in container.windows() {
725-
known_hwnds.push(window.hwnd);
726+
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
726727
}
727728
}
728729

729730
for window in workspace.floating_windows() {
730-
known_hwnds.push(window.hwnd);
731+
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
731732
}
732733

733734
if let Some(window) = workspace.maximized_window() {
734-
known_hwnds.push(window.hwnd);
735+
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
735736
}
736737

737738
if let Some(container) = workspace.monocle_container() {
738739
for window in container.windows() {
739-
known_hwnds.push(window.hwnd);
740+
known_hwnds.insert(window.hwnd, (m_idx, w_idx));
740741
}
741742
}
742743
}
743744
}
744745

745-
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
746-
let file = OpenOptions::new()
747-
.write(true)
748-
.truncate(true)
749-
.create(true)
750-
.open(hwnd_json)?;
746+
if self.known_hwnds != known_hwnds {
747+
// Update reaper cache
748+
{
749+
let mut reaper_cache = crate::reaper::HWNDS_CACHE.lock();
750+
*reaper_cache = known_hwnds.clone();
751+
}
752+
753+
// Save to file
754+
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
755+
let file = OpenOptions::new()
756+
.write(true)
757+
.truncate(true)
758+
.create(true)
759+
.open(hwnd_json)?;
751760

752-
serde_json::to_writer_pretty(&file, &known_hwnds)?;
761+
serde_json::to_writer_pretty(&file, &known_hwnds.keys().collect::<Vec<_>>())?;
753762

754-
self.known_hwnds = known_hwnds;
763+
// Store new hwnds
764+
self.known_hwnds = known_hwnds;
765+
}
755766

756767
notify_subscribers(
757768
Notification {

komorebi/src/reaper.rs

+174-37
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,209 @@
11
#![deny(clippy::unwrap_used, clippy::expect_used)]
22

33
use crate::border_manager;
4+
use crate::notify_subscribers;
5+
use crate::winevent::WinEvent;
6+
use crate::NotificationEvent;
7+
use crate::Window;
48
use crate::WindowManager;
9+
use crate::WindowManagerEvent;
10+
use crate::DATA_DIR;
11+
12+
use crossbeam_channel::Receiver;
13+
use crossbeam_channel::Sender;
14+
use lazy_static::lazy_static;
515
use parking_lot::Mutex;
16+
use std::collections::HashMap;
17+
use std::fs::OpenOptions;
618
use std::sync::Arc;
19+
use std::sync::OnceLock;
720
use std::time::Duration;
821

9-
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
22+
lazy_static! {
23+
pub static ref HWNDS_CACHE: Arc<Mutex<HashMap<isize, (usize, usize)>>> =
24+
Arc::new(Mutex::new(HashMap::new()));
25+
}
26+
27+
pub struct ReaperNotification(pub HashMap<isize, (usize, usize)>);
28+
29+
static CHANNEL: OnceLock<(Sender<ReaperNotification>, Receiver<ReaperNotification>)> =
30+
OnceLock::new();
31+
32+
pub fn channel() -> &'static (Sender<ReaperNotification>, Receiver<ReaperNotification>) {
33+
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
34+
}
35+
36+
fn event_tx() -> Sender<ReaperNotification> {
37+
channel().0.clone()
38+
}
39+
40+
fn event_rx() -> Receiver<ReaperNotification> {
41+
channel().1.clone()
42+
}
43+
44+
pub fn send_notification(hwnds: HashMap<isize, (usize, usize)>) {
45+
if event_tx().try_send(ReaperNotification(hwnds)).is_err() {
46+
tracing::warn!("channel is full; dropping notification")
47+
}
48+
}
49+
50+
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
51+
watch_for_orphans(wm.clone());
52+
1053
std::thread::spawn(move || loop {
11-
match find_orphans(wm.clone()) {
54+
match handle_notifications(wm.clone()) {
1255
Ok(()) => {
1356
tracing::warn!("restarting finished thread");
1457
}
1558
Err(error) => {
16-
if cfg!(debug_assertions) {
17-
tracing::error!("restarting failed thread: {:?}", error)
18-
} else {
19-
tracing::error!("restarting failed thread: {}", error)
20-
}
59+
tracing::warn!("restarting failed thread: {}", error);
2160
}
2261
}
2362
});
2463
}
2564

26-
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
27-
tracing::info!("watching");
28-
29-
let arc = wm.clone();
65+
fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
66+
tracing::info!("listening");
3067

31-
loop {
32-
std::thread::sleep(Duration::from_secs(1));
68+
let receiver = event_rx();
3369

34-
let mut wm = arc.lock();
70+
for notification in receiver {
71+
let orphan_hwnds = notification.0;
72+
let mut wm = wm.lock();
3573
let offset = wm.work_area_offset;
3674

3775
let mut update_borders = false;
3876

39-
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
40-
let work_area = *monitor.work_area_size();
41-
let window_based_work_area_offset = (
42-
monitor.window_based_work_area_offset_limit(),
43-
monitor.window_based_work_area_offset(),
44-
);
45-
46-
let offset = if monitor.work_area_offset().is_some() {
47-
monitor.work_area_offset()
48-
} else {
49-
offset
50-
};
51-
52-
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
53-
let reaped_orphans = workspace.reap_orphans()?;
54-
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
55-
workspace.update(&work_area, offset, window_based_work_area_offset)?;
56-
update_borders = true;
77+
for (hwnd, (m_idx, w_idx)) in orphan_hwnds.iter() {
78+
if let Some(monitor) = wm.monitors_mut().get_mut(*m_idx) {
79+
let focused_workspace_idx = monitor.focused_workspace_idx();
80+
let work_area = *monitor.work_area_size();
81+
let window_based_work_area_offset = (
82+
monitor.window_based_work_area_offset_limit(),
83+
monitor.window_based_work_area_offset(),
84+
);
85+
86+
let offset = if monitor.work_area_offset().is_some() {
87+
monitor.work_area_offset()
88+
} else {
89+
offset
90+
};
91+
92+
if let Some(workspace) = monitor.workspaces_mut().get_mut(*w_idx) {
93+
// Remove orphan window
94+
if let Err(error) = workspace.remove_window(*hwnd) {
95+
tracing::warn!(
96+
"error reaping orphan window ({}) on monitor: {}, workspace: {}. Error: {}",
97+
hwnd,
98+
m_idx,
99+
w_idx,
100+
error,
101+
);
102+
}
103+
104+
if focused_workspace_idx == *w_idx {
105+
// If this is not a focused workspace there is no need to update the
106+
// workspace or the borders. That will already be done when the user
107+
// changes to this workspace.
108+
workspace.update(&work_area, offset, window_based_work_area_offset)?;
109+
update_borders = true;
110+
}
57111
tracing::info!(
58-
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
59-
reaped_orphans.0,
60-
reaped_orphans.1,
61-
i,
62-
j
112+
"reaped orphan window ({}) on monitor: {}, workspace: {}",
113+
hwnd,
114+
m_idx,
115+
w_idx,
63116
);
64117
}
65118
}
119+
120+
wm.known_hwnds.remove(hwnd);
121+
122+
let window = Window::from(*hwnd);
123+
notify_subscribers(
124+
crate::Notification {
125+
event: NotificationEvent::WindowManager(WindowManagerEvent::Destroy(
126+
WinEvent::ObjectDestroy,
127+
window,
128+
)),
129+
state: wm.as_ref().into(),
130+
},
131+
true,
132+
)?;
66133
}
67134

68135
if update_borders {
69136
border_manager::send_notification(None);
70137
}
138+
139+
// Save to file
140+
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
141+
let file = OpenOptions::new()
142+
.write(true)
143+
.truncate(true)
144+
.create(true)
145+
.open(hwnd_json)?;
146+
147+
serde_json::to_writer_pretty(&file, &wm.known_hwnds.keys().collect::<Vec<_>>())?;
148+
}
149+
150+
Ok(())
151+
}
152+
153+
fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
154+
// Cache current hwnds
155+
{
156+
let mut cache = HWNDS_CACHE.lock();
157+
*cache = wm.lock().known_hwnds.clone();
158+
}
159+
160+
std::thread::spawn(move || loop {
161+
match find_orphans() {
162+
Ok(()) => {
163+
tracing::warn!("restarting finished thread");
164+
}
165+
Err(error) => {
166+
if cfg!(debug_assertions) {
167+
tracing::error!("restarting failed thread: {:?}", error)
168+
} else {
169+
tracing::error!("restarting failed thread: {}", error)
170+
}
171+
}
172+
}
173+
});
174+
}
175+
176+
fn find_orphans() -> color_eyre::Result<()> {
177+
tracing::info!("watching");
178+
179+
loop {
180+
std::thread::sleep(Duration::from_millis(20));
181+
182+
let mut cache = HWNDS_CACHE.lock();
183+
let mut orphan_hwnds = HashMap::new();
184+
185+
for (hwnd, (m_idx, w_idx)) in cache.iter() {
186+
let window = Window::from(*hwnd);
187+
188+
if !window.is_window()
189+
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
190+
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
191+
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
192+
// docs is closed
193+
//
194+
// I hate every single person who worked on Microsoft Office 365, especially Word
195+
|| !window.is_visible()
196+
{
197+
orphan_hwnds.insert(window.hwnd, (*m_idx, *w_idx));
198+
}
199+
}
200+
201+
if !orphan_hwnds.is_empty() {
202+
// Update reaper cache
203+
cache.retain(|h, _| !orphan_hwnds.contains_key(h));
204+
205+
// Send handles to remove
206+
event_tx().send(ReaperNotification(orphan_hwnds))?;
207+
}
71208
}
72209
}

komorebi/src/static_config.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,7 @@ impl StaticConfig {
12081208
pending_move_op: Arc::new(None),
12091209
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
12101210
uncloack_to_ignore: 0,
1211-
known_hwnds: Vec::new(),
1211+
known_hwnds: HashMap::new(),
12121212
};
12131213

12141214
match value.focus_follows_mouse {

0 commit comments

Comments
 (0)