From 1197ca63efc9d8029cf53dddd90832be5be7e939 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 25 Feb 2026 19:29:46 +0100 Subject: [PATCH] Cache user lang determination --- .../RustAvailabilityMonitorService.cs | 2 +- .../Tools/Services/RustService.OS.cs | 33 +++++++-- .../Tools/Services/RustService.cs | 3 + .../wwwroot/changelog/v26.3.1.md | 3 + runtime/src/environment.rs | 73 ++++++++++++++----- 5 files changed, 88 insertions(+), 26 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Services/RustAvailabilityMonitorService.cs b/app/MindWork AI Studio/Tools/Services/RustAvailabilityMonitorService.cs index 40c22f0f..e4026fd3 100644 --- a/app/MindWork AI Studio/Tools/Services/RustAvailabilityMonitorService.cs +++ b/app/MindWork AI Studio/Tools/Services/RustAvailabilityMonitorService.cs @@ -100,7 +100,7 @@ public sealed class RustAvailabilityMonitorService : BackgroundService, IMessage { try { - await this.rustService.ReadUserLanguage(); + await this.rustService.ReadUserLanguage(forceRequest: true); } catch (Exception e) { diff --git a/app/MindWork AI Studio/Tools/Services/RustService.OS.cs b/app/MindWork AI Studio/Tools/Services/RustService.OS.cs index 215b3a02..0b81ccfe 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.OS.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.OS.cs @@ -2,15 +2,34 @@ public sealed partial class RustService { - public async Task ReadUserLanguage() + public async Task ReadUserLanguage(bool forceRequest = false) { - var response = await this.http.GetAsync("/system/language"); - if (!response.IsSuccessStatusCode) + if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserLanguage)) + return this.cachedUserLanguage; + + await this.userLanguageLock.WaitAsync(); + try { - this.logger!.LogError($"Failed to read the user language from Rust: '{response.StatusCode}'"); - return string.Empty; + if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserLanguage)) + return this.cachedUserLanguage; + + var response = await this.http.GetAsync("/system/language"); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to read the user language from Rust: '{response.StatusCode}'"); + return string.Empty; + } + + var userLanguage = (await response.Content.ReadAsStringAsync()).Trim(); + if (string.IsNullOrWhiteSpace(userLanguage)) + return string.Empty; + + this.cachedUserLanguage = userLanguage; + return userLanguage; + } + finally + { + this.userLanguageLock.Release(); } - - return await response.Content.ReadAsStringAsync(); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.cs b/app/MindWork AI Studio/Tools/Services/RustService.cs index 5d4e2b08..9f495adb 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.cs @@ -17,6 +17,7 @@ public sealed partial class RustService : BackgroundService private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService)); private readonly HttpClient http; + private readonly SemaphoreSlim userLanguageLock = new(1, 1); private readonly JsonSerializerOptions jsonRustSerializerOptions = new() { @@ -29,6 +30,7 @@ public sealed partial class RustService : BackgroundService private ILogger? logger; private Encryption? encryptor; + private string? cachedUserLanguage; private readonly string apiPort; private readonly string certificateFingerprint; @@ -88,6 +90,7 @@ public sealed partial class RustService : BackgroundService public override void Dispose() { this.http.Dispose(); + this.userLanguageLock.Dispose(); base.Dispose(); } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index d0cfc3a3..0ca17e5c 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1 +1,4 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) + +- Improved the performance by caching the OS language detection and requesting the user language only once per app start. +- Improved the user-language logging by limiting language detection logs to a single entry per app start. \ No newline at end of file diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index c5f0a6c7..a1477269 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -15,6 +15,9 @@ pub static DATA_DIRECTORY: OnceLock = OnceLock::new(); /// The config directory where the application stores its configuration. pub static CONFIG_DIRECTORY: OnceLock = OnceLock::new(); +/// The user language cached once per runtime process. +static USER_LANGUAGE: OnceLock = OnceLock::new(); + /// Returns the config directory. #[get("/system/directories/config")] pub fn get_config_directory(_token: APIToken) -> String { @@ -87,12 +90,11 @@ fn normalize_locale_tag(locale: &str) -> Option { } #[cfg(target_os = "linux")] -fn read_locale_from_environment() -> Option { +fn read_locale_from_environment() -> Option<(String, &'static str)> { if let Ok(language) = env::var("LANGUAGE") { for candidate in language.split(':') { if let Some(locale) = normalize_locale_tag(candidate) { - info!("Detected user language from Linux environment variable 'LANGUAGE': '{}'.", locale); - return Some(locale); + return Some((locale, "LANGUAGE")); } } } @@ -100,8 +102,7 @@ fn read_locale_from_environment() -> Option { for key in ["LC_ALL", "LC_MESSAGES", "LANG"] { if let Ok(value) = env::var(key) { if let Some(locale) = normalize_locale_tag(&value) { - info!("Detected user language from Linux environment variable '{}': '{}'.", key, locale); - return Some(locale); + return Some((locale, key)); } } } @@ -110,10 +111,35 @@ fn read_locale_from_environment() -> Option { } #[cfg(not(target_os = "linux"))] -fn read_locale_from_environment() -> Option { +fn read_locale_from_environment() -> Option<(String, &'static str)> { None } +enum LanguageDetectionSource { + SysLocale, + LinuxEnvironmentVariable(&'static str), + DefaultLanguage, +} + +fn detect_user_language() -> (String, LanguageDetectionSource) { + if let Some(locale) = get_locale() { + if let Some(normalized_locale) = normalize_locale_tag(&locale) { + return (normalized_locale, LanguageDetectionSource::SysLocale); + } + + warn!("sys-locale returned an unusable locale value: '{}'.", locale); + } + + if let Some((locale, key)) = read_locale_from_environment() { + return (locale, LanguageDetectionSource::LinuxEnvironmentVariable(key)); + } + + ( + String::from(DEFAULT_LANGUAGE), + LanguageDetectionSource::DefaultLanguage, + ) +} + #[cfg(test)] mod tests { use super::normalize_locale_tag; @@ -137,21 +163,32 @@ mod tests { #[get("/system/language")] pub fn read_user_language(_token: APIToken) -> String { - if let Some(locale) = get_locale() { - if let Some(normalized_locale) = normalize_locale_tag(&locale) { - info!("Detected user language from sys-locale: '{}'.", normalized_locale); - return normalized_locale; - } + USER_LANGUAGE + .get_or_init(|| { + let (user_language, source) = detect_user_language(); + match source { + LanguageDetectionSource::SysLocale => { + info!("Detected user language from sys-locale: '{}'.", user_language); + }, - warn!("sys-locale returned an unusable locale value: '{}'.", locale); - } + LanguageDetectionSource::LinuxEnvironmentVariable(key) => { + info!( + "Detected user language from Linux environment variable '{}': '{}'.", + key, user_language + ); + }, - if let Some(locale) = read_locale_from_environment() { - return locale; - } + LanguageDetectionSource::DefaultLanguage => { + warn!( + "Could not determine the system language. Use default '{}'.", + DEFAULT_LANGUAGE + ); + }, + } - warn!("Could not determine the system language. Use default '{}'.", DEFAULT_LANGUAGE); - String::from(DEFAULT_LANGUAGE) + user_language + }) + .clone() } #[get("/system/enterprise/config/id")]