Improve folly/portability/Time.cpp OS X and Windows compatibility
authorChristopher Dykes <cdykes@fb.com>
Tue, 12 Jul 2016 18:34:23 +0000 (11:34 -0700)
committerFacebook Github Bot <facebook-github-bot-bot@fb.com>
Tue, 12 Jul 2016 18:38:28 +0000 (11:38 -0700)
Summary:
Folly currently contains wrappers to emulate
`clock_gettime()` on platforms which don't support it.

The OS X and Windows wrappers are missing a bit of functionality we
need. In this diff, I:

1. Bring the OS X and Windows wrappers closer to Linux's functionality
2. Add unit tests

Reviewed By: bmaurer, mzlee

Differential Revision: D3418054

fbshipit-source-id: 47eba871e844b4c3dccd7ab201ec82e1f6edf729

folly/portability/Time.cpp
folly/portability/Time.h
folly/portability/test/TimeTest.cpp [new file with mode: 0644]
folly/test/Makefile.am
folly/test/TestUtils.h

index a9160356f33fb7983b1dc76e8cb48a513a115de1..da44db618eb9d46b2a35b957cc484d102c8af58b 100755 (executable)
  */
 
 #include <folly/portability/Time.h>
+#include <folly/Likely.h>
+
+#include <assert.h>
+
+#include <chrono>
+
+template <typename _Rep, typename _Period>
+static void duration_to_ts(
+    std::chrono::duration<_Rep, _Period> d,
+    struct timespec* ts) {
+  ts->tv_sec = std::chrono::duration_cast<std::chrono::seconds>(d).count();
+  ts->tv_nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(
+                    d % std::chrono::seconds(1))
+                    .count();
+}
 
 #if !FOLLY_HAVE_CLOCK_GETTIME
 #if __MACH__
 #include <errno.h>
+#include <mach/mach_init.h>
+#include <mach/mach_port.h>
 #include <mach/mach_time.h>
-
-static const mach_timebase_info_data_t* tbInfo() {
-  static auto info = [] {
-    static mach_timebase_info_data_t info;
-    return (mach_timebase_info(&info) == KERN_SUCCESS) ? &info : nullptr;
-  }();
-  return info;
+#include <mach/mach_types.h>
+#include <mach/task.h>
+#include <mach/thread_act.h>
+#include <mach/vm_map.h>
+
+static std::chrono::nanoseconds time_value_to_ns(time_value_t t) {
+  return std::chrono::seconds(t.seconds) +
+      std::chrono::microseconds(t.microseconds);
 }
 
-int clock_gettime(clockid_t clk_id, struct timespec* ts) {
-  auto tb_info = tbInfo();
-  if (tb_info == nullptr) {
-    errno = EINVAL;
+static int clock_process_cputime(struct timespec* ts) {
+  // Get CPU usage for live threads.
+  task_thread_times_info thread_times_info;
+  mach_msg_type_number_t thread_times_info_count;
+  TASK_THREAD_TIMES_INFO_COUNT;
+  kern_return_t kern_result = task_info(
+      mach_task_self(),
+      TASK_THREAD_TIMES_INFO,
+      (thread_info_t)&thread_times_info,
+      &thread_times_info_count);
+  if (UNLIKELY(kern_result != KERN_SUCCESS)) {
     return -1;
   }
 
-  uint64_t now_ticks = mach_absolute_time();
-  uint64_t now_ns = (now_ticks * tb_info->numer) / tb_info->denom;
-  ts->tv_sec = now_ns / 1000000000;
-  ts->tv_nsec = now_ns % 1000000000;
+  // Get CPU usage for terminated threads.
+  mach_task_basic_info task_basic_info;
+  mach_msg_type_number_t task_basic_info_count = MACH_TASK_BASIC_INFO_COUNT;
+  kern_result = task_info(
+      mach_task_self(),
+      MACH_TASK_BASIC_INFO,
+      (thread_info_t)&task_basic_info,
+      &task_basic_info_count);
+  if (UNLIKELY(kern_result != KERN_SUCCESS)) {
+    return -1;
+  }
+
+  auto cputime = time_value_to_ns(thread_times_info.user_time) +
+      time_value_to_ns(thread_times_info.system_time) +
+      time_value_to_ns(task_basic_info.user_time) +
+      time_value_to_ns(task_basic_info.system_time);
+  duration_to_ts(cputime, ts);
+  return 0;
+}
 
+static int clock_thread_cputime(struct timespec* ts) {
+  mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
+  thread_basic_info_data_t thread_info_data;
+  thread_act_t thread = mach_thread_self();
+  kern_return_t kern_result = thread_info(
+      thread, THREAD_BASIC_INFO, (thread_info_t)&thread_info_data, &count);
+  mach_port_deallocate(mach_task_self(), thread);
+  if (UNLIKELY(kern_result != KERN_SUCCESS)) {
+    return -1;
+  }
+  auto cputime = time_value_to_ns(thread_info_data.system_time) +
+      time_value_to_ns(thread_info_data.user_time);
+  duration_to_ts(cputime, ts);
   return 0;
 }
 
+int clock_gettime(clockid_t clk_id, struct timespec* ts) {
+  switch (clk_id) {
+    case CLOCK_REALTIME: {
+      auto now = std::chrono::system_clock::now().time_since_epoch();
+      duration_to_ts(now, ts);
+      return 0;
+    }
+    case CLOCK_MONOTONIC: {
+      auto now = std::chrono::steady_clock::now().time_since_epoch();
+      duration_to_ts(now, ts);
+      return 0;
+    }
+    case CLOCK_PROCESS_CPUTIME_ID:
+      return clock_process_cputime(ts);
+    case CLOCK_THREAD_CPUTIME_ID:
+      return clock_thread_cputime(ts);
+    default:
+      errno = EINVAL;
+      return -1;
+  }
+}
+
 int clock_getres(clockid_t clk_id, struct timespec* ts) {
-  auto tb_info = tbInfo();
-  if (tb_info == nullptr) {
-    errno = EINVAL;
+  if (clk_id != CLOCK_MONOTONIC) {
     return -1;
   }
 
+  static auto info = [] {
+    static mach_timebase_info_data_t info;
+    auto result = (mach_timebase_info(&info) == KERN_SUCCESS) ? &info : nullptr;
+    assert(result);
+    return result;
+  }();
+
   ts->tv_sec = 0;
-  ts->tv_nsec = tb_info->numer / tb_info->denom;
+  ts->tv_nsec = info->numer / info->denom;
 
   return 0;
 }
@@ -64,7 +144,27 @@ int clock_getres(clockid_t clk_id, struct timespec* ts) {
 
 #include <folly/portability/Windows.h>
 
-static constexpr size_t kNsPerSec = 1000000000;
+using unsigned_nanos = std::chrono::duration<uint64_t, std::nano>;
+
+static unsigned_nanos filetimeToUnsignedNanos(FILETIME ft) {
+  ULARGE_INTEGER i;
+  i.HighPart = ft.dwHighDateTime;
+  i.LowPart = ft.dwLowDateTime;
+
+  // FILETIMEs are in units of 100ns.
+  return unsigned_nanos(i.QuadPart * 100);
+};
+
+static LARGE_INTEGER performanceFrequency() {
+  static auto result = [] {
+    LARGE_INTEGER freq;
+    // On Windows XP or later, this will never fail.
+    BOOL res = QueryPerformanceFrequency(&freq);
+    assert(res);
+    return freq;
+  }();
+  return result;
+}
 
 extern "C" int clock_getres(clockid_t clock_id, struct timespec* res) {
   if (!res) {
@@ -74,12 +174,14 @@ extern "C" int clock_getres(clockid_t clock_id, struct timespec* res) {
 
   switch (clock_id) {
     case CLOCK_MONOTONIC: {
-      LARGE_INTEGER freq;
-      if (!QueryPerformanceFrequency(&freq)) {
+      LARGE_INTEGER freq = performanceFrequency();
+      if (freq.QuadPart == -1) {
         errno = EINVAL;
         return -1;
       }
 
+      static constexpr size_t kNsPerSec = 1000000000;
+
       res->tv_sec = 0;
       res->tv_nsec = (long)((kNsPerSec + (freq.QuadPart >> 1)) / freq.QuadPart);
       if (res->tv_nsec < 1) {
@@ -116,29 +218,24 @@ extern "C" int clock_gettime(clockid_t clock_id, struct timespec* tp) {
     return -1;
   }
 
-  const auto ftToUint = [](FILETIME ft) -> uint64_t {
-    ULARGE_INTEGER i;
-    i.HighPart = ft.dwHighDateTime;
-    i.LowPart = ft.dwLowDateTime;
-    return i.QuadPart;
-  };
-  const auto timeToTimespec = [](timespec* tp, uint64_t t) -> int {
-    constexpr size_t k100NsPerSec = kNsPerSec / 100;
-
-    // The filetimes t is based on are represented in
-    // 100ns's. (ie. a value of 4 is 400ns)
-    tp->tv_sec = t / k100NsPerSec;
-    tp->tv_nsec = ((long)(t % k100NsPerSec)) * 100;
+  const auto unanosToTimespec = [](timespec* tp, unsigned_nanos t) -> int {
+    static constexpr unsigned_nanos one_sec(std::chrono::seconds(1));
+    tp->tv_sec = std::chrono::duration_cast<std::chrono::seconds>(t).count();
+    tp->tv_nsec = (t % one_sec).count();
     return 0;
   };
 
   FILETIME createTime, exitTime, kernalTime, userTime;
   switch (clock_id) {
     case CLOCK_REALTIME: {
-      constexpr size_t kDeltaEpochIn100NS = 116444736000000000ULL;
-
-      GetSystemTimeAsFileTime(&createTime);
-      return timeToTimespec(tp, ftToUint(createTime) - kDeltaEpochIn100NS);
+      auto now = std::chrono::system_clock::now().time_since_epoch();
+      duration_to_ts(now, tp);
+      return 0;
+    }
+    case CLOCK_MONOTONIC: {
+      auto now = std::chrono::steady_clock::now().time_since_epoch();
+      duration_to_ts(now, tp);
+      return 0;
     }
     case CLOCK_PROCESS_CPUTIME_ID: {
       if (!GetProcessTimes(
@@ -151,7 +248,10 @@ extern "C" int clock_gettime(clockid_t clock_id, struct timespec* tp) {
         return -1;
       }
 
-      return timeToTimespec(tp, ftToUint(kernalTime) + ftToUint(userTime));
+      return unanosToTimespec(
+          tp,
+          filetimeToUnsignedNanos(kernalTime) +
+              filetimeToUnsignedNanos(userTime));
     }
     case CLOCK_THREAD_CPUTIME_ID: {
       if (!GetThreadTimes(
@@ -164,25 +264,10 @@ extern "C" int clock_gettime(clockid_t clock_id, struct timespec* tp) {
         return -1;
       }
 
-      return timeToTimespec(tp, ftToUint(kernalTime) + ftToUint(userTime));
-    }
-    case CLOCK_MONOTONIC: {
-      LARGE_INTEGER fl, cl;
-      if (!QueryPerformanceFrequency(&fl) || !QueryPerformanceCounter(&cl)) {
-        errno = EINVAL;
-        return -1;
-      }
-
-      int64_t freq = fl.QuadPart;
-      int64_t counter = cl.QuadPart;
-      tp->tv_sec = counter / freq;
-      tp->tv_nsec = (long)(((counter % freq) * kNsPerSec + (freq >> 1)) / freq);
-      if (tp->tv_nsec >= kNsPerSec) {
-        tp->tv_sec++;
-        tp->tv_nsec -= kNsPerSec;
-      }
-
-      return 0;
+      return unanosToTimespec(
+          tp,
+          filetimeToUnsignedNanos(kernalTime) +
+              filetimeToUnsignedNanos(userTime));
     }
 
     default:
index 642a95de59e06365b01dbde8c9fe18987865014d..9d795830c283d70f389e8d3a42cf02cf83daa0de 100755 (executable)
 // platforms we support.
 #if !FOLLY_HAVE_CLOCK_GETTIME && (defined(__MACH__) || defined(_WIN32))
 #define CLOCK_REALTIME 0
-// The Windows implementation supports a few other
-// clock types as well.
-#ifdef _WIN32
-# define CLOCK_MONOTONIC 1
-# define CLOCK_PROCESS_CPUTIME_ID 2
-# define CLOCK_THREAD_CPUTIME_ID 3
-#endif
+#define CLOCK_MONOTONIC 1
+#define CLOCK_PROCESS_CPUTIME_ID 2
+#define CLOCK_THREAD_CPUTIME_ID 3
 
 typedef uint8_t clockid_t;
 extern "C" int clock_gettime(clockid_t clk_id, struct timespec* ts);
diff --git a/folly/portability/test/TimeTest.cpp b/folly/portability/test/TimeTest.cpp
new file mode 100644 (file)
index 0000000..83ecc0c
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <folly/portability/Time.h>
+
+#include <folly/test/TestUtils.h>
+
+#include <chrono>
+
+#include <gtest/gtest.h>
+
+static constexpr auto kAcceptableDeltaSecs = std::chrono::seconds(120);
+
+using folly::test::AreWithinSecs;
+
+#ifdef CLOCK_REALTIME
+
+TEST(Time, clockGettimeRealtimeAreWithin120SecsOfStdChronoSystemClock) {
+  struct timespec ts;
+  auto ret = clock_gettime(CLOCK_REALTIME, &ts);
+  ASSERT_EQ(0, ret);
+
+  auto gettimeResult =
+      std::chrono::seconds(ts.tv_sec) + std::chrono::nanoseconds(ts.tv_nsec);
+  auto stdChronoSystemClockNow =
+      std::chrono::system_clock::now().time_since_epoch();
+  ASSERT_TRUE(AreWithinSecs(
+      gettimeResult, stdChronoSystemClockNow, kAcceptableDeltaSecs));
+}
+
+#endif /* CLOCK_REALTIME */
+
+#ifdef CLOCK_MONOTONIC
+
+TEST(Time, clockGettimeMonotonicAreWithin120SecsOfStdChronoSteadyClock) {
+  struct timespec ts;
+  auto ret = clock_gettime(CLOCK_MONOTONIC, &ts);
+  ASSERT_EQ(0, ret);
+
+  auto gettimeResult =
+      std::chrono::seconds(ts.tv_sec) + std::chrono::nanoseconds(ts.tv_nsec);
+  auto stdChronoSteadyClockNow =
+      std::chrono::steady_clock::now().time_since_epoch();
+  ASSERT_TRUE(AreWithinSecs(
+      gettimeResult, stdChronoSteadyClockNow, kAcceptableDeltaSecs));
+}
+
+#endif /* CLOCK_MONOTONIC */
+
+#ifdef CLOCK_PROCESS_CPUTIME_ID
+
+TEST(Time, clockGettimeProcessCputimeIsGreaterThanZero) {
+  struct timespec ts;
+  auto ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts);
+  ASSERT_EQ(0, ret);
+
+  auto gettimeResult =
+      std::chrono::seconds(ts.tv_sec) + std::chrono::nanoseconds(ts.tv_nsec);
+
+  ASSERT_GT(gettimeResult, std::chrono::nanoseconds::zero());
+}
+
+#endif /* CLOCK_PROCESS_CPUTIME_ID */
+
+#ifdef CLOCK_THREAD_CPUTIME_ID
+
+TEST(Time, clockGettimeProcessThreadTimeIsGreaterThanZero) {
+  struct timespec ts;
+  auto ret = clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts);
+  ASSERT_EQ(0, ret);
+
+  auto gettimeResult =
+      std::chrono::seconds(ts.tv_sec) + std::chrono::nanoseconds(ts.tv_nsec);
+
+  ASSERT_GT(gettimeResult, std::chrono::nanoseconds::zero());
+}
+
+#endif /* CLOCK_THREAD_CPUTIME_ID */
index eb338cae9ec9be5e47600163e64022ce0040fef9..fbd2d9ed50a69f05942d11198479d32774802655 100644 (file)
@@ -229,6 +229,10 @@ indestructible_test_SOURCES = IndestructibleTest.cpp
 indestructible_test_LDADD = libfollytestmain.la
 TESTS += indestructible_test
 
+portability_time_test_SOURCES = ../portability/test/PortabilityTimeTest.cpp
+portability_time_test_LDADD = libfollytestmain.la
+TESTS += portability_time_test
+
 try_test_SOURCES = TryTest.cpp
 try_test_LDADD = libfollytestmain.la
 TESTS += try_test
index 14a6012db664cf27e7efa058c6fa6bc5fb6ab78d..2750f6d045a2e67beb0a8ce2ffc63ca39711fae1 100644 (file)
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include <chrono>
+
 #include <gtest/gtest.h>
 
 // We use this to indicate that tests have failed because of timing
 // a normal test failure; there is only an effect if the test framework
 // interprets the message.
 #define SKIP() GTEST_FATAL_FAILURE_("Test skipped by client")
+
+namespace folly {
+namespace test {
+
+template <typename T1, typename T2>
+::testing::AssertionResult
+AreWithinSecs(T1 val1, T2 val2, std::chrono::seconds acceptableDeltaSecs) {
+  auto deltaSecs =
+      std::chrono::duration_cast<std::chrono::seconds>(val1 - val2);
+  if (deltaSecs <= acceptableDeltaSecs &&
+      deltaSecs >= -1 * acceptableDeltaSecs) {
+    return ::testing::AssertionSuccess();
+  } else {
+    return ::testing::AssertionFailure()
+        << val1.count() << " and " << val2.count() << " are not within "
+        << acceptableDeltaSecs.count() << " secs of each other";
+  }
+}
+}
+}