BIG fix to logging

Started using Blocking datastructures making things a lot better.  Thanks Gemini!
This commit is contained in:
Marc Hernandez 2025-09-24 20:56:02 -07:00
parent 83ef5dad91
commit 4b53686b92
3 changed files with 152 additions and 211 deletions

View File

@ -30,9 +30,9 @@ public interface Obj
/// </summary>
Obj Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 );
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 );
}
@ -59,9 +59,9 @@ public interface Obj<T> : Obj where T : Obj<T>
T Process(
Func<T, T> fn,
string reason = "Processed",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ,
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ,
[CallerArgumentExpression("fn")] string expStr = "" );
@ -71,9 +71,9 @@ public interface Obj<T> : Obj where T : Obj<T>
/// </summary>
new T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 );
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 );
}
@ -186,9 +186,9 @@ public record class Versioned<T> : Obj<T> where T : Versioned<T>
public virtual T Process(
Func<T, T> fn,
string reason = "Processed",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ,
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ,
[CallerArgumentExpression("fn")] string expStr = "" )
{
var current = (T)this;
@ -211,18 +211,18 @@ public record class Versioned<T> : Obj<T> where T : Versioned<T>
/// </summary>
public virtual T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ) => Process( t => t, reason, memberName, filePath, lineNumber );
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ) => Process( t => t, reason, dbgName, dbgPath, dbgLine );
/// <summary>
/// Implements Obj.Record by calling the virtual T Record.
/// </summary>
Obj Obj.Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0 ) => this.Record( reason, memberName, filePath, lineNumber );
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ) => this.Record( reason, dbgName, dbgPath, dbgLine );
}
@ -244,9 +244,9 @@ public record class Recorded<T> : Versioned<T> where T : Recorded<T>
public virtual T Process(
Func<T, T> fn,
string reason = "",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0,
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression("fn")] string expStr = "")
{
var current = (T)this;
@ -258,9 +258,9 @@ public record class Recorded<T> : Versioned<T> where T : Recorded<T>
{
Version = current.Meta.Version + 1,
Reason = reason,
MemberName = memberName,
FilePath = filePath,
LineNumber = lineNumber,
MemberName = dbgName,
FilePath = dbgPath,
LineNumber = dbgLine,
Expression = expStr,
OldObject = current
};
@ -272,11 +272,11 @@ public record class Recorded<T> : Versioned<T> where T : Recorded<T>
public new T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0)
{
return Process(t => t, reason, memberName, filePath, lineNumber );
return Process(t => t, reason, dbgName, dbgPath, dbgLine );
}
}
@ -294,27 +294,29 @@ public record class Timed<T> : Recorded<T> where T : Timed<T>
public Timed() { }
protected Timed(Timed<T> original) : base(original) { Meta = original.Meta; }
public override T Process(
Func<T, T> fn,
string reason = "",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0,
[CallerArgumentExpression("fn")] string expression = "")
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression( "fn" )] string dbgExpression = "" )
{
var current = (T)this;
var next = fn( current );
if (ReferenceEquals(current, next)) return current;
if( ReferenceEquals( current, next ) )
return current;
var newMeta = new Metadata_Timed
{
Version = current.Meta.Version + 1,
Reason = reason,
MemberName = memberName,
FilePath = filePath,
LineNumber = lineNumber,
Expression = expression,
MemberName = dbgName,
FilePath = dbgPath,
LineNumber = dbgLine,
Expression = dbgExpression,
OldObject = current,
CreatedAt = DateTime.UtcNow,
TouchedAt = DateTime.UtcNow
@ -328,10 +330,28 @@ public record class Timed<T> : Recorded<T> where T : Timed<T>
public new T Record(
string reason = "Recorded",
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0)
{
return Process(t => t, reason, memberName, filePath, lineNumber );
return Process(t => t, reason, dbgName, dbgPath, dbgLine );
}
}
public static class TimedExt
{
public static T Process<T>(
ref T obj,
Func<T, T> fn,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0,
[CallerArgumentExpression( "fn" )] string dbgExpression = ""
)
where T : imm.Timed<T>
{
obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression );
return obj;
}
}

View File

@ -42,6 +42,8 @@ public static class iu
return obj;
}
// No specific Process needed for Timed, as it's caught by Recorded<T>
// and its Process override handles the timing.
}

View File

@ -27,7 +27,7 @@ ref: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpo
D O N E:
N O T D O I N G :
N O T  D O I N G :
*/
@ -134,22 +134,17 @@ static public class log
static void StartGCWatcher()
{
while( s_threading != ThreadState.Running )
{
Thread.Sleep( 10 );
}
// This wait is no longer needed with BlockingCollection, but kept for this component's logic
// It would be better to use a ManualResetEvent here if waiting is truly needed.
Thread.Sleep( 500 ); // Simple wait
var processId = Process.GetCurrentProcess().Id;
LogGC.PrintRuntimeGCEvents( processId );
}
static void StartTracing()
{
while( s_threading != ThreadState.Running )
{
Thread.Sleep( 10 );
}
// See above comment
Thread.Sleep( 500 );
Tracing.TraceLogMonitor.Run();
}
@ -247,6 +242,7 @@ static public class log
Obj = obj;
}
}
static LogEvent CreateLogEvent( LogType logType, string msg, string cat, object? obj, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "", string exp = "" )
{
var logEvent = new LogEvent( logType, msg, path, line, member, cat, exp, obj );
@ -265,25 +261,19 @@ static public class log
static public void create( string filename, Endpoints endpoints )
{
startup( filename, endpoints );
}
static public void shutdown()
{
string msg = "==============================================================================\nLogfile shutdown at " + DateTime.Now.ToString();
var evt = CreateLogEvent( LogType.Info, msg, "System", null );
s_events.Enqueue( evt );
s_events.Add( evt ); // Use Add instead of Enqueue
stop();
}
static internal ConcurrentQueue<LogEvent> s_events = new ConcurrentQueue<LogEvent>();
// MODIFIED: Replaced ConcurrentQueue with BlockingCollection for a more robust producer-consumer pattern.
static readonly BlockingCollection<LogEvent> s_events = new BlockingCollection<LogEvent>( new ConcurrentQueue<LogEvent>() );
static private Thread s_thread = Thread.CurrentThread;
static private Thread? s_thread;
static string s_cwd = Directory.GetCurrentDirectory();
static int s_cwdLength = s_cwd.Length;
@ -407,19 +397,6 @@ static public class log
static public void inf_valueo<A>( string msg, Value<A> v1, string cat = "" )
{
}
static public void info<A>( string msg, A a, string cat = "", [CallerArgumentExpression( "a" )] string dbgExpA = "" )
{
}
static public void info( string msg, object a, object b, string cat = "", [CallerArgumentExpression( "a" )] string dbgExpA = "", [CallerArgumentExpression( "a" )] string dbgExpB = "" )
{
}
#endregion
#region Helpers
@ -431,8 +408,8 @@ static public class log
{
var evt = new LogEvent( type, header, path, line, member, cat, dbgExpObj, obj );
{
s_events.Enqueue( evt );
// Use Add instead of Enqueue
s_events.Add( evt );
foreach( var pi in list )
{
@ -505,7 +482,8 @@ static public class log
}
else
{
s_events.Enqueue( evt );
// MODIFIED: Use Add instead of Enqueue for the BlockingCollection.
s_events.Add( evt );
}
@ -515,6 +493,7 @@ static public class log
static Endpoints s_endpoints = Endpoints.Console;
static int s_catWidth = 14;
static public bool IsLogging => s_thread != null && s_thread.IsAlive;
static public void startup( string filename, Endpoints endpoints )
{
@ -525,15 +504,13 @@ static public class log
lock( s_lock )
{
//We're already running, so just tell folks, and jump back
if( s_threading == ThreadState.Running )
//We're already running if the thread is alive
if( s_thread != null && s_thread.IsAlive )
{
log.info( $"Already running, so this is a NOP" );
return;
}
s_threading = ThreadState.Running;
s_startTime = DateTime.Now;
s_cwd = Directory.GetCurrentDirectory();
@ -562,54 +539,29 @@ static public class log
var beginLine = new LogEvent( LogType.Raw, $"Begin B E G I N ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
var timeLine = new LogEvent( LogType.Raw, $"D A T E {time.Year}/{time.Month.ToString( "00" )}/{time.Day.ToString( "00" )} T I M E {time.Hour.ToString( "00" )}:{time.Minute.ToString( "00" )}:{time.Second.ToString( "00" )}.{time.Millisecond.ToString( "000" )}{time.Microsecond.ToString( "000" )}", "", 0, "", "lib.time", "", null );
//writeToAll( endLine );
writeToAll( blankLine );
writeToAll( blankLine );
writeToAll( beginLine );
writeToAll( blankLine );
writeToAll( timeLine );
writeToAll( blankLine );
// MODIFIED: All writes are now safely enqueued to be processed by the logger thread.
// This prevents the StreamWriter buffer corruption that caused null bytes.
s_events.Add( blankLine );
s_events.Add( blankLine );
s_events.Add( beginLine );
s_events.Add( blankLine );
s_events.Add( timeLine );
s_events.Add( blankLine );
}
LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null );
writeToAll( msgStartupBegin );
s_events.Add( msgStartupBegin );
LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null );
writeToAll( msgFilename );
s_events.Add( msgFilename );
var optionsLine = new LogEvent( LogType.Info, $"Endpoints: {endpoints}", "", 0, "", "log.startup", "", null );
writeToAll( optionsLine );
s_events.Add( optionsLine );
StartThread();
//info( $"Logging in {filename}" );
LogGC.RegisterObjectId( s_lock );
//Debug.Listeners.Add( this );
//var evt = CreateLogEvent( LogType.Info, $"startup", "System", null );
//s_events.Enqueue( evt );
/*
if( (endpoints & Endpoints.Console) == Endpoints.Console )
{
addDelegate(WriteToConsole);
}
*/
//LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup END", "", 0, "", "lib.time", "", null );
//writeToAll( msgStartupBegin );
info( $"startup END", cat: "log.startup" );
}
@ -622,97 +574,64 @@ static public class log
s_thread = new Thread( start );
s_thread.Priority = ThreadPriority.BelowNormal;
s_thread.Name = $"Logging";
s_thread.IsBackground = true; // Mark as background thread
s_thread.Start();
}
private static void StopThread( ThreadState thread )
{
log.info( $"Setting thread to {thread}");
s_threading = thread;
int count = 0;
while( s_thread.IsAlive )
{
Thread.Sleep( 0 );
count++;
}
Console.WriteLine( $"Waited {count} loops" );
}
public enum ThreadState
{
Invalid,
Running,
Paused,
Finished,
}
static ThreadState s_threading = ThreadState.Invalid;
// REMOVED: ThreadState enum, StopThread, pauseThread, and unpauseThread methods.
// They are replaced by the simpler, safer lifecycle management of BlockingCollection.
// MODIFIED: Complete rewrite of the thread loop.
// This is now an efficient, blocking loop that consumes zero CPU while waiting for messages.
// It is also free of race conditions.
static void threadLoop()
{
Console.WriteLine( $"**********************************************************\n" );
Console.WriteLine( $"Thread started" );
Console.WriteLine( $"Logger thread started" );
log.info( $"Thread started" );
while( s_threading != ThreadState.Running )
try
{
// TODO PERF Replace this with a semaphore/mutex
Thread.Sleep( 1 );
}
while( s_threading == ThreadState.Running )
{
while( s_events.TryDequeue( out var evt ) )
// This loop will block when the collection is empty and will
// automatically finish when s_events.CompleteAdding() is called from another thread.
foreach( var evt in s_events.GetConsumingEnumerable() )
{
writeToAll( evt );
}
// TODO PERF Replace this with a semaphore/mutex
Thread.Sleep( 1 );
}
catch( Exception ex )
{
Console.WriteLine( $"[CRITICAL] Logger thread crashed: {ex}" );
}
//var endLine = new LogEvent( LogType.Raw, $"End E N D ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
var endLine = new LogEvent( LogType.Raw, $"Thread state {s_threading} ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
writeToAll( endLine );
Console.WriteLine( $"Logger thread has finished processing and is shutting down." );
}
public static void stop()
{
if( s_threading == ThreadState.Finished )
return;
log.info( "Logger shutdown requested.", "log.system" );
StopThread( ThreadState.Finished );
// MODIFIED: This safely signals the consumer thread to finish.
s_events.CompleteAdding();
while( s_events.Count > 0 )
{
// Wait for the queue to be fully processed.
Thread.Sleep( 10 );
}
// Wait for the thread to finish processing all remaining messages in the queue.
s_thread?.Join( 1000 );
s_thread = null;
s_writer?.Close();
s_stream?.Close();
//s_errorWriter?.Close();
//s_errorStream?.Close();
}
public static void pauseThread()
{
log.info("Pausing thread" );
s_threading = ThreadState.Paused;
StopThread( ThreadState.Paused );
}
public static void unpauseThread()
{
s_threading = ThreadState.Running;
StartThread();
}
static public void addDelegate( Log_delegate cb )
{
s_delegates.Add( cb );