From 9c83a6e71defd6df321ab874bb4551da6e65104b Mon Sep 17 00:00:00 2001 From: Raphiiko Date: Wed, 20 Dec 2023 23:48:14 +0100 Subject: [PATCH 1/2] Add memory monitoring to dev builds --- src-core/Cargo.lock | 7 ++++ src-core/Cargo.toml | 1 + src-core/src/os/mod.rs | 4 +++ src-core/src/utils/mod.rs | 73 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src-core/Cargo.lock b/src-core/Cargo.lock index 418705e9..9e21452f 100644 --- a/src-core/Cargo.lock +++ b/src-core/Cargo.lock @@ -2079,6 +2079,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "human_bytes" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" + [[package]] name = "humantime" version = "2.1.0" @@ -3281,6 +3287,7 @@ dependencies = [ "enumset", "futures", "futures-util", + "human_bytes", "hyper 0.14.26", "lazy_static", "log", diff --git a/src-core/Cargo.toml b/src-core/Cargo.toml index 7a069cef..795c2673 100644 --- a/src-core/Cargo.toml +++ b/src-core/Cargo.toml @@ -80,6 +80,7 @@ tempfile = "3.6.0" strum = "0.25" strum_macros = "0.25" steamworks = { version = "0.10.0" } +human_bytes = { version = "0.4.3" } system_shutdown = "4.0.1" async-recursion = "1.0.5" enumset = "1.1.3" diff --git a/src-core/src/os/mod.rs b/src-core/src/os/mod.rs index 372399ba..46a9c69e 100644 --- a/src-core/src/os/mod.rs +++ b/src-core/src/os/mod.rs @@ -60,6 +60,10 @@ async fn watch_processes() { } } } + // Only monitor memory usage in dev builds, to help find memory leaks + if crate::BUILD_FLAVOUR == crate::flavour::BuildFlavour::Dev { + crate::utils::monitor_memory_usage(false).await; + } tokio::time::sleep(Duration::from_secs(1)).await; } } diff --git a/src-core/src/utils/mod.rs b/src-core/src/utils/mod.rs index 579a8b38..8a1963ee 100644 --- a/src-core/src/utils/mod.rs +++ b/src-core/src/utils/mod.rs @@ -1,10 +1,11 @@ -use log::error; +use human_bytes::human_bytes; +use log::{error, warn}; use serde::Serialize; use std::{ os::raw::c_char, time::{SystemTime, UNIX_EPOCH}, }; -use sysinfo::{ProcessExt, Signal, System, SystemExt}; +use sysinfo::{Pid, PidExt, Process, ProcessExt, Signal, System, SystemExt}; use tauri::Manager; use tokio::sync::Mutex; @@ -12,6 +13,7 @@ use crate::globals::{TAURI_APP_HANDLE, TAURI_CLI_MATCHES}; lazy_static! { static ref SYSINFO: Mutex = Mutex::new(System::new_all()); + static ref LAST_MEM_DIALOG: Mutex = Mutex::new(0); } pub mod models; @@ -111,3 +113,70 @@ pub fn convert_char_array_to_string(slice: &[c_char]) -> Option { String::from_utf8(trimmed_array).ok() } + +pub async fn monitor_memory_usage(refresh_processes: bool) { + let mut sysinfo_guard = SYSINFO.lock().await; + let sysinfo = &mut *sysinfo_guard; + // Get processes + if refresh_processes { + sysinfo.refresh_processes(); + } + let processes = sysinfo.processes(); + let parent_process_id = std::process::id(); + // Collect all child process IDs + let mut process_ids: Vec = vec![]; + loop { + let mut found = false; + for process in processes.values() { + let pid = process.pid().as_u32(); + if process_ids.contains(&pid) { + continue; + } + if let Some(parent_pid) = process.parent() { + let parent_pid = parent_pid.as_u32(); + if process_ids.contains(&parent_pid) || parent_pid == parent_process_id { + process_ids.push(pid); + found = true; + } + } + } + if !found { + break; + } + } + // Notify if any process memory usage is too high + let mut children: Vec<&Process> = Vec::new(); + let mut last_mem_dialog = { + let last_mem_dialog_guard = LAST_MEM_DIALOG.lock().await; + *last_mem_dialog_guard + }; + for pid in process_ids { + let ppid = &Pid::from_u32(pid); + let process = processes.get(ppid).unwrap(); + children.push(&process); + let mem_b = process.memory(); + if mem_b >= 1024000000 { + let mem = human_bytes(mem_b as f64); + warn!( + "[Core] SUBPROCESS \"{}\" IS USING {} OF MEMORY.", + process.name(), + mem + ); + if last_mem_dialog + 30000 < get_time() { + let _ = tauri::api::dialog::MessageDialogBuilder::new( + "Possible Memory Leak Detected", + &format!( + "OyasumiVR's subprocess \"{}\" is using {} of memory. This is highly unlikely to happen, and likely indicates the presence of a memory leak. If you see this, please contact a developer.", + process.name(), + mem + ), + ) + .kind(tauri::api::dialog::MessageDialogKind::Error) + .show(|_|{}); + let mut last_mem_dialog_guard = LAST_MEM_DIALOG.lock().await; + *last_mem_dialog_guard = get_time(); + last_mem_dialog = get_time(); + } + } + } +} From d92005de7791c53edef1f2fecb1dafc92e834f91 Mon Sep 17 00:00:00 2001 From: Raphiiko Date: Thu, 21 Dec 2023 00:06:28 +0100 Subject: [PATCH 2/2] Allow memory watcher to be activated manually --- src-core/src/main.rs | 1 + src-core/src/os/commands.rs | 11 +++++++++ src-core/src/os/mod.rs | 7 ++++-- src-ui/app/app.module.ts | 4 ++++ .../settings-advanced-view.component.html | 19 +++++++++++++-- .../settings-advanced-view.component.ts | 23 +++++++++++++++++++ src-ui/assets/i18n/en.json | 15 ++++++++++++ 7 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src-core/src/main.rs b/src-core/src/main.rs index e8f402d4..0d9ceaed 100644 --- a/src-core/src/main.rs +++ b/src-core/src/main.rs @@ -102,6 +102,7 @@ fn configure_command_handlers() -> impl Fn(tauri::Invoke) { os::commands::set_hardware_mic_activity_enabled, os::commands::set_hardware_mic_activivation_threshold, os::commands::is_vrchat_active, + os::commands::activate_memory_watcher, osc::commands::osc_send_bool, osc::commands::osc_send_float, osc::commands::osc_send_int, diff --git a/src-core/src/os/commands.rs b/src-core/src/os/commands.rs index 77f904e0..8d868746 100644 --- a/src-core/src/os/commands.rs +++ b/src-core/src/os/commands.rs @@ -308,3 +308,14 @@ pub async fn set_mic_activity_device_id(device_id: Option) { }; manager.set_mic_activity_device_id(device_id).await; } + +#[tauri::command] +pub async fn activate_memory_watcher() -> bool { + let mut watcher_active_guard = super::MEMORY_WATCHER_ACTIVE.lock().await; + if *watcher_active_guard { + return false; + } + info!("[Core] Activating memory watcher"); + *watcher_active_guard = true; + true +} diff --git a/src-core/src/os/mod.rs b/src-core/src/os/mod.rs index 46a9c69e..d712c670 100644 --- a/src-core/src/os/mod.rs +++ b/src-core/src/os/mod.rs @@ -24,6 +24,7 @@ lazy_static! { static ref SOLOUD: std::sync::Mutex = std::sync::Mutex::new(Soloud::default().unwrap()); static ref AUDIO_DEVICE_MANAGER: Mutex> = Mutex::default(); static ref VRCHAT_ACTIVE: Mutex = Mutex::new(false); + static ref MEMORY_WATCHER_ACTIVE: Mutex = Mutex::new(false); } pub async fn init_audio_device_manager() { @@ -60,8 +61,10 @@ async fn watch_processes() { } } } - // Only monitor memory usage in dev builds, to help find memory leaks - if crate::BUILD_FLAVOUR == crate::flavour::BuildFlavour::Dev { + if { + let watcher_active = MEMORY_WATCHER_ACTIVE.lock().await; + *watcher_active + } { crate::utils::monitor_memory_usage(false).await; } tokio::time::sleep(Duration::from_secs(1)).await; diff --git a/src-ui/app/app.module.ts b/src-ui/app/app.module.ts index e160e9c7..62c5b473 100644 --- a/src-ui/app/app.module.ts +++ b/src-ui/app/app.module.ts @@ -671,6 +671,10 @@ export class AppModule { } // Only initialize update service after language selection await this.updateService.init(); + // Start memory watcher automatically on development builds + if (FLAVOUR === 'DEV') { + await invoke('activate_memory_watcher'); + } } async preloadAssets() { diff --git a/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.html b/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.html index 6ab889d1..9080cd5c 100644 --- a/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.html +++ b/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.html @@ -105,10 +105,10 @@

settings.advanced.persistentData.title

{{ 'settings.advanced.persistentData.dataType.' + item.key + '.title' | translate - }} + }} {{ 'settings.advanced.persistentData.dataType.' + item.key + '.description' | translate - }} + }}
+
+
+ settings.advanced.fixes.memoryWatcher.title + settings.advanced.fixes.memoryWatcher.description +
+
+ +
+
diff --git a/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.ts b/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.ts index c8e601cf..ef850763 100644 --- a/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.ts +++ b/src-ui/app/views/dashboard-view/views/settings-advanced-view/settings-advanced-view.component.ts @@ -32,6 +32,7 @@ import { SetDebugTranslationsRequest } from '../../../../../../src-grpc-web-clie import { OpenVRService } from 'src-ui/app/services/openvr.service'; import { AppSettingsService } from '../../../../services/app-settings.service'; import { OscService } from '../../../../services/osc.service'; +import { FLAVOUR } from '../../../../../build'; @Component({ selector: 'app-settings-advanced-view', @@ -56,6 +57,7 @@ export class SettingsAdvancedViewComponent { { key: 'miscData' }, ]; checkedPersistentStorageItems: string[] = []; + memoryWatcherActive = FLAVOUR === 'DEV'; constructor( private router: Router, @@ -312,4 +314,25 @@ export class SettingsAdvancedViewComponent { }) .subscribe(); } + + async activateMemoryWatcher() { + this.memoryWatcherActive = true; + if (await invoke('activate_memory_watcher')) { + this.modalService + .addModal(ConfirmModalComponent, { + title: 'settings.advanced.fixes.memoryWatcher.modal.success.title', + message: 'settings.advanced.fixes.memoryWatcher.modal.success.message', + showCancel: false, + }) + .subscribe(); + } else { + this.modalService + .addModal(ConfirmModalComponent, { + title: 'settings.advanced.fixes.memoryWatcher.modal.error.title', + message: 'settings.advanced.fixes.memoryWatcher.modal.error.message', + showCancel: false, + }) + .subscribe(); + } + } } diff --git a/src-ui/assets/i18n/en.json b/src-ui/assets/i18n/en.json index d6d76b81..2837ddf2 100644 --- a/src-ui/assets/i18n/en.json +++ b/src-ui/assets/i18n/en.json @@ -1178,6 +1178,21 @@ "message": "The internal OSC- and OSCQuery services have been restarted." } } + }, + "memoryWatcher": { + "title": "Memory Watcher", + "description": "Activate the memory watcher to monitor all child processes. An error dialog will be shown if any processes exceed a gigabyte of memory usage, and extra information will be logged.", + "action": "Activate", + "modal": { + "error": { + "title": "Already Active", + "message": "The memory watcher was already active." + }, + "success": { + "title": "Memory Watcher Activated", + "message": "The memory watcher has been activated!" + } + } } } },