Skip to content

Commit

Permalink
Linux/Timeglue: Allow clock base selection via PsychTweak().
Browse files Browse the repository at this point in the history
PsychTweak('GetSecsClock', clockid); now allows to select the main clock,
aka reference clock, aka GetSecs() clock, primarily used for timing and
time stamping in Psychtoolbox by mex files like Screen, PsychPortAudio,
IOPort, PsychHID, GetSecs, WaitSecs, ...

Default is clockid 0, aka wall clock / gettimeofday / CLOCK_REALTIME
clock, for backwards compatibility with PTB on Linux since the year 2007.

clockid 1, aka CLOCK_MONOTONIC, may come in handy to avoid time jumps due
to system admin, leap seconds, NTP (mis-)adjustments/jumps etc. if one
doesn't need UTC time sync across machines. As most of Linux low level
subsystems like graphics and display, ALSA sound, HID input etc. deliver
and expect CLOCK_MONOTONIC times, this can avoid clock remapping.

A few more Posix defined clocks.

-> PsychTweak and Linux time glue have been updated accordingly.

-> Some verification is still missing, but we are defaulting to the
   old clock behaviour, so no worries of regressions.
  • Loading branch information
kleinerm committed Mar 11, 2024
1 parent 04b0e49 commit 32f319f
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,12 @@ void PsychGSCheckInit(const char* engineName)

// Select opmode of GStreamers master clock:
// We use monotonic clock on Windows and OS/X, as these correspond to the
// clocks we use for GetSecs(), but realtime clock on Linux:
// clocks we use for GetSecs(), but realtime clock or monotonic clock on Linux,
// whatever GetSecs uses:
system_clock = gst_system_clock_obtain();
if (system_clock) {
g_object_set(G_OBJECT(system_clock), "clock-type", ((PSYCH_SYSTEM == PSYCH_LINUX) ? GST_CLOCK_TYPE_REALTIME : GST_CLOCK_TYPE_MONOTONIC), NULL);
g_object_set(G_OBJECT(system_clock), "clock-type",
(((PSYCH_SYSTEM == PSYCH_LINUX) && (PsychOSMonotonicToRefTime(0) != 0)) ? GST_CLOCK_TYPE_REALTIME : GST_CLOCK_TYPE_MONOTONIC), NULL);
}
if (PsychPrefStateGet_Verbosity() > 3) printf("PTB-INFO: Using GStreamer version '%s'.\n", (char*) gst_version_string());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,10 @@ void PsychDCUpdateCameraFrameTimestamp(PsychVidcapRecordType* capdev)
capdev->current_pts = nowTimeSecs - (nowCycleTimeSecs + (128 - frameCycleTimeSecs));
}

// On both macOS and Linux our libdc1394 code computes CLOCK_REALTIME / gettimeofday()
// timestamps. Need to convert them to GetSecs reference time:
capdev->current_pts = PsychOSRealtimeToRefTime(capdev->current_pts);

if (PsychPrefStateGet_Verbosity() > 10) {
printf("PTB-INFO: Basler SFF timestamp on device %i is %f secs [raw value %i].\n", capdev->capturehandle, capdev->current_pts, sff_cycletimestamp->cycle_time_stamp.unstructured.value);
}
Expand All @@ -1456,17 +1460,9 @@ void PsychDCUpdateCameraFrameTimestamp(PsychVidcapRecordType* capdev)
// engine with (theroretically) microsecond precision and is assumed to be pretty accurate:
capdev->current_pts = ((double) capdev->frame->timestamp) / 1000000.0f;

// On OS/X, current_pts is in gettimeofday() time, just as on Linux, but PTB's GetSecs
// clock represents host uptime, not gettimeofday() time. Therefore we need to remap
// on OS/X from gettimeofday() time to regular PTB GetSecs() time, via an instant
// clock calibration between both clocks and offset correction:
#if PSYCH_SYSTEM == PSYCH_OSX
struct timeval tv;
double tRef;
gettimeofday(&tv, NULL);
PsychGetAdjustedPrecisionTimerSeconds(&tRef);
capdev->current_pts -= (((double) ((psych_uint64) tv.tv_sec * 1000000 + (psych_uint64) tv.tv_usec)) / 1000000.0f) - tRef;
#endif
// On both macOS and Linux our libdc1394 code computes CLOCK_REALTIME / gettimeofday()
// timestamps. Need to convert them to GetSecs reference time:
capdev->current_pts = PsychOSRealtimeToRefTime(capdev->current_pts);
}
}

Expand Down Expand Up @@ -3417,16 +3413,20 @@ double PsychDCVideoCaptureSetParameter(int capturehandle, const char* pname, dou
if (strstr(pname, "GetCycleTimer")!=0) {
dc1394basler_sff_cycle_time_stamp_t ct;
uint64_t systemtime;
double getsecstime;

err = dc1394_read_cycle_timer(capdev->camera, &(ct.cycle_time_stamp.unstructured.value), &systemtime);
if (err && (err != DC1394_FUNCTION_NOT_SUPPORTED)) PsychErrorExitMsg(PsychError_system, "Failed to query cycle timer!");
if (err == DC1394_FUNCTION_NOT_SUPPORTED) return(oldvalue);

// System time is CLOCK_REALTIME time in microseconds, so convert to GetSecs seconds:
getsecstime = PsychOSRealtimeToRefTime(((double) ((psych_uint64) systemtime)) / 1000000.0f);

// Copy out Firewire bus time converted into seconds:
PsychCopyOutDoubleArg(1, FALSE, PsychDCBusCycleTimeToSecs(ct.cycle_time_stamp.unstructured.value));

// System time is CLOCK_REALTIME time in microseconds, so convert to GetSecs() seconds:
PsychCopyOutDoubleArg(2, FALSE, ((double) ((psych_uint64) systemtime)) / 1000000.0f);
// Copy out GetSecs timestamp:
PsychCopyOutDoubleArg(2, FALSE, getsecstime);

// Copy out Firewire bus time in seconds:
PsychCopyOutDoubleArg(3, FALSE, (double) ct.cycle_time_stamp.structured.second_count);
Expand Down
73 changes: 60 additions & 13 deletions PsychSourceGL/Source/Linux/Base/PsychTimeGlue.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ static psych_bool isKernelTimebaseFrequencyHzInitialized = FALSE;
static double kernelTimebaseFrequencyHz;
static double sleepwait_threshold = 0.001;
static double clockinc = 0;
static clockid_t main_clock = CLOCK_REALTIME;

double PsychWaitUntilSeconds(double whenSecs)
{
Expand Down Expand Up @@ -88,7 +89,7 @@ double PsychWaitUntilSeconds(double whenSecs)
// signals. If clock_nanosleep gets EINTR - Interrupted by a posix signal, we simply loop and restart the
// sleep. If it returns a different error condition, we abort sleep iteration -- something would be seriously
// wrong...
if ((rc = clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &rqtp, NULL)) && (rc != EINTR)) break;
if ((rc = clock_nanosleep(main_clock, TIMER_ABSTIME, &rqtp, NULL)) && (rc != EINTR)) break;

// Update our 'now' time for reiterating or continuing with busy-sleep...
PsychGetPrecisionTimerSeconds(&now);
Expand Down Expand Up @@ -187,6 +188,33 @@ double PsychGetKernelTimebaseFrequencyHz(void)

void PsychInitTimeGlue(void)
{
// Selection of main clock, aka GetSecs() clock, which is used pretty much
// everywhere. Default to CLOCK_REALTIME aka gettimeofday() clock aka
// wall clock, which is what was used on Linux since day one:
main_clock = CLOCK_REALTIME;

// Allow user override via environment variable, to be set via PsychTweak():
if (getenv("PSYCH_GETSECS_CLOCK")) {
main_clock = atoi(getenv("PSYCH_GETSECS_CLOCK"));
switch (main_clock) {
case CLOCK_REALTIME: // 0
case CLOCK_MONOTONIC: // 1
case CLOCK_MONOTONIC_RAW: // 4
case CLOCK_BOOTTIME: // 7
case CLOCK_TAI: // 11
errno = 0;
if (clock_getres(main_clock, NULL) && (errno == EINVAL)) {
printf("PTB-ERROR: Selected clock_id %i for GetSecs and timekeeping unsupported by operating system! Reverting to 0 == CLOCK_REALTIME.\n", main_clock);
main_clock = CLOCK_REALTIME;
}
break;

default:
printf("PTB-ERROR: Tried to select an unsupported clock_id %i for GetSecs and timekeeping! Reverting to default 0 == CLOCK_REALTIME.\n", main_clock);
main_clock = CLOCK_REALTIME;
}
}

// Set this, although its totally pointless on our implementation...
PsychEstimateGetSecsValueAtTickCountZero();
}
Expand Down Expand Up @@ -223,19 +251,33 @@ void PsychGetPrecisionTimerTicksMinimumDelta(psych_uint32 *delta)
struct timespec res;

// We return the real clock tick resolution in microseconds:
clock_getres(CLOCK_REALTIME, &res);
clock_getres(main_clock, &res);
*delta = (psych_uint32) ((((double) res.tv_sec) + ((double) res.tv_nsec / 1e9)) * 1e6);
}

/* CLOCK_REALTIME / gettimeofday() time to Linux GetSecs time. */
double PsychOSRealtimeToRefTime(double t)
{
// TODO FIXME: This only works for CLOCK_MONOTONIC GetSecs main_clock timebase,
// and can be inaccurate! Should use same approach as in PsychOSMonotonicToRefTime()!

// CLOCK_MONOTONIC GetSecs timebase?
if (main_clock == CLOCK_MONOTONIC) {
// Yes. Need to convert from CLOCK_REALTIME / gettimeofday() to CLOCK_MONOTONIC:
t -= PsychGetWallClockSeconds() - PsychOSGetLinuxMonotonicTime();
}

return(t);
}

/* PsychOSGetLinuxMonotonicTime() -- Linux only.
*
* Return CLOCK_MONOTONIC time (usually system uptime) in seconds.
* Return zero on failure.
*
* Some subsystems return time not in gettimeofday() time aka CLOCK_REALTIME time,
* but in CLOCK_MONOTONIC time. In such cases we need to query this time to compute
* proper offsets for remapping into the gettimeofday() timebase which is used
* everywhere in PTB.
* Some subsystems return time not in main_clock time, but in CLOCK_MONOTONIC
* time. In such cases we need to query this time to compute proper offsets for
* remapping into the main_clock timebase which is used everywhere in PTB.
*
* An example is ALSA audio support in PsychPortAudio: ALSA drivers are free to
* return their audio timestamps in CLOCK_REALTIME time or CLOCK_MONOTONIC time,
Expand All @@ -254,14 +296,15 @@ double PsychOSGetLinuxMonotonicTime(void)
* Map given input time value monotonicTime to PTB reference time if
* neccessary, pass-through otherwise.
*
* Can conditionally convert from CLOCK_MONOTONIC time to reftime, e.g.,
* to CLOCK_REALTIME aka gettimeofday().
*
*/
double PsychOSMonotonicToRefTime(double monotonicTime)
{
double now, now2, tMonotonic;

// Short-cut this if main "reference" clock is already CLOCK_MONOTONIC:
if (main_clock == CLOCK_MONOTONIC)
return(monotonicTime);

// Get current reftime:
PsychGetAdjustedPrecisionTimerSeconds(&now);
// Get current CLOCK_MONOTONIC time:
Expand Down Expand Up @@ -300,11 +343,15 @@ double PsychOSMonotonicToRefTime(double monotonicTime)
*
* Map given input PTB reference time to CLOCK_MONOTONIC time.
*
* Iow CLOCK_REALTIME to CLOCK_MONOTONIC time.
* Iow main_clock to CLOCK_MONOTONIC time.
*
*/
double PsychOSRefTimeToMonotonicTime(double refInputTime)
{
// Short-cut this if main "reference" clock is already CLOCK_MONOTONIC:
if (main_clock == CLOCK_MONOTONIC)
return(refInputTime);

double monotonicNowTime = PsychOSGetLinuxMonotonicTime();
double referenceNowTime = PsychOSMonotonicToRefTime(monotonicNowTime);
return(monotonicNowTime + (refInputTime - referenceNowTime));
Expand All @@ -319,7 +366,7 @@ void PsychGetPrecisionTimerSeconds(double *secs)
if (firstTime) {
// We query the real clock tick resolution in secs and store in global clockinc.
// This is useful as a constraint on sleepwait_threshold etc. for our sleep routines...
clock_getres(CLOCK_REALTIME, &res);
clock_getres(main_clock, &res);
clockinc = ((double) res.tv_sec) + ((double) res.tv_nsec / 1.e9);

// sleepwait_threshold should be significantly higher than the granularity of
Expand Down Expand Up @@ -347,10 +394,10 @@ void PsychGetPrecisionTimerSeconds(double *secs)
static double oldss = -1;
double ss;
struct timespec ts;
if (0 != clock_gettime(CLOCK_REALTIME, &ts)) {
if (0 != clock_gettime(main_clock, &ts)) {
// This error is basically impossible, but for beauty points we check for it anyway:
ss = 0;
printf("PTB-CRITICAL_ERROR: clock_gettime(%i) failed!!\n", CLOCK_REALTIME);
printf("PTB-CRITICAL_ERROR: clock_gettime(%i) failed!!\n", main_clock);
}
else {
ss = ((double) ts.tv_sec) + ((double) ts.tv_nsec / (double) 1e9);
Expand Down
1 change: 1 addition & 0 deletions PsychSourceGL/Source/Linux/Base/PsychTimeGlue.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const char* PsychSupportStatus(void);
double PsychOSGetLinuxMonotonicTime(void);
double PsychOSMonotonicToRefTime(double monotonicTime);
double PsychOSRefTimeToMonotonicTime(double refInputTime);
double PsychOSRealtimeToRefTime(double t);

// Test if module needs to call XInitThreads() itself during startup:
int PsychOSNeedXInitThreads(int verbose);
Expand Down
12 changes: 12 additions & 0 deletions PsychSourceGL/Source/OSX/Base/PsychTimeGlue.c
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ double PsychGetWallClockSeconds(void)
return(((double) tv.tv_sec) + (((double) tv.tv_usec) / 1000000.0));
}

/* CLOCK_REALTIME / gettimeofday() time to macOS GetSecs time. */
double PsychOSRealtimeToRefTime(double t)
{
// Standard monotonic GetSecs timebase?
if (PsychOSMonotonicToRefTime(0) == 0) {
// Yes. Need to convert from CLOCK_REALTIME / gettimeofday() to monotonic:
t -= PsychGetWallClockSeconds() - PsychGetAdjustedPrecisionTimerSeconds(NULL);
}

return(t);
}

/* No-Op function on macOS atm. */
double PsychOSMonotonicToRefTime(double monotonicTime)
{
Expand Down
1 change: 1 addition & 0 deletions PsychSourceGL/Source/OSX/Base/PsychTimeGlue.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ int PsychGetOSXMinorVersion(psych_bool* isARM);
const char* PsychSupportStatus(void);
double PsychOSMonotonicToRefTime(double monotonicTime);
double PsychOSRefTimeToMonotonicTime(double refInputTime);
double PsychOSRealtimeToRefTime(double t);

// Execute a block of code { statement1; statement2; ...; statementn; } on the
// application main thread. Execute directly if current thread is main thread,
Expand Down
6 changes: 6 additions & 0 deletions PsychSourceGL/Source/Windows/Base/PsychTimeGlue.c
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,12 @@ double PsychGetWallClockSeconds(void)
return(wallSecs);
}

/* No-Op function on MS-Windows atm. */
double PsychOSRealtimeToRefTime(double t)
{
return(t);
}

/* No-Op function on MS-Windows atm. */
double PsychOSMonotonicToRefTime(double monotonicTime)
{
Expand Down
1 change: 1 addition & 0 deletions PsychSourceGL/Source/Windows/Base/PsychTimeGlue.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ psych_uint64 PsychAutoLockThreadToCores(psych_uint64* curCpuMask);
const char* PsychSupportStatus(void);
double PsychOSMonotonicToRefTime(double monotonicTime);
double PsychOSRefTimeToMonotonicTime(double refInputTime);
double PsychOSRealtimeToRefTime(double t);

//end include once
#endif
60 changes: 53 additions & 7 deletions Psychtoolbox/PsychBasic/PsychTweak.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@
% This function allows to tweak some low-level operating parameters of
% Psychtoolbox. Such tweaks often affect all mex files, not only specific
% files. You must execute this function before any other Psychtoolbox mex
% file is used, otherwise mex files will not pick up consistent settings
% file is used, otherwise mex files may not pick up consistent settings
% and weird things may happen! If in doubt, execute "clear mex" before
% executing this function.
%
% Currently the function mostly implements tweaks for MS-Windows to allow
% to cope with the brokenness of the system, especially in the domain of
% timing and timestamping.
%
%
% Available subfunctions:
% =======================
Expand Down Expand Up @@ -82,14 +78,43 @@
%
%
% PsychTweak('LibUSBDebug', verbosity);
%
% -- Select level of verbosity for low-level debug output of USB functions.
% This currently sets the debug level of libusb-1.0 based functions, e.g,
% PsychHID, PsychKinectCore, some videocapture functions and others.
% Possible values: 0 = Silence (default), 1 = Errors, 2 = Errors +
% Warnings, 3 = Errors + Warnings + Info messages.
%
%
% Linux only tweaks:
% ------------------
%
% PsychTweak('GetSecsClock', clockid);
% -- Select type of system clock to use for all timing functions like GetSecs,
% WaitSecs, all timing and timestamps in Screen(), PsychPortAudio() etc.
% clockid can be any of:
%
% 0 = CLOCK_REALTIME (aka gettimeofday() time or wall clock). This is the default.
% It counts the seconds since 1st January 1970 midnight. This clock can be set
% by the system administrator, or external time services like NTP network time
% service. If NTP or the system administrator so desires, time can jump forward
% or backwards. Leap seconds can do similar things. Allows easy synchronization
% of clocks across computers if they are all driven by a NTP or PTP time source.
%
% 1 = CLOCK_MONOTONIC. This clock can not jump backwards, but is always monotonically
% increasing. Its zero point is typically system boot time. It freezes if the
% system is suspended / sleeping. The clock is still slowly adjusted (slewed) by
% external time sources like NTP or PTP if available. This can be the most efficient
% selection if you don't need to have consistent time across computers or equipment.
%
% 4 = CLOCK_MONOTONIC_RAW. Like CLOCK_MONOTONIC, but no time correction wrt. an external
% reference time source like NTP or PTP is performed.
%
% 7 = CLOCK_BOOTTIME. Like CLOCK_MONOTONIC, but keeps counting while system is sleeping.
%
% Note that only clockid 0 and 1 have been verified for precision and correctness to some
% degree. Other settings may cause to functions to malfunction or be imprecise.
%
%
% MS-Windows only tweaks:
% -----------------------
%
Expand Down Expand Up @@ -359,7 +384,7 @@
% otherwise they won't pick up the tweak settings consistently. Check if
% any ptb mex files are loaded:

% inmem not yet implemented as of Octave 3.6.x, so Matlab only:
% inmem not yet implemented as of Octave 8.4, so Matlab only:
if exist('inmem') %#ok<EXIST>
% Get list of all loaded mex files in cell array mexf:
[foo, mexf] = inmem('-completenames'); %#ok<ASGLU>
Expand All @@ -378,12 +403,33 @@
% Reset some signalling environment variables used by mex files:
if strcmpi(cmd, 'Reset')
setenv('PSYCH_LOWRESCLOCK_FALLBACK');
setenv('PSYCH_GETSECS_CLOCK');
return;
else
% Reset this one even if no 'Reset' command given:
setenv('PSYCH_LOWRESCLOCK_FALLBACK');
end

if strcmpi(cmd, 'GetSecsClock')
if length(varargin) < 1
error('Must provide a clockid.');
end

val = varargin{1};
if ~isnumeric(val) || ~isscalar(val)
error('Must provide a single integer as argument!');
end

val = round(val);

if ~ismember(val, [0, 1])
warning('A clockid other than 0 or 1 may cause some functions not to work correctly, or not with high precision.');
end

setenv('PSYCH_GETSECS_CLOCK', sprintf('%i', val));
return;
end

if strcmpi(cmd, 'BackwardTimejumpTolerance')
if length(varargin) < 1
error('Must provide a timing threshold in seconds.');
Expand Down

0 comments on commit 32f319f

Please sign in to comment.