Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata Added a migration for TimePlayed, just like in #4861 Consolidated ApplicationData's FileSize* properties into one FileSize property Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes Added new value converters for TimeSpans and file sizes for the Avalonia UI Added TimePlayedSortComparer Fixed sort order in LastPlayedSortComparer Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize Fixed crashes caused by SortHelper Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils Replaced SaveModel.GetSizeString() with ValueFormatUtils * Additional ApplicationLibrary changes that got lost in the last commit * Removed unneeded usings * Removed converters as they are no longer needed * Updated comment on FormatDateTime * Removed base10 parameter from ValueFormatUtils FormatFileSize now always returns base 2 values with base 10 units Made ParseFileSize capable of parsing both base 2 and base 10 units * Removed nullable attribute from TimePlayed property Centralized TimePlayed update code into ApplicationMetadata * Changed UpdateTimePlayed() to use TimeSpan logic * Removed JsonIgnore attributes from ApplicationData * Implemented requested format changes * Fixed mistakes in method documentation comments * Made it so the Last Played value "Never" is localized in the Avalonia UI * Implemented suggestions * Remove unused import * Did a comment refinement pass in ValueFormatUtils.cs * Reordered ValueFormatUtils methods and sorted them into #regions * Integrated functionality from #5056 Also removed Logger print from last_played migration code * Implemented suggestions * Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common * common: Respect proper value format convention and use base10 by default This could be discuss again in another issue/PR, for now revert to the previous behavior. Signed-off-by: Mary Guillemard <mary@mary.zone> --------- Signed-off-by: Mary Guillemard <mary@mary.zone> Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Mary Guillemard <mary@mary.zone>
This commit is contained in:
@ -1,85 +0,0 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
class LinuxSystemInfo : SystemInfo
|
||||
{
|
||||
internal LinuxSystemInfo()
|
||||
{
|
||||
string cpuName = GetCpuidCpuName();
|
||||
|
||||
if (cpuName == null)
|
||||
{
|
||||
var cpuDict = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["model name"] = null,
|
||||
["Processor"] = null,
|
||||
["Hardware"] = null,
|
||||
};
|
||||
|
||||
ParseKeyValues("/proc/cpuinfo", cpuDict);
|
||||
|
||||
cpuName = cpuDict["model name"] ?? cpuDict["Processor"] ?? cpuDict["Hardware"] ?? "Unknown";
|
||||
}
|
||||
|
||||
var memDict = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["MemTotal"] = null,
|
||||
["MemAvailable"] = null,
|
||||
};
|
||||
|
||||
ParseKeyValues("/proc/meminfo", memDict);
|
||||
|
||||
// Entries are in KiB
|
||||
ulong.TryParse(memDict["MemTotal"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong totalKiB);
|
||||
ulong.TryParse(memDict["MemAvailable"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong availableKiB);
|
||||
|
||||
CpuName = $"{cpuName} ; {LogicalCoreCount} logical";
|
||||
RamTotal = totalKiB * 1024;
|
||||
RamAvailable = availableKiB * 1024;
|
||||
}
|
||||
|
||||
private static void ParseKeyValues(string filePath, Dictionary<string, string> itemDict)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"File \"{filePath}\" not found");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int count = itemDict.Count;
|
||||
|
||||
using StreamReader file = new(filePath);
|
||||
|
||||
string line;
|
||||
while ((line = file.ReadLine()) != null)
|
||||
{
|
||||
string[] kvPair = line.Split(':', 2, StringSplitOptions.TrimEntries);
|
||||
|
||||
if (kvPair.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = kvPair[0];
|
||||
|
||||
if (itemDict.TryGetValue(key, out string value) && value == null)
|
||||
{
|
||||
itemDict[key] = kvPair[1];
|
||||
|
||||
if (--count <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
partial class MacOSSystemInfo : SystemInfo
|
||||
{
|
||||
internal MacOSSystemInfo()
|
||||
{
|
||||
if (SysctlByName("kern.osversion", out string buildRevision) != 0)
|
||||
{
|
||||
buildRevision = "Unknown Build";
|
||||
}
|
||||
|
||||
OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})";
|
||||
|
||||
string cpuName = GetCpuidCpuName();
|
||||
|
||||
if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0)
|
||||
{
|
||||
cpuName = "Unknown";
|
||||
}
|
||||
|
||||
ulong totalRAM = 0;
|
||||
|
||||
if (SysctlByName("hw.memsize", ref totalRAM) != 0) // Bytes
|
||||
{
|
||||
totalRAM = 0;
|
||||
}
|
||||
|
||||
CpuName = $"{cpuName} ; {LogicalCoreCount} logical";
|
||||
RamTotal = totalRAM;
|
||||
RamAvailable = GetVMInfoAvailableMemory();
|
||||
}
|
||||
|
||||
static ulong GetVMInfoAvailableMemory()
|
||||
{
|
||||
var port = mach_host_self();
|
||||
|
||||
uint pageSize = 0;
|
||||
var result = host_page_size(port, ref pageSize);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_page_size() error = {result}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int Flavor = 4; // HOST_VM_INFO64
|
||||
uint count = (uint)(Marshal.SizeOf<VMStatistics64>() / sizeof(int)); // HOST_VM_INFO64_COUNT
|
||||
VMStatistics64 stats = new();
|
||||
result = host_statistics64(port, Flavor, ref stats, ref count);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_statistics64() error = {result}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (ulong)(stats.FreeCount + stats.InactiveCount) * pageSize;
|
||||
}
|
||||
|
||||
private const string SystemLibraryName = "libSystem.dylib";
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, IntPtr oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize);
|
||||
|
||||
private static int SysctlByName(string name, IntPtr oldValue, ref ulong oldSize)
|
||||
{
|
||||
if (sysctlbyname(name, oldValue, ref oldSize, IntPtr.Zero, 0) == -1)
|
||||
{
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, $"Cannot retrieve '{name}'. Error Code {err}");
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int SysctlByName<T>(string name, ref T oldValue)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
ulong oldValueSize = (ulong)Unsafe.SizeOf<T>();
|
||||
|
||||
return SysctlByName(name, (IntPtr)Unsafe.AsPointer(ref oldValue), ref oldValueSize);
|
||||
}
|
||||
}
|
||||
|
||||
private static int SysctlByName(string name, out string oldValue)
|
||||
{
|
||||
oldValue = default;
|
||||
|
||||
ulong strSize = 0;
|
||||
|
||||
int res = SysctlByName(name, IntPtr.Zero, ref strSize);
|
||||
|
||||
if (res == 0)
|
||||
{
|
||||
byte[] rawData = new byte[strSize];
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* rawDataPtr = rawData)
|
||||
{
|
||||
res = SysctlByName(name, (IntPtr)rawDataPtr, ref strSize);
|
||||
}
|
||||
|
||||
if (res == 0)
|
||||
{
|
||||
oldValue = Encoding.ASCII.GetString(rawData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial uint mach_host_self();
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial int host_page_size(uint host, ref uint out_page_size);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
struct VMStatistics64
|
||||
{
|
||||
public uint FreeCount;
|
||||
public uint ActiveCount;
|
||||
public uint InactiveCount;
|
||||
public uint WireCount;
|
||||
public ulong ZeroFillCount;
|
||||
public ulong Reactivations;
|
||||
public ulong Pageins;
|
||||
public ulong Pageouts;
|
||||
public ulong Faults;
|
||||
public ulong CowFaults;
|
||||
public ulong Lookups;
|
||||
public ulong Hits;
|
||||
public ulong Purges;
|
||||
public uint PurgeableCount;
|
||||
public uint SpeculativeCount;
|
||||
public ulong Decompressions;
|
||||
public ulong Compressions;
|
||||
public ulong Swapins;
|
||||
public ulong Swapouts;
|
||||
public uint CompressorPageCount;
|
||||
public uint ThrottledCount;
|
||||
public uint ExternalPageCount;
|
||||
public uint InternalPageCount;
|
||||
public ulong TotalUncompressedPagesInCompressor;
|
||||
}
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial int host_statistics64(uint hostPriv, int hostFlavor, ref VMStatistics64 hostInfo64Out, ref uint hostInfo64OutCnt);
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
{
|
||||
public class SystemInfo
|
||||
{
|
||||
public string OsDescription { get; protected set; }
|
||||
public string CpuName { get; protected set; }
|
||||
public ulong RamTotal { get; protected set; }
|
||||
public ulong RamAvailable { get; protected set; }
|
||||
protected static int LogicalCoreCount => Environment.ProcessorCount;
|
||||
|
||||
protected SystemInfo()
|
||||
{
|
||||
OsDescription = $"{RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})";
|
||||
CpuName = "Unknown";
|
||||
}
|
||||
|
||||
private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB";
|
||||
|
||||
public void Print()
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
|
||||
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
|
||||
Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}");
|
||||
}
|
||||
|
||||
public static SystemInfo Gather()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return new WindowsSystemInfo();
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return new LinuxSystemInfo();
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return new MacOSSystemInfo();
|
||||
}
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, "SystemInfo unsupported on this platform");
|
||||
|
||||
return new SystemInfo();
|
||||
}
|
||||
|
||||
// x86 exposes a 48 byte ASCII "CPU brand" string via CPUID leaves 0x80000002-0x80000004.
|
||||
internal static string GetCpuidCpuName()
|
||||
{
|
||||
if (!X86Base.IsSupported)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if CPU supports the query
|
||||
if ((uint)X86Base.CpuId(unchecked((int)0x80000000), 0).Eax < 0x80000004)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int[] regs = new int[12];
|
||||
|
||||
for (uint i = 0; i < 3; ++i)
|
||||
{
|
||||
(regs[4 * i], regs[4 * i + 1], regs[4 * i + 2], regs[4 * i + 3]) = X86Base.CpuId((int)(0x80000002 + i), 0);
|
||||
}
|
||||
|
||||
string name = Encoding.ASCII.GetString(MemoryMarshal.Cast<int, byte>(regs)).Replace('\0', ' ').Trim();
|
||||
|
||||
return string.IsNullOrEmpty(name) ? null : name;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
partial class WindowsSystemInfo : SystemInfo
|
||||
{
|
||||
internal WindowsSystemInfo()
|
||||
{
|
||||
CpuName = $"{GetCpuidCpuName() ?? GetCpuNameWMI()} ; {LogicalCoreCount} logical"; // WMI is very slow
|
||||
(RamTotal, RamAvailable) = GetMemoryStats();
|
||||
}
|
||||
|
||||
private static (ulong Total, ulong Available) GetMemoryStats()
|
||||
{
|
||||
MemoryStatusEx memStatus = new();
|
||||
if (GlobalMemoryStatusEx(ref memStatus))
|
||||
{
|
||||
return (memStatus.TotalPhys, memStatus.AvailPhys); // Bytes
|
||||
}
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, $"GlobalMemoryStatusEx failed. Error {Marshal.GetLastWin32Error():X}");
|
||||
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
private static string GetCpuNameWMI()
|
||||
{
|
||||
ManagementObjectCollection cpuObjs = GetWMIObjects("root\\CIMV2", "SELECT * FROM Win32_Processor");
|
||||
|
||||
if (cpuObjs != null)
|
||||
{
|
||||
foreach (var cpuObj in cpuObjs)
|
||||
{
|
||||
return cpuObj["Name"].ToString().Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER").Trim();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MemoryStatusEx
|
||||
{
|
||||
public uint Length;
|
||||
public uint MemoryLoad;
|
||||
public ulong TotalPhys;
|
||||
public ulong AvailPhys;
|
||||
public ulong TotalPageFile;
|
||||
public ulong AvailPageFile;
|
||||
public ulong TotalVirtual;
|
||||
public ulong AvailVirtual;
|
||||
public ulong AvailExtendedVirtual;
|
||||
|
||||
public MemoryStatusEx()
|
||||
{
|
||||
Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
|
||||
}
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer);
|
||||
|
||||
private static ManagementObjectCollection GetWMIObjects(string scope, string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new ManagementObjectSearcher(scope, query).Get();
|
||||
}
|
||||
catch (PlatformNotSupportedException ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user