Skip to content

Commit 1beb455

Browse files
committed
Add support for precise absolute tapered start time
Adding two variables, specifying the beginning and the end of a tapered start interval, measured in nanoseconds since epoch: ``` FAKETIME_TAPER_BEGIN_NSEC_SINCE_EPOCH FAKETIME_TAPER_END_NSEC_SINCE_EPOCH ``` The behavior these implement is similar to what `FAKETIME_START_AFTER_SECONDS` does, with a few key differences: * timestamp is absolute, instead of being relative to process startup * timestamp is specified in nanoseconds * conversion interacts correctly with `utimes` family of functions * conversion is tapered (see below), which makes mapping reversible (up to loss of precision) The reason we want this feature is the following use case. We run a large test suite under faketime. That test suite has access to filesystem artifacts that were created prior to test start up. Among those artifacts are some caches which are considered up to date iff the timestamps of the files match what's recorded in a data structure. This means that for those caches to be considered valid we need their timestamps to not be fake. The reason we can't use `FAKETIME_START_AFTER_SECONDS` directly is that the test suite consists of multiple processes, for those processes to correctly interact with each other they need a consistent timestamp mapping that is shared between them. In fact the simplest bash script already behaves incorrectly because the commands use different process start times. ``` touch old LD_PRELOAD=... FAKETIME=+100d FAKETIME_START_AFTER_SECONDS=0 bash -c 'touch new; stat old new' ``` The expected behavior is that the timestamp of `old` is not rewritten, while the timestamp of `new` is rewritten. That is in fact achievable now: ``` FAKETIME_TAPER_BEGIN_NSEC_SINCE_EPOCH=$(date +%s%N) sleep 0.1 FAKETIME_TAPER_END_NSEC_SINCE_EPOCH=$(date +%s%N) FAKETIME=+100d FAKETIME_KEEP_BEFORE_NSEC_SINCE_EPOCH="$now_ns" bash -c 'touch new; stat old new' ``` What is tapering and why do we need it? The idea is to make the time transition smooth instead of abrupt, gradually increasing the offset amount from the start to the end of the tapering interval. The reason we want this is to make the time mapping reversible (up to some loss of precision). This means some programs that interact with the file system will no longer be confused. For example, if you do the equivalent of `touch -d "3 days ago"` and then read back the timestamp, you'll get approximately the expected timestamp, instead of something completely off.
1 parent 942b30e commit 1beb455

File tree

1 file changed

+181
-2
lines changed

1 file changed

+181
-2
lines changed

src/libfaketime.c

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,11 @@ static int outfile = -1;
340340

341341
static bool limited_faking = false;
342342
static long callcounter = 0;
343+
344+
static bool ft_taper = 0;
345+
static long long ft_taper_begin_nsec_since_epoch = -1;
346+
static long long ft_taper_end_nsec_since_epoch = -1;
347+
343348
static long ft_start_after_secs = -1;
344349
static long ft_stop_after_secs = -1;
345350
static long ft_start_after_ncalls = -1;
@@ -1156,6 +1161,133 @@ int statx(int dirfd, const char *pathname, int flags, unsigned int mask, struct
11561161
}
11571162
#endif
11581163

1164+
static long long faketime_offset_ns()
1165+
{
1166+
return ((long long)user_offset.tv_sec) * 1000000000 + (long long)user_offset.tv_nsec;
1167+
}
1168+
1169+
// (a * b + c) / d, but do something to avoid multiplication overflow
1170+
// (only "mostly" prevents overflow, not if the values are already
1171+
// over 2^62)
1172+
//
1173+
// precondition: a < d, r < d
1174+
static long long mad_div_avoid_overflow_inner(long long a, long long b, long long c, long long d)
1175+
{
1176+
if (a <= 1 || b <= 1) {
1177+
return (a * b + c) / d;
1178+
}
1179+
// how many multiples of [a] we need to make a full [d]
1180+
long long k = (d + a - 1) / a; // 2 <= k <= d/2+1
1181+
long long rem_per_f = a * k - d;
1182+
// how many occurrences of [a*k] (and therefore [d]) we can
1183+
// easily take from [a*b]
1184+
long long f = b / k;
1185+
// remaining: rem_per_f * f + (b % k) * a + c
1186+
long long rem = (b % k) * a + c;
1187+
long long res = f + rem / d;
1188+
rem = rem % d;
1189+
return res + mad_div_avoid_overflow_inner(rem_per_f, f, rem, d);
1190+
}
1191+
1192+
static long long mult_div_avoid_overflow(long long a, long long b, long long d)
1193+
{
1194+
long long res = a / d;
1195+
return res + mad_div_avoid_overflow_inner(a%d, b, 0, d);
1196+
}
1197+
1198+
static long long faketime_do_tapered_transform_gen(long long t, long long taper_begin, long long taper_end, long long offset)
1199+
{
1200+
if (t <= taper_begin)
1201+
{
1202+
return t;
1203+
}
1204+
if (t >= taper_end)
1205+
{
1206+
return t + offset;
1207+
}
1208+
long long w = taper_end - taper_begin;
1209+
long long h = w + offset;
1210+
// [h] output time passes over [w] input time
1211+
long long dt1 = t - taper_begin;
1212+
// so we're looking for dt1 * h / w
1213+
long long dt2 = mult_div_avoid_overflow(dt1, h, w);
1214+
return taper_begin + dt2;
1215+
}
1216+
1217+
static long long faketime_do_tapered_offset(long long t)
1218+
{
1219+
if (!ft_taper)
1220+
{
1221+
return t + faketime_offset_ns();
1222+
}
1223+
return faketime_do_tapered_transform_gen(t, ft_taper_begin_nsec_since_epoch, ft_taper_end_nsec_since_epoch, faketime_offset_ns());
1224+
}
1225+
1226+
static long long faketime_undo_tapered_offset(long long t)
1227+
{
1228+
if (!ft_taper)
1229+
{
1230+
return t - faketime_offset_ns();
1231+
}
1232+
long long offset = faketime_offset_ns();
1233+
return faketime_do_tapered_transform_gen(t, ft_taper_begin_nsec_since_epoch, ft_taper_end_nsec_since_epoch + offset, -offset);
1234+
}
1235+
1236+
static void faketime_div_mod(long long ns, long long d, long long *res, long long *rem)
1237+
{
1238+
*res = ns / d;
1239+
*rem = ns % d;
1240+
if(*rem < 0) {
1241+
(*rem) += d;
1242+
(*res)--;
1243+
}
1244+
}
1245+
1246+
static struct timeval faketime_undo_tapered_offset_timeval(struct timeval t)
1247+
{
1248+
long long nsec = ((long long)t.tv_sec) * 1000000000 + ((long long)t.tv_usec) * 1000;
1249+
nsec = faketime_undo_tapered_offset(nsec);
1250+
long long sec;
1251+
long long nsec_rem;
1252+
faketime_div_mod(nsec, 1000000000, &sec, &nsec_rem);
1253+
t.tv_sec = sec;
1254+
t.tv_usec = nsec_rem / 1000;
1255+
return t;
1256+
}
1257+
1258+
static struct timespec faketime_undo_tapered_offset_timespec(struct timespec t)
1259+
{
1260+
long long nsec = ((long long)t.tv_sec) * 1000000000 + ((long long)t.tv_nsec);
1261+
nsec = faketime_undo_tapered_offset(nsec);
1262+
long long sec;
1263+
long long nsec_rem;
1264+
faketime_div_mod(nsec, 1000000000, &sec, &nsec_rem);
1265+
t.tv_sec = sec;
1266+
t.tv_nsec = nsec_rem;
1267+
return t;
1268+
}
1269+
1270+
static struct timespec faketime_do_tapered_offset_timespec(struct timespec t)
1271+
{
1272+
long long nsec = ((long long)t.tv_sec) * 1000000000 + ((long long)t.tv_nsec);
1273+
nsec = faketime_do_tapered_offset(nsec);
1274+
long long sec;
1275+
long long nsec_rem;
1276+
faketime_div_mod(nsec, 1000000000, &sec, &nsec_rem);
1277+
t.tv_sec = sec;
1278+
t.tv_nsec = nsec_rem;
1279+
return t;
1280+
}
1281+
1282+
// the precision here will be really atrocious,
1283+
// if tapering compresses time significantly
1284+
static int faketime_undo_tapered_offset_sec(int t)
1285+
{
1286+
long long nsec = ((long long)t) * 1000000000;
1287+
nsec = faketime_undo_tapered_offset(nsec);
1288+
return nsec / 1000000000;
1289+
}
1290+
11591291
#ifdef FAKE_FILE_TIMESTAMPS
11601292
#ifdef MACOS_DYLD_INTERPOSE
11611293
int macos_utime(const char *filename, const struct utimbuf *times)
@@ -1180,6 +1312,13 @@ int utime(const char *filename, const struct utimbuf *times)
11801312
{
11811313
ntbuf.actime = times->actime - user_offset.tv_sec;
11821314
ntbuf.modtime = times->modtime - user_offset.tv_sec;
1315+
1316+
if (ft_taper)
1317+
{
1318+
ntbuf.actime = faketime_undo_tapered_offset_sec(times->actime);
1319+
ntbuf.modtime = faketime_undo_tapered_offset_sec(times->modtime);
1320+
}
1321+
11831322
times = &ntbuf;
11841323
}
11851324
#ifdef MACOS_DYLD_INTERPOSE
@@ -1217,6 +1356,11 @@ int utimes(const char *filename, const struct timeval times[2])
12171356
user_offset2.tv_usec = user_offset.tv_nsec / 1000;
12181357
timersub(&times[0], &user_offset2, &tn[0]);
12191358
timersub(&times[1], &user_offset2, &tn[1]);
1359+
if (ft_taper)
1360+
{
1361+
tn[0] = faketime_undo_tapered_offset_timeval(times[0]);
1362+
tn[1] = faketime_undo_tapered_offset_timeval(times[1]);
1363+
}
12201364
times = tn;
12211365
}
12221366
#ifdef MACOS_DYLD_INTERPOSE
@@ -1261,6 +1405,11 @@ static void fake_two_timespec(const struct timespec in_times[2], struct timespec
12611405
else
12621406
{
12631407
timersub2(&in_times[j], &user_offset, &out_times[j], n);
1408+
if (ft_taper)
1409+
{
1410+
out_times[j] = faketime_undo_tapered_offset_timespec(in_times[j]);
1411+
}
1412+
12641413
}
12651414
}
12661415
}
@@ -2897,6 +3046,24 @@ static void ftpl_really_init(void)
28973046
}
28983047
}
28993048

3049+
if ((tmp_env = getenv("FAKETIME_TAPER_BEGIN_NSEC_SINCE_EPOCH")) != NULL)
3050+
{
3051+
ft_taper_begin_nsec_since_epoch = atoll(tmp_env);
3052+
if(!ft_taper)
3053+
{
3054+
ft_taper = true;
3055+
ft_taper_end_nsec_since_epoch = ft_taper_begin_nsec_since_epoch;
3056+
}
3057+
}
3058+
if ((tmp_env = getenv("FAKETIME_TAPER_END_NSEC_SINCE_EPOCH")) != NULL)
3059+
{
3060+
ft_taper_end_nsec_since_epoch = atoll(tmp_env);
3061+
if(!ft_taper)
3062+
{
3063+
ft_taper = true;
3064+
ft_taper_begin_nsec_since_epoch = ft_taper_end_nsec_since_epoch;
3065+
}
3066+
}
29003067
if ((tmp_env = getenv("FAKETIME_START_AFTER_SECONDS")) != NULL)
29013068
{
29023069
ft_start_after_secs = atol(tmp_env);
@@ -3220,7 +3387,6 @@ int fake_clock_gettime(clockid_t clk_id, struct timespec *tp)
32203387
timespecsub(tp, &ftpl_starttime.real, &tmp_ts);
32213388
break;
32223389
}
3223-
32243390
if (limited_faking)
32253391
{
32263392
/* Check whether we actually should be faking the returned timestamp. */
@@ -3373,7 +3539,20 @@ int fake_clock_gettime(clockid_t clk_id, struct timespec *tp)
33733539
{
33743540
timeadj = tdiff;
33753541
}
3376-
timespecadd(&user_faked_time_timespec, &timeadj, tp);
3542+
3543+
if(ft_taper)
3544+
{
3545+
// Tapered offset time transition ignores the speed up/slow
3546+
// down setting. Someone should figure out how to do speed
3547+
// up / slow down correctly in that case (ideally, a global
3548+
// speed-up, as a part of a global time mapping routine, not
3549+
// relying on a per-process state).
3550+
*tp = faketime_do_tapered_offset_timespec(*tp);
3551+
}
3552+
else
3553+
{
3554+
timespecadd(&user_faked_time_timespec, &timeadj, tp);
3555+
}
33773556
}
33783557
break;
33793558

0 commit comments

Comments
 (0)