Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ set (DATADIR "${CMAKE_INSTALL_PREFIX}/share")
set (PKGDATADIR "${DATADIR}/dm_logger")
set (GETTEXT_PACKAGE "dm_logger")
set (CMAKE_C_FLAGS "-g -O3")

# Required so MinGW's <time.h> exposes the inline localtime_r/gmtime_r
# shims (they forward to MSVC's localtime_s under the hood). On glibc
# this macro is a no-op -- localtime_r is always declared there.
# Verified against x86_64-w64-mingw32-gcc 9.2.0; older MinGW-w64
# versions (pre-v4.0, ~2015) will fail loudly with
# "undefined reference to localtime_r" on the first cross-build.
add_definitions(-D_POSIX_THREAD_SAFE_FUNCTIONS=1)
set (PREFIX ${CMAKE_INSTALL_PREFIX})
set (DOLLAR "$")

Expand Down
84 changes: 82 additions & 2 deletions src/dm_logger.vala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,85 @@ namespace DMLogger

public static Logger log;

/* ----- Fast timestamp formatting (perf hot path) -----
*
* The previous implementation built a fresh GTimeZone + GDateTime
* for every log entry. Profiling (callgrind.out.61062) showed that
* this single line was responsible for ~30 % of total docTYPE
* runtime (9.7 G of 28.8 G instructions), because
* g_time_zone_new("local") parses /etc/localtime on every call.
*
* We replace it with libc's localtime_r + strftime, and cache the
* formatted "YYYY-MM-DD HH:MM:SS" prefix per whole second so that
* bursts of log entries in the same second skip strftime entirely.
*
* Thread-safety: this helper is only called from
* LogEntry.print_out, which in turn runs from the single dedicated
* logger writer thread (Logger.run) and from the not-threaded
* fallback path. Both contexts are single-threaded, so the
* function-local statics need no locking.
*/

[CCode (cname = "localtime_r", cheader_filename = "time.h")]
private extern unowned Posix.tm? _dm_logger_localtime_r( ref time_t timep, out Posix.tm result );

[CCode (cname = "strftime", cheader_filename = "time.h")]
private extern size_t _dm_logger_strftime( char* s, size_t max, string format, Posix.tm* tm );

/* Per-second cache backing format_log_timestamp().
* int64.MIN is impossible for a real timestamp (would be the year
* ~-292277, far before the UNIX epoch), so it is a safe
* "never matched yet" sentinel.
*
* These are namespace-private globals because Vala does not support
* function-local static variables. Their single-threaded use is
* documented in the comment block above format_log_timestamp(). */
private int64 _dm_logger_ts_cached_seconds = int64.MIN;
private char _dm_logger_ts_cached_prefix[20];

/**
* Formats the timestamp portion of a log line into the caller-supplied
* buffer as "YYYY-MM-DD HH:MM:SS" (19 chars + trailing NUL).
*
* @param tstamp_usec Wall-clock time in microseconds since the UNIX epoch
* (same units as LogEntry.tstamp).
* @param buf Caller-allocated buffer; must be at least 20 bytes.
* On success it contains a NUL-terminated string.
*
* Single-threaded use only; see comment block above.
*/
public void format_log_timestamp( int64 tstamp_usec, char[] buf )
{
Comment on lines +89 to +105
int64 seconds = tstamp_usec / (int64)1000000;

if ( seconds != _dm_logger_ts_cached_seconds )
{
time_t t = (time_t)seconds;
Posix.tm tm_local = Posix.tm();
if ( _dm_logger_localtime_r( ref t, out tm_local ) != null )
{
size_t n = _dm_logger_strftime( (char*)_dm_logger_ts_cached_prefix, 20,
"%Y-%m-%d %H:%M:%S", &tm_local );
if ( n == 0 )
{
/* Cold fallback: strftime ran out of room (impossible with
* our 20-byte buffer for this format, but be safe). */
GLib.Memory.copy( _dm_logger_ts_cached_prefix, "????-??-?? ??:??:??\0".data, 20 );
}
}
else
{
/* localtime_r failed (only happens for absurd time_t values
* on 32-bit builds). Emit a placeholder so the logger never
* blocks the writer thread on this. */
GLib.Memory.copy( _dm_logger_ts_cached_prefix, "????-??-?? ??:??:??\0".data, 20 );
}
_dm_logger_ts_cached_seconds = seconds;
}

GLib.Memory.copy( buf, _dm_logger_ts_cached_prefix, 20 );
}

/* Ab diesem Trace-Level soll geloggt werden */
public int log_trace_level = 0;

Expand Down Expand Up @@ -434,8 +513,9 @@ namespace DMLogger
out_stream.printf( "INFO " );
}

DMDateTime dt = new DMDateTime.from_unix_local( (int64)( this.tstamp / (int64)1000000 ) );
out_stream.printf( "[%s.%06lld] ", dt.format( "%F %H:%M:%S" ), (int64)( this.tstamp % (int64)1000000 ) );
char ts_buf[20];
format_log_timestamp( this.tstamp, ts_buf );
out_stream.printf( "[%s.%06lld] ", (string)ts_buf, (int64)( this.tstamp % (int64)1000000 ) );

if ( debug_mode == true )
{
Expand Down
61 changes: 61 additions & 0 deletions tests/test_dm_logger.vala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ public class TestDMLogger

ts_dm_logger.add_suite( ts_dm_logger_entry_bin );

/* timestamp formatting */
GLib.TestSuite ts_dm_logger_timestamp = new GLib.TestSuite( "timestamp" );
ts_dm_logger_timestamp.add(
new GLib.TestCase(
"test_f_dm_logger_format_log_timestamp",
TestDMLogger.default_setup,
TestDMLogger.test_dm_logger_format_log_timestamp,
TestDMLogger.default_teardown
)
);
ts_dm_logger.add_suite( ts_dm_logger_timestamp );

GLib.Test.run( );
return 0;

Expand Down Expand Up @@ -115,6 +127,55 @@ public class TestDMLogger
GLib.assert( logger.tid_entry_bin.get( OpenDMLib.gettid( ) ).length == 3 );
GLib.assert( logger.tid_entry_bin.get( thread_id ).length == 1 );
}

/**
* Tests that format_log_timestamp produces the expected
* "YYYY-MM-DD HH:MM:SS" prefix in the caller-supplied buffer
* and that calls with the same whole-second value reuse the
* cached prefix (correctness check: result must still be right).
*
* The check is timezone-independent: we feed a known epoch-second
* value, call format_log_timestamp twice with that same second
* and once with second+1, and verify all three prefixes are
* well-formed and that the first two are byte-equal.
*/
public static void test_dm_logger_format_log_timestamp( )
{
char buf_a[20];
char buf_b[20];
char buf_c[20];

/* 1_700_000_000 seconds == 2023-11-14 22:13:20 UTC.
* Local-time representation depends on TZ, but determinism
* within one run is all we need. */
int64 base_ts = (int64)1700000000 * (int64)1000000; /* microseconds */

DMLogger.format_log_timestamp( base_ts, buf_a );
DMLogger.format_log_timestamp( base_ts + (int64)500000, buf_b ); /* same second */
DMLogger.format_log_timestamp( base_ts + (int64)1500000, buf_c ); /* +1 second */

string s_a = (string)buf_a;
string s_b = (string)buf_b;
string s_c = (string)buf_c;

/* Well-formedness: "YYYY-MM-DD HH:MM:SS" => 19 chars. */
GLib.assert( s_a.length == 19 );
GLib.assert( s_b.length == 19 );
GLib.assert( s_c.length == 19 );

/* Layout: digits and separators at fixed positions. */
GLib.assert( s_a[ 4] == '-' );
GLib.assert( s_a[ 7] == '-' );
GLib.assert( s_a[10] == ' ' );
GLib.assert( s_a[13] == ':' );
GLib.assert( s_a[16] == ':' );

/* Same whole-second => identical prefix. */
GLib.assert( s_a == s_b );

/* +1 second => different prefix (last char differs at minimum). */
GLib.assert( s_a != s_c );
}
}

/**
Expand Down