From a7b9c87926efdd74c2bc806484c7a1b3052c3973 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 31 Jul 2021 04:20:50 +0200 Subject: [PATCH] Save 0.4% Windows server CPU. --- Robust.Server/BaseServer.cs | 43 +--------- .../Console/ISystemConsoleManager.cs | 4 +- Robust.Server/Console/SystemConsoleManager.cs | 77 ++++++++++++++++- .../Console/SystemConsoleManagerDummy.cs | 7 +- Robust.Shared/Utility/ProcessExt.cs | 82 +++++++++++++++++++ 5 files changed, 167 insertions(+), 46 deletions(-) create mode 100644 Robust.Shared/Utility/ProcessExt.cs diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 459e48edac..23dfa90b78 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -94,10 +94,6 @@ namespace Robust.Server private ILogHandler? _logHandler; private IGameLoop _mainLoop = default!; - private TimeSpan _lastTitleUpdate; - private long _lastReceivedBytes; - private long _lastSentBytes; - private string? _shutdownReason; private readonly ManualResetEventSlim _shutdownEvent = new(false); @@ -515,28 +511,6 @@ namespace Robust.Server _mainLoop = gameLoop; } - /// - /// Updates the console window title with performance statistics. - /// - private void UpdateTitle() - { - if (!Environment.UserInteractive || System.Console.IsInputRedirected) - { - return; - } - - // every 1 second update stats in the console window title - if ((_time.RealTime - _lastTitleUpdate).TotalSeconds < 1.0) - return; - - var netStats = UpdateBps(); - System.Console.Title = string.Format("FPS: {0:N2} SD: {1:N2}ms | Net: ({2}) | Memory: {3:N0} KiB", - Math.Round(_time.FramesPerSecondAvg, 2), - _time.RealFrameTimeStdDev.TotalMilliseconds, - netStats, - Process.GetCurrentProcess().PrivateMemorySize64 >> 10); - _lastTitleUpdate = _time.RealTime; - } /// /// Loads the server settings from the ConfigurationManager. @@ -593,22 +567,9 @@ namespace Robust.Server } } - private string UpdateBps() - { - var stats = IoCManager.Resolve().Statistics; - - var bps = - $"Send: {(stats.SentBytes - _lastSentBytes) >> 10:N0} KiB/s, Recv: {(stats.ReceivedBytes - _lastReceivedBytes) >> 10:N0} KiB/s"; - - _lastSentBytes = stats.SentBytes; - _lastReceivedBytes = stats.ReceivedBytes; - - return bps; - } - private void Input(FrameEventArgs args) { - _systemConsole.Update(); + _systemConsole.UpdateInput(); _network.ProcessPackets(); _taskManager.ProcessPendingTasks(); @@ -622,7 +583,7 @@ namespace Robust.Server // These are always the same on the server, there is no prediction. _time.LastRealTick = _time.CurTick; - UpdateTitle(); + _systemConsole.UpdateTick(); using (TickUsage.WithLabels("PreEngine").NewTimer()) { diff --git a/Robust.Server/Console/ISystemConsoleManager.cs b/Robust.Server/Console/ISystemConsoleManager.cs index eb4a67d0ac..5f5627f1b7 100644 --- a/Robust.Server/Console/ISystemConsoleManager.cs +++ b/Robust.Server/Console/ISystemConsoleManager.cs @@ -8,12 +8,14 @@ /// /// process input/output of the console. This needs to be called often. /// - void Update(); + void UpdateInput(); /// /// Prints to the system console. /// /// Text to write to the system console. void Print(string text); + + void UpdateTick(); } } diff --git a/Robust.Server/Console/SystemConsoleManager.cs b/Robust.Server/Console/SystemConsoleManager.cs index ceaf5ccf99..e1d3ea6edd 100644 --- a/Robust.Server/Console/SystemConsoleManager.cs +++ b/Robust.Server/Console/SystemConsoleManager.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Robust.Shared.Asynchronous; using Robust.Shared.IoC; +using Robust.Shared.Network; +using Robust.Shared.Timing; +using Robust.Shared.Utility; using Con = System.Console; namespace Robust.Server.Console @@ -12,6 +16,12 @@ namespace Robust.Server.Console [Dependency] private readonly IServerConsoleHost _conShell = default!; [Dependency] private readonly ITaskManager _taskManager = default!; [Dependency] private readonly IBaseServer _baseServer = default!; + [Dependency] private readonly IServerNetManager _netManager = default!; + [Dependency] private readonly IGameTiming _time = default!; + + // + // Command entry stuff. + // private readonly Dictionary commandHistory = new(); private string currentBuffer = ""; @@ -21,6 +31,20 @@ namespace Robust.Server.Console private int tabCompleteIndex; private ConsoleKey lastKeyPressed = ConsoleKey.NoName; + // + // Title update stuff. + // + + // This is ridiculously expensive to fetch for some reason. + // I'm gonna just assume that this can't change during the lifetime of the process. I hope. + // I want this ridiculous 0.1% CPU usage off my profiler. + private readonly bool _userInteractive = Environment.UserInteractive; + + private TimeSpan _lastTitleUpdate; + private long _lastReceivedBytes; + private long _lastSentBytes; + + public void Dispose() { if (Environment.UserInteractive) @@ -37,7 +61,53 @@ namespace Robust.Server.Console } } - public void Update() + public void UpdateTick() + { + UpdateTitle(); + } + + /// + /// Updates the console window title with performance statistics. + /// + private void UpdateTitle() + { + if (!_userInteractive || System.Console.IsInputRedirected) + { + return; + } + + // every 1 second update stats in the console window title + if ((_time.RealTime - _lastTitleUpdate).TotalSeconds < 1.0) + return; + + var netStats = UpdateBps(); + var process = Process.GetCurrentProcess(); + var privateSize = process.GetPrivateMemorySize64NotSlowHolyFuckingShitMicrosoft(); + + System.Console.WriteLine($"A: {privateSize} B: {process.PrivateMemorySize64}"); + + System.Console.Title = string.Format("FPS: {0:N2} SD: {1:N2}ms | Net: ({2}) | Memory: {3:N0} KiB", + Math.Round(_time.FramesPerSecondAvg, 2), + _time.RealFrameTimeStdDev.TotalMilliseconds, + netStats, + privateSize >> 10); + _lastTitleUpdate = _time.RealTime; + } + + private string UpdateBps() + { + var stats = _netManager.Statistics; + + var bps = + $"Send: {(stats.SentBytes - _lastSentBytes) >> 10:N0} KiB/s, Recv: {(stats.ReceivedBytes - _lastReceivedBytes) >> 10:N0} KiB/s"; + + _lastSentBytes = stats.SentBytes; + _lastReceivedBytes = stats.ReceivedBytes; + + return bps; + } + + public void UpdateInput() { if (Con.IsInputRedirected) { @@ -161,7 +231,7 @@ namespace Robust.Server.Console { var currentLineCursor = Con.CursorTop; Con.SetCursorPosition(0, Con.CursorTop); - Con.Write(new string(' ', Con.WindowWidth-1)); + Con.Write(new string(' ', Con.WindowWidth - 1)); Con.SetCursorPosition(0, currentLineCursor); } @@ -174,7 +244,8 @@ namespace Robust.Server.Console if (tabCompleteList.Count == 0) { - tabCompleteList = _conShell.RegisteredCommands.Keys.Where(key => key.StartsWith(currentBuffer)).ToList(); + tabCompleteList = _conShell.RegisteredCommands.Keys.Where(key => key.StartsWith(currentBuffer)) + .ToList(); if (tabCompleteList.Count == 0) { return String.Empty; diff --git a/Robust.Server/Console/SystemConsoleManagerDummy.cs b/Robust.Server/Console/SystemConsoleManagerDummy.cs index 0c0fe7f881..c19e7d26a7 100644 --- a/Robust.Server/Console/SystemConsoleManagerDummy.cs +++ b/Robust.Server/Console/SystemConsoleManagerDummy.cs @@ -2,7 +2,7 @@ namespace Robust.Server.Console { internal sealed class SystemConsoleManagerDummy : ISystemConsoleManager { - public void Update() + public void UpdateInput() { // Nada. } @@ -11,5 +11,10 @@ namespace Robust.Server.Console { // Nada. } + + public void UpdateTick() + { + // Nada. + } } } diff --git a/Robust.Shared/Utility/ProcessExt.cs b/Robust.Shared/Utility/ProcessExt.cs new file mode 100644 index 0000000000..2db1b202fb --- /dev/null +++ b/Robust.Shared/Utility/ProcessExt.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +// ReSharper disable CommentTypo +#pragma warning disable 649 + +namespace Robust.Shared.Utility +{ + internal static unsafe class ProcessExt + { + // THIS ENTIRE METHOD EXISTS PURELY OUT OF SPITE FOR MICROSOFT. + // NOT TO SAVE 0.3% CPU USAGE ON WINDOWS. NAH. + // TO SPITE MICROSOFT. + // WHAT ABOUT THE 2.2% CPU USAGE IN CONSOLE.KEYAVAILABLE? WHAT ABOUT THAT WHATABOUTISM HUH??? + // I WANTED TO GO TO BED NOW IT'S 4:20 AM (BASED) GOD DAMNIT. + // AS A RESULT, EVERYTHING HERE WILL BE WRITTEN IN *FUCKING* CAPS. + [MethodImpl(MethodImplOptions.NoInlining)] + public static long GetPrivateMemorySize64NotSlowHolyFuckingShitMicrosoft(this Process process) + { + if (!OperatingSystem.IsWindows()) + // IT'S NOT SLOW ON LINUX, *LUCKILY*. + // WELL I DIDN'T PROFILE IT BUT THEY DON'T DO A STUPID SEARCH OVER EVERY PROCESS. + // IT USES PROCFS, IF YOU'RE CURIOUS. + return process.PrivateMemorySize64; + + // + // GLASS TOLD ME TO PUT A TL;DR IN SO TL;DR + // PROCESS.PRIVATEMEMORYSIZE64 (AND A BUNCH OF OTHERS) ARE SLOW AS BALLS (2+ms for me). + // THIS ISN'T. + // + // AS IT *FUCKING* TURNS OUT. + // *MANY* OF THE FIELDS ON PROCESS (THE CLASS, YOU KNOW THE ONE THIS METHOD IS AN EXTENSION FOR) HAVE TO FETCH A "PROCESS INFO". + // HOW DO YOU FETCH A PROCESS INFO ON WINDOWS? OH YEAH YOU CALL NTQUERYSYSTEMINFORMATION APPARENTLY. + // OH WAIT. THAT RETURNS INFO FOR *EVERY* PROCESS ON THE FUCKING SYSTEM. + // SO YES. IF YOU LOOK UP PROCESS.PRIVATEMEMORYSIZE64, IT HAS TO SEARCH THROUGH *EVERY PROCESS ON THE FUCKING SYSTEM* TO FIND THE PROCESS IT'S LOOKING FOR. + // THIS TAKES MORE THAN 2 FUCKING MILLISECONDS PER LOOKUP ON MY SYSTEM. + // IF THE TITLE BAR ON THE CONSOLE WAS UPDATED EVERY TICK INSTEAD OF ONCE PER FRAME, THIS WOULD LITERALLY BE MOST OF THE IDLE SERVER CPU USAGE. + // ***ARE YOU FUCKING KIDDING ME ????*** + // OH YEAH, NTQUERYSYSTEMINFORMATION IS DOCUMENTED AS AN UNSTABLE API THAT CAN CHANGE AT ANY TIME, APPARENTLY. + // .NET FEELS IT'S FINE TO USE THAT (YEAH GUESS THEY'RE NEVER REMOVING IT FROM WINDOWS LMAO) + // ARE YOU FUCKING TELLING ME THE .NET TEAM COULDN'T HAVE WALKED TO THE OTHER SIDE OF THE OFFICE AND TOLD THE KERNEL TEAM TO ADD A VERSION OF NTQUERYSYSTEMINFORMATION THAT FETCHES INFO FOR A SINGLE PROCESS? + // THEY THOUGHT THIS SHIT WAS FUCKING ACCEPTABLE? + // JESUS FUCKING CHRIST. + // ANYWAYS, THIS EXTENSION METHOD USES GETPROCESSMEMORYINFO() INSTEAD BECAUSE THAT'S PROBABLY NOT FUCKING MORONIC. + // + + PROCESS_MEMORY_COUNTERS_EX counters; + + if (GetProcessMemoryInfo(process.Handle, &counters, sizeof(PROCESS_MEMORY_COUNTERS_EX)) == 0) + return 0; + + var count = counters.PrivateUsage; + + return count; + } + + private struct PROCESS_MEMORY_COUNTERS_EX + { + public int cb; + public int PageFaultCount; + public nint PeakWorkingSetSize; + public nint WorkingSetSize; + public nint QuotaPeakPagedPoolUsage; + public nint QuotaPagedPoolUsage; + public nint QuotaPeakNonPagedPoolUsage; + public nint QuotaNonPagedPoolUsage; + public nint PagefileUsage; + public nint PeakPagefileUsage; + public nint PrivateUsage; + } + + // ReSharper disable once StringLiteralTypo + [DllImport("psapi.dll", SetLastError = true)] + private static extern int GetProcessMemoryInfo( + IntPtr Process, + PROCESS_MEMORY_COUNTERS_EX* ppsmemCounters, + int cb); + } +}