From 15e4ce8640502d2014123eedc74b3691a4a75c7c Mon Sep 17 00:00:00 2001 From: Xekep Date: Thu, 26 Feb 2026 20:30:01 +0300 Subject: [PATCH] Reduce logger hot-path overhead and sync IO pressure Batch text log flushes, serialize writes under a lock, and avoid expensive caller stack introspection for info/verbose log events. Also remove LINQ First() allocation in SQL failure replay. --- TShockAPI/SqlLog.cs | 26 ++++++++------ TShockAPI/TextLog.cs | 80 ++++++++++++++++++++++++++++---------------- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/TShockAPI/SqlLog.cs b/TShockAPI/SqlLog.cs index 5fd9a2bcf..558b8ef75 100644 --- a/TShockAPI/SqlLog.cs +++ b/TShockAPI/SqlLog.cs @@ -21,7 +21,6 @@ You should have received a copy of the GNU General Public License using System.Data; using System.Diagnostics; using System.Globalization; -using System.Linq; using MySql.Data.MySqlClient; using TShockAPI.DB; using TShockAPI.DB.Queries; @@ -274,15 +273,7 @@ public void Write(string message, TraceLevel level) if (!MayWriteType(level)) return; - var caller = "TShock"; - - var frame = new StackTrace().GetFrame(2); - if (frame != null) - { - var meth = frame.GetMethod(); - if (meth != null && meth.DeclaringType != null) - caller = meth.DeclaringType.Name; - } + var caller = ResolveCaller(level); try { @@ -298,7 +289,7 @@ public void Write(string message, TraceLevel level) var success = true; while (_failures.Count > 0 && success) { - var info = _failures.First(); + var info = _failures[0]; try { @@ -348,6 +339,19 @@ public void Write(string message, TraceLevel level) } } + private static string ResolveCaller(TraceLevel level) + { + if (level >= TraceLevel.Info) + return "TShock"; + + var caller = "TShock"; + var frame = new StackFrame(3, false); + var meth = frame.GetMethod(); + if (meth != null && meth.DeclaringType != null) + caller = meth.DeclaringType.Name; + return caller; + } + public void Dispose() { _backupLog.Dispose(); diff --git a/TShockAPI/TextLog.cs b/TShockAPI/TextLog.cs index ae3bb29e5..bf61665f5 100644 --- a/TShockAPI/TextLog.cs +++ b/TShockAPI/TextLog.cs @@ -30,7 +30,12 @@ namespace TShockAPI public class TextLog : ILog, IDisposable { private readonly bool ClearFile; + private readonly object _writeLock = new object(); + private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1); + private const int FlushBatchSize = 32; private StreamWriter _logWriter; + private DateTime _lastFlushUtc = DateTime.UtcNow; + private int _writesSinceFlush; /// /// File name of the Text log @@ -248,44 +253,63 @@ public void Write(string message, TraceLevel level) { if (!MayWriteType(level)) return; - if (_logWriter is null) + lock (_writeLock) { - _logWriter = new StreamWriter(FileName, !ClearFile); - } + if (_logWriter is null) + { + _logWriter = new StreamWriter(FileName, !ClearFile); + } - var caller = "TShock"; + var caller = ResolveCaller(level); + var logEntry = string.Format("{0} - {1}: {2}: {3}", + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), + caller, level.ToString().ToUpper(), message); - var frame = new StackTrace().GetFrame(2); - if (frame != null) - { - var meth = frame.GetMethod(); - if (meth != null && meth.DeclaringType != null) - caller = meth.DeclaringType.Name; - } + try + { + _logWriter.WriteLine(logEntry); + _writesSinceFlush++; - var logEntry = string.Format("{0} - {1}: {2}: {3}", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), - caller, level.ToString().ToUpper(), message); - try - { - _logWriter.WriteLine(logEntry); - _logWriter.Flush(); - } - catch (ObjectDisposedException) - { - ServerApi.LogWriter.PluginWriteLine(TShock.instance, logEntry, TraceLevel.Error); - Console.WriteLine("Unable to write to log as log has been disposed."); - Console.WriteLine("{0} - {1}: {2}: {3}", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), - caller, level.ToString().ToUpper(), message); + if (level <= TraceLevel.Warning || _writesSinceFlush >= FlushBatchSize || DateTime.UtcNow - _lastFlushUtc >= FlushInterval) + { + _logWriter.Flush(); + _writesSinceFlush = 0; + _lastFlushUtc = DateTime.UtcNow; + } + } + catch (ObjectDisposedException) + { + ServerApi.LogWriter.PluginWriteLine(TShock.instance, logEntry, TraceLevel.Error); + Console.WriteLine("Unable to write to log as log has been disposed."); + Console.WriteLine("{0} - {1}: {2}: {3}", + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), + caller, level.ToString().ToUpper(), message); + } } } + private static string ResolveCaller(TraceLevel level) + { + if (level >= TraceLevel.Info) + return "TShock"; + + var caller = "TShock"; + var frame = new StackFrame(3, false); + var meth = frame.GetMethod(); + if (meth != null && meth.DeclaringType != null) + caller = meth.DeclaringType.Name; + return caller; + } + public void Dispose() { - if (_logWriter != null) + lock (_writeLock) { - _logWriter.Dispose(); + if (_logWriter != null) + { + _logWriter.Flush(); + _logWriter.Dispose(); + } } } }