[UnitTest/InputService] Revise inputservice and add its unittests
authorDongju Chae <dongju.chae@samsung.com>
Fri, 10 Apr 2020 05:57:26 +0000 (14:57 +0900)
committer송욱/On-Device Lab(SR)/Staff Engineer/삼성전자 <wook16.song@samsung.com>
Tue, 14 Apr 2020 06:53:16 +0000 (15:53 +0900)
This patch revises inputservice add add its unittests

Signed-off-by: Dongju Chae <dongju.chae@samsung.com>
src/core/ne-host-input-service.cc
src/core/ne-inputservice.h
src/core/ne-thread-pool.cc
src/core/npu/NPUdrvAPI_emul.cc
tests/unittests/meson.build
tests/unittests/ne_core_inputservice_test.cc [new file with mode: 0644]
tests/unittests/ne_core_thread_pool_test.cc
tests/unittests/ne_unittest_utils.h

index 0e89bc5..c8a2fc2 100644 (file)
@@ -43,13 +43,14 @@ int
 HostInputService::submit (const DriverAPI *api, uint32_t id,
     const Model *model, Buffer *buffer, outputCallback callback)
 {
+  if (api == nullptr || model == nullptr)
+    return -EINVAL;
+
   taskFunc func = std::bind (&HostInputService::invoke, this,
       api, model, buffer, callback);
   ThreadTask *task = new ThreadTask (id, func);
 
-  ThreadPool::getInstance().enqueueTask (task);
-
-  return 0;
+  return ThreadPool::getInstance().enqueueTask (task);
 }
 
 /**
@@ -74,27 +75,46 @@ void
 HostInputService::invoke (const DriverAPI *api, const Model *model,
     Buffer *buffer, outputCallback callback)
 {
-  if (api->getState() == 0) {
-    input_config_t input_config;
+  input_config_t input_config;
+  npuConstraint constraint;
+  int state;
+
+  state = api->getState();
+  if (state != 0) {
+    logerr (TAG, "device is not available to run inference %d\n", state);
+    goto handle_callback;
+  }
 
-    input_config.enable = 1;
+  if (model == nullptr) {
+    logerr (TAG, "No valid model provided\n");
+    goto handle_callback;
+  }
+
+  input_config.enable = true;
+  input_config.model_id = model->getDmabuf ();
+  if (buffer != nullptr) {
     input_config.dmabuf_id = buffer->getDmabuf ();
-    input_config.model_id = model->getDmabuf ();
     input_config.activation_offset_addr0 = buffer->getOffset ();
     input_config.activation_offset_addr1 = buffer->getOffset ();
+  } else {
+    /** some instructions do not require the buffer (e.g., nop) */
+    input_config.dmabuf_id = -1;
+    input_config.activation_offset_addr0 = 0;
+    input_config.activation_offset_addr1 = 0;
+  }
 
-    /** set constraints */
-    npuConstraint constraint = model->getConstraint ();
-    input_config.timeout_ms = constraint.timeout_ms;
-    input_config.priority = constraint.priority;
+  /** set constraints */
+  constraint = model->getConstraint ();
+  input_config.timeout_ms = constraint.timeout_ms;
+  input_config.priority = constraint.priority;
 
-    /** run the inference with the input */
-    int status = api->runInput (&input_config);
-    if (status != 0) {
-      logerr (TAG, "Failed to run the NPU inference: %d\n", status);
-    }
-  }
+  /** run the inference with the input */
+  state = api->runInput (&input_config);
+  if (state != 0)
+    logerr (TAG, "Failed to run the NPU inference: %d\n", state);
 
+handle_callback:
   /** should call the callback regardless of failure, to avoid deadlock */
-  callback ();
+  if (callback != nullptr)
+    callback ();
 }
index 7891f22..1c000b7 100644 (file)
@@ -30,16 +30,18 @@ class InputService {
   public:
     /** @brief submit request to somewhere (depends on each impl.) */
     virtual int submit (const DriverAPI *api, uint32_t request_id, const Model *model,
-        Buffer *buffer, outputCallback callback) { return 0; }
+        Buffer *buffer, outputCallback callback = nullptr) { return 0; }
     /** @brief remove the submitted request (if possible) */
     virtual int remove (uint32_t request_id) { return 0; }
-    /** @brief invoke the request in one's turn */
-    virtual void invoke (const DriverAPI *api, const Model *model, Buffer *bufer,
-        outputCallback callback) {}
 
   protected:
     InputService () {}
     virtual ~InputService () {}
+
+  private:
+    /** @brief invoke the request in one's turn */
+    virtual void invoke (const DriverAPI *api, const Model *model, Buffer *bufer,
+        outputCallback callback) {}
 };
 
 /** @brief host input service */
@@ -48,11 +50,14 @@ class HostInputService : public InputService {
     static HostInputService & getInstance ();
 
     int submit (const DriverAPI *api, uint32_t request_id, const Model *model,
-        Buffer *buffer, outputCallback callback);
+        Buffer *buffer, outputCallback callback = nullptr);
     int remove (uint32_t request_id);
+
+  private:
+    /** do not allow to directly call invoke () */
     void invoke (const DriverAPI *api, const Model *model, Buffer *bufer,
         outputCallback callback);
-  private:
+
     static std::unique_ptr<HostInputService> instance_;
     static std::once_flag once_flag_;
 };
index d313e97..b0f5b23 100644 (file)
@@ -83,8 +83,20 @@ ThreadPool::enqueueTask (ThreadTask *task)
 
   {
     std::unique_lock<std::mutex> lock(m_);
+    std::deque<std::unique_ptr<ThreadTask>>::iterator it;
+    uint32_t task_id = task->getID();
+
+    it = std::find_if (queue_.begin(), queue_.end(),
+        [&task_id] (const std::unique_ptr<ThreadTask>& task) {
+        return task->getID() == task_id;
+        });
+    /** does not allow the same ID */
+    if (it != queue_.end())
+      return -EBUSY;
+
     queue_.push_back (std::unique_ptr<ThreadTask> (task));
   }
+
   cv_.notify_one();
 
   return 0;
@@ -105,10 +117,9 @@ ThreadPool::removeTask (uint32_t task_id)
       [&task_id] (const std::unique_ptr<ThreadTask>& task) {
         return task->getID() == task_id;
       });
-  if (it == queue_.end()) {
+  if (it == queue_.end())
     return -EBUSY;
-  } else {
-    queue_.erase (it);
-    return 0;
-  }
+
+  queue_.erase (it);
+  return 0;
 }
index 8d73ec7..5df7110 100644 (file)
@@ -204,9 +204,8 @@ TrinityEmulAPI::runInput (input_config_t *input_config) const
   if (model == nullptr)
     return -EINVAL;
 
+  /** buffer can be nullptr */
   void *buffer = mmap (input_config->dmabuf_id, 0x1000);
-  if (buffer == nullptr)
-    return -EINVAL;
 
   /** call emulation codes (AIP/NPU_SystemService_Emulator) */
   run_npu_emul (static_cast<char*>(model), static_cast<char*>(buffer));
index 053c848..29c1381 100644 (file)
@@ -80,4 +80,14 @@ if gtest_dep.found()
     install_dir : join_paths(ne_bindir, 'unittests')
   )
   test('unittest_ne_core_comm', unittest_ne_core_comm)
+
+  unittest_ne_core_inputservice = executable('unittest_ne_core_inputservice',
+    ['ne_core_inputservice_test.cc'],
+    include_directories: ne_host_inc,
+    dependencies: [gtest_dep, ne_core_dep],
+    install : true,
+    install_rpath : ne_libdir,
+    install_dir : join_paths(ne_bindir, 'unittests')
+  )
+  test('unittest_ne_core_inputservice', unittest_ne_core_inputservice)
 endif
diff --git a/tests/unittests/ne_core_inputservice_test.cc b/tests/unittests/ne_core_inputservice_test.cc
new file mode 100644 (file)
index 0000000..b50b4cc
--- /dev/null
@@ -0,0 +1,192 @@
+/**
+ * Proprietary
+ * Copyright (C) 2020 Samsung Electronics
+ * Copyright (C) 2020 Dongju Chae <dongju.chae@samsung.com>
+ */
+/**
+ * @file ne_core_inputservice_test.cc
+ * @date 10 Apr 2020
+ * @brief UnitTests to test functions in inputservice for NPU Engine
+ * @author Dongju Chae <dongju.chae@samsung.com>
+ * @bug No known bugs except for NYI items
+ */
+
+#include <ne-inputservice.h>
+#include <ne-thread-pool.h> /* just for remove() test */
+
+#include "ne_unittest_utils.h"
+
+/**
+ * @brief test single instances
+ */
+TEST (ne_core_inputservice_test, instance_singleton)
+{
+  HostInputService & host_is = HostInputService::getInstance ();
+  HostInputService & host_is2 = HostInputService::getInstance ();
+
+  EXPECT_EQ (&host_is, &host_is2);
+
+  HwInputService & hw_is = HwInputService::getInstance ();
+  HwInputService & hw_is2 = HwInputService::getInstance ();
+
+  EXPECT_EQ (&hw_is, &hw_is2);
+}
+
+/**
+ * @brief test features of submit() with a buffer
+ */
+TEST (ne_core_inputservice_test, host_submit)
+{
+  std::unique_ptr<DriverAPI> api;
+
+  api = DriverAPI::createDriverAPI (NPUCOND_TRIV_CONN_SOCIP, 0);
+  ASSERT_NE (api.get(), nullptr);
+
+  /** create dummy model & buffer */
+
+  Model * model = new Model (new HWmemDevice);
+  model->setDriverAPI (api.get());
+  EXPECT_EQ (model->alloc (4096), 0);
+
+  Buffer * buffer = new Buffer (new HWmemDevice);
+  buffer->setDriverAPI (api.get());
+  EXPECT_EQ (buffer->alloc (4096), 0);
+
+  HostInputService & service = HostInputService::getInstance ();
+  /** run without buffer */
+  EXPECT_EQ (service.submit (api.get(), 0, model, nullptr), 0);
+  /** run with buffer */
+  EXPECT_EQ (service.submit (api.get(), 1, model, buffer), 0);
+
+  sleep (1);
+
+  int num_called = 0;
+  std::condition_variable cv;
+  std::mutex m;
+
+  auto callback = std::bind (test_callback, &num_called, &m, &cv);
+  EXPECT_EQ (service.submit (api.get(), 2, model, buffer, callback), 0);
+  EXPECT_EQ (service.submit (api.get(), 3, model, buffer, callback), 0);
+  EXPECT_EQ (service.submit (api.get(), 4, model, buffer, callback), 0);
+
+  wait_callbacks (num_called, 3, m, cv);
+  EXPECT_EQ (num_called, 3);
+}
+
+/**
+ * @brief test features of submit() with error handling
+ */
+TEST (ne_core_inputservice_test, host_submit_args_n)
+{
+  std::unique_ptr<DriverAPI> api;
+  api = DriverAPI::createDriverAPI (NPUCOND_TRIV_CONN_SOCIP, 0);
+  ASSERT_NE (api.get(), nullptr);
+
+  /** create dummy model */
+  Model * model = new Model (new HWmemDevice);
+  model->setDriverAPI (api.get());
+  EXPECT_EQ (model->alloc (4096), 0);
+
+  HostInputService & service = HostInputService::getInstance ();
+
+  EXPECT_NE (service.submit (nullptr, 0, nullptr, nullptr), 0);
+  EXPECT_NE (service.submit (api.get(), 0, nullptr, nullptr), 0);
+  EXPECT_NE (service.submit (nullptr, 0, model, nullptr), 0);
+  EXPECT_EQ (service.submit (api.get(), 0, model, nullptr), 0);
+
+  sleep (1);
+}
+
+/**
+ * @brief test features of submit() with error handling
+ */
+TEST (ne_core_inputservice_test, host_submit_same_id_n)
+{
+  std::unique_ptr<DriverAPI> api;
+  api = DriverAPI::createDriverAPI (NPUCOND_TRIV_CONN_SOCIP, 0);
+  ASSERT_NE (api.get(), nullptr);
+
+  /** create dummy model */
+  Model * model = new Model (new HWmemDevice);
+  model->setDriverAPI (api.get());
+  EXPECT_EQ (model->alloc (4096), 0);
+
+  HostInputService & service = HostInputService::getInstance ();
+
+  int num_called = 0;
+  std::condition_variable cv;
+  std::mutex m;
+
+  auto callback = std::bind (test_callback_sleep, &num_called, &m, &cv);
+
+  /** saturate requests up to the maximum (in thread pool) */
+  uint32_t num_threads = ThreadPool::getInstance().getNumThreads();
+
+  for (uint32_t i = 0; i < num_threads + 1; i++)
+    EXPECT_EQ (service.submit (api.get(), i, model, nullptr, callback), 0);
+
+  /** there's a pending task with the same ID */
+  EXPECT_NE (service.submit (api.get(), num_threads, model, nullptr, callback), 0);
+  EXPECT_NE (service.submit (api.get(), num_threads, model, nullptr, callback), 0);
+
+  wait_callbacks (num_called, num_threads + 1, m, cv);
+}
+
+/**
+ * @brief test features of remove() in HostInputService
+ */
+TEST (ne_core_inputservice_test, host_remove)
+{
+  std::unique_ptr<DriverAPI> api;
+  api = DriverAPI::createDriverAPI (NPUCOND_TRIV_CONN_SOCIP, 0);
+  ASSERT_NE (api.get(), nullptr);
+
+  /** create dummy model */
+  Model * model = new Model (new HWmemDevice);
+  model->setDriverAPI (api.get());
+  EXPECT_EQ (model->alloc (4096), 0);
+
+  HostInputService & service = HostInputService::getInstance ();
+
+  int num_called = 0;
+  std::condition_variable cv;
+  std::mutex m;
+
+  auto callback = std::bind (test_callback_sleep, &num_called, &m, &cv);
+
+  /** saturate requests up to the maximum (in thread pool) */
+  uint32_t num_threads = ThreadPool::getInstance().getNumThreads();
+
+  for (uint32_t i = 0; i < num_threads + 1; i++)
+    EXPECT_EQ (service.submit (api.get(), i, model, nullptr, callback), 0);
+
+  /** remove un-resolved task */
+  EXPECT_EQ (service.remove (num_threads), 0);
+  wait_callbacks (num_called, num_threads, m, cv);
+}
+
+/**
+ * @brief test features of remove() with error handling
+ */
+TEST (ne_core_inputservice_test, host_remove_n)
+{
+  HostInputService & service = HostInputService::getInstance ();
+
+  /** no such tasks */
+  EXPECT_NE (service.remove (0), 0);
+  EXPECT_NE (service.remove (1), 0);
+  EXPECT_NE (service.remove (2), 0);
+}
+
+/**
+ * TODO: add testcases for HwInputService when implemeneted
+ */
+
+/**
+ * @brief main function for unit test
+ */
+int
+main (int argc, char **argv)
+{
+  return start_gtest (argc, argv);
+}
index 6d3b7ec..64f73e3 100644 (file)
@@ -27,14 +27,6 @@ TEST (ne_core_thread_pool_test, get_instance_singleton)
   EXPECT_EQ (&pool, &pool2);
 }
 
-static void test_callback (int * num_called,
-    std::mutex * m, std::condition_variable * cv)
-{
-  std::unique_lock<std::mutex> lock (*m);
-  (*num_called)++;
-  cv->notify_one ();
-}
-
 /**
  * @brief test enqueueTask()
  */
@@ -57,13 +49,7 @@ TEST (ne_core_thread_pool_test, enqueue_task)
 
   int max_called = 4;
   /** wait untill all callbacks are called */
-  {
-    std::unique_lock<std::mutex> lock (m);
-    cv.wait (lock, [&]() {
-      return max_called == num_called;
-    });
-  }
-
+  wait_callbacks (num_called, max_called, m, cv);
   EXPECT_EQ (num_called, max_called);
 
   /** we can reuse previous IDs */
@@ -72,12 +58,7 @@ TEST (ne_core_thread_pool_test, enqueue_task)
   EXPECT_EQ (pool.enqueueTask (new ThreadTask (0, callback)), 0);
   EXPECT_EQ (pool.enqueueTask (new ThreadTask (1, callback)), 0);
 
-  {
-    std::unique_lock<std::mutex> lock (m);
-    cv.wait (lock, [&]() {
-      return max_called == num_called;
-    });
-  }
+  wait_callbacks (num_called, max_called, m, cv);
 
   EXPECT_EQ (num_called, max_called);
 }
@@ -93,17 +74,28 @@ TEST (ne_core_thread_pool_test, enqueue_task_args_n)
   EXPECT_NE (pool.enqueueTask (nullptr), 0);
 }
 
-static void test_callback2 (int * num_called,
-    std::mutex * m, std::condition_variable * cv)
+/**
+ * @brief test enqueueTask() with error handling
+ */
+TEST (ne_core_thread_pool_test, enqueue_task_same_id_n)
 {
-  {
-    std::unique_lock<std::mutex> lock (*m);
-    (*num_called)++;
-    cv->notify_one ();
-  }
-
-  /** this worker needs to wait 3 sec for next job */
-  sleep (3);
+  ThreadPool & pool = ThreadPool::getInstance ();
+
+  int num_called = 0;
+  std::condition_variable cv;
+  std::mutex m;
+
+  uint32_t num_threads = pool.getNumThreads();
+  auto callback = std::bind (test_callback_sleep, &num_called, &m, &cv);
+
+  /** at least one request should be pending */
+  for (uint32_t i = 0; i < num_threads + 1; i++)
+    EXPECT_EQ (pool.enqueueTask (new ThreadTask (i, callback)), 0);
+
+  EXPECT_NE (pool.enqueueTask (new ThreadTask (num_threads, callback)), 0);
+  EXPECT_NE (pool.enqueueTask (new ThreadTask (num_threads, callback)), 0);
+
+  wait_callbacks (num_called, num_threads + 1, m, cv);
 }
 
 /**
@@ -118,7 +110,7 @@ TEST (ne_core_thread_pool_test, remove_task)
   std::mutex m;
 
   uint32_t num_threads = pool.getNumThreads();
-  auto callback = std::bind (test_callback2, &num_called, &m, &cv);
+  auto callback = std::bind (test_callback_sleep, &num_called, &m, &cv);
 
   /** at least one request should be pending */
   for (uint32_t i = 0; i < num_threads + 1; i++)
@@ -128,14 +120,7 @@ TEST (ne_core_thread_pool_test, remove_task)
   EXPECT_EQ (pool.removeTask (num_threads), 0);
 
   int max_called = num_threads;
-  /** wait untill all callbacks are called */
-  {
-    std::unique_lock<std::mutex> lock (m);
-    cv.wait (lock, [&]() {
-      return max_called == num_called;
-    });
-  }
-
+  wait_callbacks (num_called, max_called, m, cv);
   EXPECT_EQ (num_called, max_called);
 }
 
@@ -166,13 +151,7 @@ TEST (ne_core_thread_pool_test, remove_task_resolved_n)
   EXPECT_EQ (pool.enqueueTask (new ThreadTask (0, callback)), 0);
 
   int max_called = 1;
-  /** wait untill all callbacks are called */
-  {
-    std::unique_lock<std::mutex> lock (m);
-    cv.wait (lock, [&]() {
-      return max_called == num_called;
-    });
-  }
+  wait_callbacks (num_called, max_called, m, cv);
 
   /** remove already resolved task */
   EXPECT_NE (pool.removeTask (0), 0);
index 809bc40..4ad1d5e 100644 (file)
 #ifndef _NE_UNITTEST_UTILS_H_
 #define _NE_UNITTEST_UTILS_H_
 
+/** DO NOT INCLUDE ANY NPU-ENGINE RELATED HEADERS */
 #include <gtest/gtest.h>
+#include <condition_variable>
+#include <mutex>
 
 /**
  * @brief start gtest with tricky exception handling (due to svace)
  */
-inline int start_gtest (int argc, char **argv) {
+static inline int start_gtest (int argc, char **argv) {
   int ret = -1;
   try {
     testing::InitGoogleTest (&argc, argv);
@@ -33,4 +36,34 @@ inline int start_gtest (int argc, char **argv) {
   return ret;
 }
 
+static inline void test_callback (int * num_called,
+    std::mutex * m, std::condition_variable * cv)
+{
+  std::unique_lock<std::mutex> lock (*m);
+  (*num_called)++;
+  cv->notify_one ();
+}
+
+static inline void test_callback_sleep (int * num_called,
+    std::mutex * m, std::condition_variable * cv)
+{
+  {
+    std::unique_lock<std::mutex> lock (*m);
+    (*num_called)++;
+    cv->notify_one ();
+  }
+
+  /** this worker needs to wait 3 sec for next job */
+  sleep (3);
+}
+
+static inline void wait_callbacks (int & num_called, int max_called,
+    std::mutex & m, std::condition_variable & cv)
+{
+  std::unique_lock<std::mutex> lock (m);
+  cv.wait (lock, [&]() {
+    return max_called == num_called;
+  });
+}
+
 #endif /* _NE_UNITTEST_UTILS_H_ */