// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/cpu_affinity_posix.h"

#include <sched.h>

#include <string>

#include "base/synchronization/waitable_event.h"
#include "base/system/sys_info.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace base {

namespace {

class TestThread : public PlatformThread::Delegate {
 public:
  TestThread()
      : termination_ready_(WaitableEvent::ResetPolicy::MANUAL,
                           WaitableEvent::InitialState::NOT_SIGNALED),
        terminate_thread_(WaitableEvent::ResetPolicy::MANUAL,
                          WaitableEvent::InitialState::NOT_SIGNALED) {}
  TestThread(const TestThread&) = delete;
  TestThread& operator=(const TestThread&) = delete;
  ~TestThread() override {
    EXPECT_TRUE(terminate_thread_.IsSignaled())
        << "Need to mark thread for termination and join the underlying thread "
        << "before destroying a FunctionTestThread as it owns the "
        << "WaitableEvent blocking the underlying thread's main.";
  }

  // Grabs |thread_id_|, signals |termination_ready_|, and then waits for
  // |terminate_thread_| to be signaled before exiting.
  void ThreadMain() override {
    thread_id_ = PlatformThread::CurrentId();
    EXPECT_NE(thread_id_, kInvalidThreadId);

    // Make sure that the thread ID is the same across calls.
    EXPECT_EQ(thread_id_, PlatformThread::CurrentId());

    termination_ready_.Signal();
    terminate_thread_.Wait();

    done_ = true;
  }

  PlatformThreadId thread_id() const {
    EXPECT_TRUE(termination_ready_.IsSignaled()) << "Thread ID still unknown";
    return thread_id_;
  }

  bool IsRunning() const { return termination_ready_.IsSignaled() && !done_; }

  // Blocks until this thread is started and ready to be terminated.
  void WaitForTerminationReady() { termination_ready_.Wait(); }

  // Marks this thread for termination (callers must then join this thread to be
  // guaranteed of termination).
  void MarkForTermination() { terminate_thread_.Signal(); }

 private:
  PlatformThreadId thread_id_ = kInvalidThreadId;

  mutable WaitableEvent termination_ready_;
  WaitableEvent terminate_thread_;
  bool done_ = false;
};

}  // namespace

#if BUILDFLAG(IS_ANDROID)
#define MAYBE_SetThreadCpuAffinityMode SetThreadCpuAffinityMode
#else
// The test only considers Android device hardware models at the moment. Some
// CrOS devices on the waterfall have asymmetric CPUs that aren't covered. The
// linux-trusty-rel bot also fails to sched_setaffinity().
#define MAYBE_SetThreadCpuAffinityMode DISABLED_SetThreadCpuAffinityMode
#endif
TEST(CpuAffinityTest, MAYBE_SetThreadCpuAffinityMode) {
  // This test currently only supports Nexus 5x and Pixel devices as big.LITTLE
  // devices. For other devices, we assume that the cores are symmetric. This is
  // currently the case for the devices on our waterfalls.
  std::string device_model = SysInfo::HardwareModelName();
  int expected_total_cores = SysInfo::SysInfo::NumberOfProcessors();
  int expected_little_cores = expected_total_cores;
  if (device_model == "Nexus 5X" || device_model == "Pixel 2" ||
      device_model == "Pixel 2 XL" || device_model == "Pixel 3" ||
      device_model == "Pixel 3 XL" || device_model == "Pixel 4" ||
      device_model == "Pixel 4 XL") {
    expected_little_cores = 4;
    EXPECT_LT(expected_little_cores, expected_total_cores);
  } else if (device_model == "Pixel" || device_model == "Pixel XL") {
    expected_little_cores = 2;
    EXPECT_LT(expected_little_cores, expected_total_cores);
  } else if (device_model == "Pixel 3a" || device_model == "Pixel 3a XL") {
    expected_little_cores = 6;
    EXPECT_LT(expected_little_cores, expected_total_cores);
  } else if (device_model == "Nexus 5" || device_model == "Nexus 7") {
    // On our Nexus 5 and Nexus 7 bots, something else in the system seems to
    // set affinity for the test process, making these tests flaky
    // (crbug.com/1113964).
    return;
  }

  TestThread thread;
  PlatformThreadHandle handle;
  ASSERT_TRUE(PlatformThread::Create(0, &thread, &handle));
  thread.WaitForTerminationReady();
  ASSERT_TRUE(thread.IsRunning());

  PlatformThreadId thread_id = thread.thread_id();
  cpu_set_t set;

  EXPECT_TRUE(
      SetThreadCpuAffinityMode(thread_id, CpuAffinityMode::kLittleCoresOnly));
  EXPECT_EQ(sched_getaffinity(thread_id, sizeof(set), &set), 0);

  EXPECT_EQ(CPU_COUNT(&set), expected_little_cores);

  EXPECT_TRUE(SetThreadCpuAffinityMode(thread_id, CpuAffinityMode::kDefault));
  EXPECT_EQ(sched_getaffinity(thread_id, sizeof(set), &set), 0);

  EXPECT_EQ(CPU_COUNT(&set), expected_total_cores);

  thread.MarkForTermination();
  PlatformThread::Join(handle);
  ASSERT_FALSE(thread.IsRunning());
}

}  // namespace base
