-
-
Notifications
You must be signed in to change notification settings - Fork 2
fix: performance — WMI caching, allocation-free stats, tray re-entrancy guard #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,22 +9,47 @@ | |
|
|
||
| /// <summary> | ||
| /// Collects system information via WMI / CIM (no PowerShell spawn needed). | ||
| /// PERF-M1: Static data (OS caption, CPU name, disk models) is cached on first | ||
| /// query and reused — only dynamic data (CPU load, RAM, uptime) is refreshed. | ||
| /// </summary> | ||
| public sealed class SystemInfoService | ||
| { | ||
| // Cached static data — queried once, never changes during app lifetime. | ||
| private OsInfo? _cachedOs; | ||
| private CpuInfo? _cachedCpuStatic; | ||
| private List<DiskInfo>? _cachedDisks; | ||
| private readonly object _cacheLock = new(); | ||
|
|
||
| public Task<SystemSnapshot> CaptureAsync(CancellationToken ct = default) | ||
| => Task.Run(() => Capture(), ct); | ||
|
|
||
| private SystemSnapshot Capture() | ||
| { | ||
| var os = QueryOs(); | ||
| var cpu = QueryCpu(); | ||
| OsInfo os; | ||
| CpuInfo cpu; | ||
| List<DiskInfo> disks; | ||
|
|
||
| lock (_cacheLock) | ||
| { | ||
| // Static OS info (caption, version, build, arch) — cache it. | ||
| // Uptime is dynamic, so we always refresh it. | ||
| _cachedOs ??= QueryOsStatic(); | ||
| os = _cachedOs with { Uptime = QueryUptime() }; | ||
|
|
||
| // CPU name/cores/threads/clock are static; only LoadPercentage is dynamic. | ||
| _cachedCpuStatic ??= QueryCpuStatic(); | ||
| cpu = _cachedCpuStatic with { LoadPercent = QueryCpuLoad() }; | ||
|
|
||
| // Disk info is static (models don't change at runtime). | ||
| _cachedDisks ??= QueryDisks(); | ||
| disks = _cachedDisks; | ||
| } | ||
|
|
||
| var mem = QueryMemory(); | ||
| var disks = QueryDisks(); | ||
| return new SystemSnapshot(os, cpu, mem, disks, DateTime.Now); | ||
| } | ||
|
|
||
| private static OsInfo QueryOs() | ||
| private static OsInfo QueryOsStatic() | ||
| { | ||
| using var searcher = new ManagementObjectSearcher("SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem"); | ||
| using var osCollection = searcher.Get(); | ||
|
|
@@ -48,9 +73,29 @@ | |
| return new OsInfo("Windows", "", "", TimeSpan.Zero, ""); | ||
| } | ||
|
|
||
| private static CpuInfo QueryCpu() | ||
| private static TimeSpan QueryUptime() | ||
| { | ||
| using var searcher = new ManagementObjectSearcher("SELECT Name,NumberOfCores,NumberOfLogicalProcessors,MaxClockSpeed,LoadPercentage FROM Win32_Processor"); | ||
| using var searcher = new ManagementObjectSearcher("SELECT LastBootUpTime FROM Win32_OperatingSystem"); | ||
| using var osCollection = searcher.Get(); | ||
| foreach (ManagementObject mo in osCollection) | ||
| { | ||
| using (mo) | ||
| { | ||
| var lastBootRaw = mo["LastBootUpTime"]?.ToString(); | ||
| if (!string.IsNullOrEmpty(lastBootRaw)) | ||
| { | ||
| try { return DateTime.Now - ManagementDateTimeConverter.ToDateTime(lastBootRaw); } | ||
| catch (FormatException) { } | ||
Check noticeCode scanning / CodeQL Poor error handling: empty catch block Note
Poor error handling: empty catch block.
|
||
|
|
||
| catch (InvalidCastException) { } | ||
Check noticeCode scanning / CodeQL Poor error handling: empty catch block Note
Poor error handling: empty catch block.
|
||
|
|
||
| } | ||
| } | ||
| } | ||
| return TimeSpan.Zero; | ||
| } | ||
|
|
||
| private static CpuInfo QueryCpuStatic() | ||
| { | ||
| using var searcher = new ManagementObjectSearcher("SELECT Name,NumberOfCores,NumberOfLogicalProcessors,MaxClockSpeed FROM Win32_Processor"); | ||
| using var cpuCollection = searcher.Get(); | ||
| foreach (ManagementObject mo in cpuCollection) | ||
| { | ||
|
|
@@ -61,12 +106,26 @@ | |
| Convert.ToUInt32(mo["NumberOfCores"] ?? 0u), | ||
| Convert.ToUInt32(mo["NumberOfLogicalProcessors"] ?? 0u), | ||
| Convert.ToUInt32(mo["MaxClockSpeed"] ?? 0u), | ||
| Convert.ToDouble(mo["LoadPercentage"] ?? 0.0)); | ||
| 0); | ||
| } | ||
| } | ||
| return new CpuInfo("Unknown", 0, 0, 0, 0); | ||
| } | ||
|
|
||
| private static double QueryCpuLoad() | ||
| { | ||
| using var searcher = new ManagementObjectSearcher("SELECT LoadPercentage FROM Win32_Processor"); | ||
| using var cpuCollection = searcher.Get(); | ||
| foreach (ManagementObject mo in cpuCollection) | ||
| { | ||
| using (mo) | ||
| { | ||
| return Convert.ToDouble(mo["LoadPercentage"] ?? 0.0); | ||
| } | ||
| } | ||
| return 0; | ||
|
Comment on lines
+115
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The WMI class Win32_Processor returns one instance per physical processor package (socket) [1][2][3]. The LoadPercentage property represents the load capacity of that specific physical processor, averaged over the last second [4][2]. Regarding system-wide CPU usage, you should not simply aggregate or average the LoadPercentage values from all Win32_Processor instances to determine total system CPU usage [5]. This approach is often inaccurate because Win32_Processor does not reliably report load across all modern multi-core or multi-threaded architectures [5]. For accurate system-wide CPU usage, it is recommended to use performance counter classes, such as Win32_PerfFormattedData_Counters_ProcessorInformation (specifically the _Total instance) or the Win32_PerfFormattedData_PerfOS_Processor class [1][6][5]. These classes are designed to provide aggregated performance data that correctly reflects the total system load [1][6]. Citations:
Consider using performance counters instead of Win32_Processor for system-wide CPU load. Win32_Processor returns one instance per physical processor package, so this implementation reads only the first socket and misses data on multi-socket systems. However, aggregating LoadPercentage across Win32_Processor instances is not recommended—this approach is unreliable on modern multi-core architectures. Use 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private static MemoryInfo QueryMemory() | ||
| { | ||
| double totalKb = 0, freeKb = 0; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: laurentiu021/SystemManager
Length of output: 12110
🏁 Script executed:
Repository: laurentiu021/SystemManager
Length of output: 11614
Defensive copy required:
_cachedDisksis returned without cloning.Lines 44–45 directly reference the cached list without creating a defensive copy. Unlike
OsInfoandCpuInfo(which use recordwithfor cloning),List<DiskInfo>is mutable. If a consumer mutatessnap.Disks, the cache is corrupted for subsequent snapshots.Change line 45 to:
Alternatively, update
SystemSnapshot.Disksto useIReadOnlyList<DiskInfo>to enforce immutability at the type level.🤖 Prompt for AI Agents