// Copyright 2019 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 <vector>

#include "base/bind.h"
#include "base/no_destructor.h"
#include "base/run_loop.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "base/threading/platform_thread.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/metrics/call_stack_profile_metrics_provider.h"
#include "components/version_info/channel.h"
#include "third_party/metrics_proto/sampled_profile.pb.h"

namespace {

// Class that intercepts and stores profiles provided to the
// CallStackProfileMetricsProvider. Intercept() is invoked on the profiler
// thread while FetchProfiles() is invoked on the main thread.
class ProfileInterceptor {
 public:
  // Get the static object instance. This object must leak because there is no
  // synchronization between it and the profiler thread which can invoke
  // Intercept at any time.
  static ProfileInterceptor& GetInstance() {
    static base::NoDestructor<ProfileInterceptor> instance;
    return *instance;
  }

  void Intercept(metrics::SampledProfile profile) {
    base::AutoLock lock(lock_);
    profiles_.push_back(std::move(profile));
  }

  std::vector<metrics::SampledProfile> FetchProfiles() {
    base::AutoLock lock(lock_);
    std::vector<metrics::SampledProfile> profiles;
    profiles.swap(profiles_);
    return profiles;
  }

 private:
  base::Lock lock_;
  std::vector<metrics::SampledProfile> profiles_ GUARDED_BY(lock_);
};

class StackSamplingBrowserTest : public InProcessBrowserTest {
 public:
  void SetUp() override {
    // Arrange to intercept the CPU profiles at the time they're provided to the
    // metrics component.
    metrics::CallStackProfileMetricsProvider::
        SetCpuInterceptorCallbackForTesting(base::BindRepeating(
            &ProfileInterceptor::Intercept,
            base::Unretained(&ProfileInterceptor::GetInstance())));
    InProcessBrowserTest::SetUp();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Enable the special browser test mode.
    command_line->AppendSwitchASCII(switches::kStartStackProfiler,
                                    switches::kStartStackProfilerBrowserTest);
  }
};

// Wait for a profile with the specified properties. Checks once per second
// until the profile is seen or we time out.
bool WaitForProfile(metrics::SampledProfile::TriggerEvent trigger_event,
                    metrics::Process process,
                    metrics::Thread thread) {
  // Profiling is only enabled for trunk builds and canary and dev channels.
  // Perform an early return and pass the test for the other channels.
  switch (chrome::GetChannel()) {
    case version_info::Channel::UNKNOWN:
    case version_info::Channel::CANARY:
    case version_info::Channel::DEV:
      break;

    default:
      return true;
  }

  // The profiling duration is one second when enabling browser test mode via
  // the kStartStackProfilerBrowserTest switch argument. We expect to see the
  // profiles shortly thereafter, but wait up to 30 seconds to give ample time
  // to avoid flaky failures.
  int seconds_to_wait = 30;
  do {
    std::vector<metrics::SampledProfile> profiles =
        ProfileInterceptor::GetInstance().FetchProfiles();
    const bool was_received =
        std::find_if(profiles.begin(), profiles.end(),
                     [&](const metrics::SampledProfile& profile) {
                       return profile.trigger_event() == trigger_event &&
                              profile.process() == process &&
                              profile.thread() == thread;
                     }) != profiles.end();
    if (was_received)
      return true;
    base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
    // Manually spinning message loop is fine here because the main thread
    // message loop will not be continuously busy at Chrome startup, and we will
    // spin it enough over 30 seconds to ensure that any necessary processing is
    // done.
    base::RunLoop().RunUntilIdle();
  } while (--seconds_to_wait > 0);

  return false;
}

}  // namespace

// Check that we receive startup profiles in the browser process for profiled
// processes/threads. We've seen multiple breakages previously where profiles
// were dropped as a result of bugs introduced by mojo refactorings.

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, BrowserProcessMainThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::BROWSER_PROCESS, metrics::MAIN_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, BrowserProcessIOThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::BROWSER_PROCESS, metrics::IO_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, GpuProcessMainThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::GPU_PROCESS, metrics::MAIN_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, GpuProcessIOThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::GPU_PROCESS, metrics::IO_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, GpuProcessCompositorThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::GPU_PROCESS, metrics::COMPOSITOR_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, RendererProcessMainThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::RENDERER_PROCESS, metrics::MAIN_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest, RendererProcessIOThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::RENDERER_PROCESS, metrics::IO_THREAD));
}

IN_PROC_BROWSER_TEST_F(StackSamplingBrowserTest,
                       RendererProcessCompositorThread) {
  EXPECT_TRUE(WaitForProfile(metrics::SampledProfile::PROCESS_STARTUP,
                             metrics::RENDERER_PROCESS,
                             metrics::COMPOSITOR_THREAD));
}
