diff --git a/Makefile b/Makefile index 34817430b..7e78c4f5a 100644 --- a/Makefile +++ b/Makefile @@ -377,7 +377,10 @@ hrpc_calc_server: prepare hrpc_calc_protoc -unittest: prepare libhv +hloop_clock_test: prepare libhv + $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Issl -Ievent -o bin/hloop_clock_test unittest/hloop_clock_test.c -Llib -lhv -pthread + +unittest: prepare libhv hloop_clock_test $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -o bin/rbtree_test unittest/rbtree_test.c base/rbtree.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -o bin/hbase_test unittest/hbase_test.c base/hbase.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -o bin/mkdir_p unittest/mkdir_test.c base/hbase.c @@ -505,4 +508,4 @@ echo-servers: echo-benchmark: echo-servers bash echo-servers/benchmark.sh -.PHONY: clean prepare install uninstall libhv examples unittest evpp echo-servers +.PHONY: clean prepare install uninstall libhv examples unittest hloop_clock_test evpp echo-servers diff --git a/event/hloop.c b/event/hloop.c index b12802435..6ea69a58b 100644 --- a/event/hloop.c +++ b/event/hloop.c @@ -553,14 +553,19 @@ hloop_status_e hloop_status(hloop_t* loop) { void hloop_update_time(hloop_t* loop) { loop->cur_hrtime = gethrtime_us(); - if (hloop_now(loop) != time(NULL)) { + uint64_t elapsed_ms = (loop->cur_hrtime - loop->start_hrtime) / 1000; + uint64_t now_ms = loop->start_ms + elapsed_ms; + uint64_t system_ms = gettimeofday_ms(); + uint64_t drift_ms = system_ms >= now_ms ? system_ms - now_ms : now_ms - system_ms; + // Compare complete timestamps: separately truncated seconds can differ without a clock change. + if (drift_ms >= 1000) { // systemtime changed, we adjust start_ms - loop->start_ms = gettimeofday_ms() - (loop->cur_hrtime - loop->start_hrtime) / 1000; + loop->start_ms = system_ms - elapsed_ms; } } uint64_t hloop_now(hloop_t* loop) { - return loop->start_ms / 1000 + (loop->cur_hrtime - loop->start_hrtime) / 1000000; + return hloop_now_ms(loop) / 1000; } uint64_t hloop_now_ms(hloop_t* loop) { diff --git a/scripts/unittest.sh b/scripts/unittest.sh index ca1046e8d..748a3d8ac 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -28,6 +28,7 @@ bin/hurl_test # bin/hthread_test # bin/hmutex_test bin/socketpair_test +bin/hloop_clock_test || exit $? # bin/threadpool_test # bin/objectpool_test bin/sizeof_test diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 6e17408b3..65f27d63b 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -163,6 +163,18 @@ set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_mqtt_test) endif() endif() +# ------event: cached clocks and wall-clock adjustment------ +add_executable(hloop_clock_test hloop_clock_test.c) +target_include_directories(hloop_clock_test PRIVATE .. ../base ../ssl ../event) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND TARGET hv_static) + target_compile_definitions(hloop_clock_test PRIVATE HLOOP_CLOCK_TEST_WRAPPED) + target_link_libraries(hloop_clock_test hv_static) + set_property(TARGET hloop_clock_test APPEND_STRING PROPERTY LINK_FLAGS + " -Wl,--wrap=gethrtime_us -Wl,--wrap=gettimeofday -Wl,--wrap=time") +else() + target_link_libraries(hloop_clock_test ${HV_LIBRARIES}) +endif() + # ------event: Windows epoll initialization------ if(WIN32 AND WITH_WEPOLL) add_executable(epoll_init_test epoll_init_test.c ../event/wepoll/wepoll.c) @@ -240,6 +252,7 @@ add_custom_target(unittest DEPENDS ${HTTP_LUA_UNITTEST_TARGETS} ${HTTP_JS_UNITTEST_TARGETS} ${EVENT_UNITTEST_TARGETS} + hloop_clock_test hdns_test hdns_benchmark ${REDIS_UNITTEST_TARGETS} diff --git a/unittest/hloop_clock_test.c b/unittest/hloop_clock_test.c new file mode 100644 index 000000000..9bb51b64b --- /dev/null +++ b/unittest/hloop_clock_test.c @@ -0,0 +1,135 @@ +#include "hevent.h" + +#include +#include +#include + +static int failures; + +/* Keep checks active in Release/NDEBUG builds. */ +static void check_equal(const char *name, uint64_t actual, uint64_t expected) { + if (actual != expected) { + fprintf(stderr, "%s: got %" PRIu64 ", expected %" PRIu64 "\n", name, actual, expected); + ++failures; + } +} + +static void test_cached_time(void) { + static const struct { + uint64_t start_ms; + uint64_t elapsed_us; + uint64_t expected_us; + } cases[] = { + {100900, 200000, 101100000}, {100900, 99999, 100999999}, {100900, 100000, 101000000}, + {100900, 1100000, 102000000}, {100000, 999999, 100999999}, {100000, 1000000, 101000000}, + }; + unsigned int i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + hloop_t loop; + memset(&loop, 0, sizeof(loop)); + loop.start_ms = cases[i].start_ms; + loop.start_hrtime = 10000000; + loop.cur_hrtime = loop.start_hrtime + cases[i].elapsed_us; + check_equal("cached seconds", hloop_now(&loop), cases[i].expected_us / 1000000); + check_equal("cached milliseconds", hloop_now_ms(&loop), cases[i].expected_us / 1000); + check_equal("cached microseconds", hloop_now_us(&loop), cases[i].expected_us); + check_equal("cached high-resolution time", hloop_now_hrtime(&loop), loop.cur_hrtime); + } +} + +#ifdef HLOOP_CLOCK_TEST_WRAPPED +/* Linux static-library tests replace clock sources without changing host time. */ +static uint64_t wall_ms; +static uint64_t hr_us; + +unsigned long long __wrap_gethrtime_us(void) { + return hr_us; +} + +int __wrap_gettimeofday(struct timeval *tv, void *tz) { + (void)tz; + tv->tv_sec = (time_t)(wall_ms / 1000); + tv->tv_usec = (long)((wall_ms % 1000) * 1000); + return 0; +} + +time_t __wrap_time(time_t *out) { + time_t now = (time_t)(wall_ms / 1000); + if (out) *out = now; + return now; +} + +static void test_second_boundary(void) { + hloop_t loop; + uint64_t first, second, third; + memset(&loop, 0, sizeof(loop)); + loop.start_ms = 100900; + loop.start_hrtime = 10000000; + + /* The wall sample follows the monotonic sample by 2 ms on the first + * update. Both clock sources then advance normally across a second. */ + hr_us = loop.start_hrtime + 100000; + wall_ms = 101002; + hloop_update_time(&loop); + first = hloop_now_ms(&loop); + hr_us = loop.start_hrtime + 1099000; + wall_ms = 101999; + hloop_update_time(&loop); + second = hloop_now_ms(&loop); + hr_us = loop.start_hrtime + 1100000; + wall_ms = 102000; + hloop_update_time(&loop); + third = hloop_now_ms(&loop); + + if (third < second) { + fprintf(stderr, "cached time moved backward from %" PRIu64 " to %" PRIu64 " ms\n", second, third); + ++failures; + } + check_equal("first boundary sample", first, 101000); + check_equal("second boundary sample", second, 101999); + check_equal("third boundary sample", third, 102000); + check_equal("unchanged wall-clock origin", loop.start_ms, 100900); + check_equal("unchanged high-resolution origin", loop.start_hrtime, 10000000); + check_equal("high-resolution sample", hloop_now_hrtime(&loop), hr_us); +} + +static void test_clock_adjustments(void) { + static const struct { + int drift_ms; + int correction_ms; + } cases[] = {{0, 0}, {1, 0}, {-1, 0}, {999, 0}, {-999, 0}, {1000, 1000}, {-1000, -1000}, {5000, 5000}, {-5000, -5000}}; + unsigned int i; + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + hloop_t loop; + uint64_t expected_ms = (uint64_t)(101100 + cases[i].correction_ms); + memset(&loop, 0, sizeof(loop)); + loop.start_ms = 100900; + loop.start_hrtime = 10000000; + hr_us = loop.start_hrtime + 200123; + wall_ms = (uint64_t)(101100 + cases[i].drift_ms); + hloop_update_time(&loop); + + check_equal("adjusted milliseconds", hloop_now_ms(&loop), expected_ms); + check_equal("adjusted seconds", hloop_now(&loop), expected_ms / 1000); + check_equal("adjusted microseconds", hloop_now_us(&loop), expected_ms * 1000 + 123); + check_equal("wall-clock origin adjustment", loop.start_ms, (uint64_t)(100900 + cases[i].correction_ms)); + check_equal("unchanged high-resolution origin", loop.start_hrtime, 10000000); + check_equal("high-resolution sample", hloop_now_hrtime(&loop), hr_us); + } +} +#endif + +int main(void) { +#ifdef HLOOP_CLOCK_TEST_WRAPPED + test_second_boundary(); + test_clock_adjustments(); +#endif + test_cached_time(); + if (failures) return 1; +#ifdef HLOOP_CLOCK_TEST_WRAPPED + puts("hloop_clock_test passed (cached time and deterministic clock updates)"); +#else + puts("hloop_clock_test passed (cached time; clock-update injection requires Linux and a static library)"); +#endif + return 0; +}