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

#include "components/performance_manager/graph/process_node_impl.h"

#include <algorithm>
#include <optional>

#include "base/byte_size.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/process/process.h"
#include "base/scoped_observation.h"
#include "base/task/task_traits.h"
#include "base/test/bind.h"
#include "base/test/scoped_command_line.h"
#include "base/test/task_environment.h"
#include "base/trace_event/named_trigger.h"
#include "components/performance_manager/embedder/scoped_global_scenario_memory.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/public/render_process_host_id.h"
#include "components/performance_manager/public/render_process_host_proxy.h"
#include "components/performance_manager/scenarios/browser_performance_scenarios.h"
#include "components/performance_manager/test_support/graph/mock_process_node_observer.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "components/performance_manager/test_support/mock_graphs.h"
#include "content/public/common/content_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace performance_manager {

namespace {

class ProcessNodeImplTest : public GraphTestHarness {};

}  // namespace

TEST_F(ProcessNodeImplTest, SafeDowncast) {
  auto process = CreateNode<ProcessNodeImpl>();
  ProcessNode* node = process.get();
  EXPECT_EQ(process.get(), ProcessNodeImpl::FromNode(node));
  NodeBase* base = process.get();
  EXPECT_EQ(base, NodeBase::FromNode(node));
  EXPECT_EQ(static_cast<Node*>(node), base->ToNode());
}

using ProcessNodeImplDeathTest = ProcessNodeImplTest;

TEST_F(ProcessNodeImplDeathTest, SafeDowncast) {
  auto process = CreateNode<ProcessNodeImpl>();
  ASSERT_DEATH_IF_SUPPORTED(FrameNodeImpl::FromNodeBase(process.get()), "");
}

TEST_F(ProcessNodeImplTest, ProcessLifeCycle) {
  auto process_node = CreateNode<ProcessNodeImpl>();

  // Test the potential lifecycles of a process node.
  // First go to exited without an intervening process attached, as would happen
  // in the case the process fails to start.
  EXPECT_FALSE(process_node->GetProcess().IsValid());
  EXPECT_FALSE(process_node->GetExitStatus());
  constexpr int32_t kExitStatus = 0xF00;
  process_node->SetProcessExitStatus(kExitStatus);
  EXPECT_TRUE(process_node->GetExitStatus());
  EXPECT_EQ(kExitStatus, process_node->GetExitStatus().value());

  // Next go through PID->exit status.
  const base::Process self = base::Process::Current();
  const base::TimeTicks launch_time = base::TimeTicks::Now();
  process_node->SetProcess(self.Duplicate(), launch_time);
  EXPECT_TRUE(process_node->GetProcess().IsValid());
  EXPECT_EQ(self.Pid(), process_node->GetProcessId());
  EXPECT_EQ(launch_time, process_node->GetLaunchTime());

  // Resurrection should clear the exit status.
  EXPECT_FALSE(process_node->GetExitStatus());

  EXPECT_TRUE(process_node->GetPrivateFootprint().is_zero());
  EXPECT_TRUE(process_node->GetResidentSet().is_zero());

  process_node->set_private_footprint(base::KiB(10));
  process_node->set_resident_set(base::KiB(20));

  // Kill it again.
  // Verify that the process is cleared, but the properties stick around.
  process_node->SetProcessExitStatus(kExitStatus);
  EXPECT_FALSE(process_node->GetProcess().IsValid());
  EXPECT_EQ(self.Pid(), process_node->GetProcessId());

  EXPECT_EQ(launch_time, process_node->GetLaunchTime());
  EXPECT_EQ(base::KiB(10), process_node->GetPrivateFootprint());
  EXPECT_EQ(base::KiB(20), process_node->GetResidentSet());

  // Resurrect again and verify the launch time and measurements
  // are cleared.
  const base::TimeTicks launch2_time = launch_time + base::Seconds(1);
  process_node->SetProcess(self.Duplicate(), launch2_time);

  EXPECT_EQ(launch2_time, process_node->GetLaunchTime());
  EXPECT_TRUE(process_node->GetPrivateFootprint().is_zero());
  EXPECT_TRUE(process_node->GetResidentSet().is_zero());
}

namespace {

class MockObserver : public MockProcessNodeObserver {
 public:
  explicit MockObserver(Graph* graph = nullptr) {
    // If a `graph` is passed, automatically start observing it.
    if (graph) {
      scoped_observation_.Observe(graph);
    }
  }

  void SetNotifiedProcessNode(const ProcessNode* process_node) {
    notified_process_node_ = process_node;
  }

  void TestNotifiedProcessNode(const ProcessNode* process_node) {
    EXPECT_EQ(notified_process_node_, process_node);
  }

  void ExpectNoEdges(const ProcessNode* process_node) {
    // Node should be created without edges.
    EXPECT_TRUE(process_node->GetFrameNodes().empty());
    EXPECT_TRUE(process_node->GetWorkerNodes().empty());
  }

  const ProcessNode* TakeNotifiedProcessNode() {
    const ProcessNode* node = notified_process_node_;
    notified_process_node_ = nullptr;
    return node;
  }

 private:
  base::ScopedObservation<Graph, ProcessNodeObserver> scoped_observation_{this};
  raw_ptr<const ProcessNode, DanglingUntriaged> notified_process_node_ =
      nullptr;
};

using ::testing::_;
using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::InvokeWithoutArgs;
using ::testing::Return;

}  // namespace

TEST_F(ProcessNodeImplTest, ObserverWorks) {
  MockObserver head_obs;
  MockObserver obs;
  MockObserver tail_obs;
  graph()->AddProcessNodeObserver(&head_obs);
  graph()->AddProcessNodeObserver(&obs);
  graph()->AddProcessNodeObserver(&tail_obs);

  // Remove observers at the head and tail of the list inside a callback, and
  // expect that `obs` is still notified correctly.
  EXPECT_CALL(head_obs, OnBeforeProcessNodeAdded(_))
      .WillOnce(InvokeWithoutArgs([&] {
        graph()->RemoveProcessNodeObserver(&head_obs);
        graph()->RemoveProcessNodeObserver(&tail_obs);
      }));
  // `tail_obs` should not be notified as it was removed.
  EXPECT_CALL(tail_obs, OnBeforeProcessNodeAdded(_)).Times(0);

  // Create a page node and expect a matching call to both "OnBeforeProcessNodeAdded" and
  // "OnProcessNodeAdded".
  {
    InSequence seq;
    EXPECT_CALL(obs, OnBeforeProcessNodeAdded(_))
        .WillOnce(DoAll(Invoke(&obs, &MockObserver::SetNotifiedProcessNode),
                        Invoke(&obs, &MockObserver::ExpectNoEdges)));
    EXPECT_CALL(obs, OnProcessNodeAdded(_))
        .WillOnce(Invoke(&obs, &MockObserver::TestNotifiedProcessNode));
  }
  auto process_node = CreateNode<ProcessNodeImpl>();
  const ProcessNode* raw_process_node = process_node.get();
  EXPECT_EQ(raw_process_node, obs.TakeNotifiedProcessNode());

  // Test process creation and exit events.
  EXPECT_CALL(obs, OnProcessLifetimeChange(_));
  process_node->SetProcess(base::Process::Current(),
                           /* launch_time=*/base::TimeTicks::Now());
  EXPECT_CALL(obs, OnProcessLifetimeChange(_));
  process_node->SetProcessExitStatus(10);

  EXPECT_CALL(obs, OnMainThreadTaskLoadIsLow(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedProcessNode));
  process_node->SetMainThreadTaskLoadIsLow(true);
  EXPECT_EQ(raw_process_node, obs.TakeNotifiedProcessNode());

  // This call does nothing as the priority is initialized at HIGHEST.
  EXPECT_EQ(base::Process::Priority::kMaxValue, process_node->GetPriority());
  process_node->set_priority(base::Process::Priority::kMaxValue);

  // This call should fire a notification.
  EXPECT_CALL(obs, OnPriorityChanged(_, base::Process::Priority::kMaxValue));
  process_node->set_priority(base::Process::Priority::kMinValue);

  EXPECT_CALL(obs, OnAllFramesInProcessFrozen(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedProcessNode));
  process_node->OnAllFramesInProcessFrozenForTesting();
  EXPECT_EQ(raw_process_node, obs.TakeNotifiedProcessNode());

  // Re-entrant iteration should work.
  EXPECT_CALL(obs, OnMainThreadTaskLoadIsLow(raw_process_node))
      .WillOnce(InvokeWithoutArgs([&] {
        process_node->set_priority(base::Process::Priority::kUserBlocking);
      }));
  EXPECT_CALL(obs, OnPriorityChanged(raw_process_node, _));
  process_node->SetMainThreadTaskLoadIsLow(false);

  // Release the page node and expect a call to both "OnBeforeProcessNodeRemoved" and
  // "OnProcessNodeRemoved".
  {
    InSequence seq;
    EXPECT_CALL(obs, OnBeforeProcessNodeRemoved(raw_process_node));
    EXPECT_CALL(obs, OnProcessNodeRemoved(raw_process_node));
  }
  process_node.reset();

  graph()->RemoveProcessNodeObserver(&obs);
}

TEST_F(ProcessNodeImplTest, ConstructionArguments_Browser) {
  auto process_node = CreateNode<ProcessNodeImpl>(BrowserProcessNodeTag{});

  EXPECT_EQ(content::PROCESS_TYPE_BROWSER, process_node->GetProcessType());
}

TEST_F(ProcessNodeImplTest, ConstructionArguments_Renderer) {
  constexpr RenderProcessHostId kRenderProcessHostId =
      RenderProcessHostId(0xF0B);
  auto process_node = CreateNode<ProcessNodeImpl>(
      RenderProcessHostProxy::CreateForTesting(kRenderProcessHostId));

  EXPECT_EQ(content::PROCESS_TYPE_RENDERER, process_node->GetProcessType());
  EXPECT_EQ(kRenderProcessHostId,
            process_node->GetRenderProcessHostProxy().render_process_host_id());
}

TEST_F(ProcessNodeImplTest, ConstructionArguments_NonRenderer) {
  constexpr BrowserChildProcessHostId kBrowserChildProcessHostId =
      BrowserChildProcessHostId(0xF0B);
  auto process_node = CreateNode<ProcessNodeImpl>(
      content::PROCESS_TYPE_GPU, BrowserChildProcessHostProxy::CreateForTesting(
                                     kBrowserChildProcessHostId));

  EXPECT_EQ(content::PROCESS_TYPE_GPU, process_node->GetProcessType());
  EXPECT_EQ(kBrowserChildProcessHostId,
            process_node->GetBrowserChildProcessHostProxy()
                .browser_child_process_host_id());
}

TEST_F(ProcessNodeImplTest, PublicInterface) {
  auto process_node = CreateNode<ProcessNodeImpl>();
  const ProcessNode* public_process_node = process_node.get();

  // Create a small frame-tree so that GetFrameNodes can be well tested.
  auto page_node = CreateNode<PageNodeImpl>();
  auto main_frame_node =
      CreateFrameNodeAutoId(process_node.get(), page_node.get());
  auto child_frame_node = CreateFrameNodeAutoId(
      process_node.get(), page_node.get(), main_frame_node.get());


  const std::string kMetricsName("TestUtilityProcess");
  process_node->SetProcessMetricsName(kMetricsName);
  EXPECT_EQ(process_node->GetMetricsName(), kMetricsName);

  process_node->SetMainThreadTaskLoadIsLow(true);
  EXPECT_TRUE(process_node->GetMainThreadTaskLoadIsLow());

  // For properties returning nodes, simply test that the public interface impls
  // yield the same result as their private counterpart.

  auto frame_nodes = process_node->frame_nodes();
  auto public_frame_nodes = public_process_node->GetFrameNodes();
  EXPECT_EQ(frame_nodes.size(), public_frame_nodes.size());
  for (const FrameNodeImpl* frame_node : frame_nodes) {
    const FrameNode* public_frame_node = frame_node;
    EXPECT_TRUE(std::ranges::contains(public_frame_nodes, public_frame_node));
  }
}

TEST_F(ProcessNodeImplTest, InitializeChildProcessCoordination) {
  auto process_node = CreateNode<ProcessNodeImpl>();

  // No global memory mapped. ProcessNodeImpl automatically creates a process
  // memory region on request.
  process_node->InitializeChildProcessCoordination(
      base::BindLambdaForTesting(
          [&](base::ReadOnlySharedMemoryRegion global_region,
              base::ReadOnlySharedMemoryRegion process_region) {
            EXPECT_FALSE(global_region.IsValid());
            EXPECT_TRUE(process_region.IsValid());
          })
          .Then(task_env().QuitClosure()));
  task_env().RunUntilQuit();

  // Map global memory.
  ScopedGlobalScenarioMemory global_shared_memory;
  process_node->InitializeChildProcessCoordination(
      base::BindLambdaForTesting(
          [&](base::ReadOnlySharedMemoryRegion global_region,
              base::ReadOnlySharedMemoryRegion process_region) {
            EXPECT_TRUE(global_region.IsValid());
            EXPECT_TRUE(process_region.IsValid());
          })
          .Then(task_env().QuitClosure()));
  task_env().RunUntilQuit();

  // In single process mode, memory shouldn't be shared even if it's mapped,
  // because the request isn't actually sent from a different process.
  base::test::ScopedCommandLine scoped_command_line;
  scoped_command_line.GetProcessCommandLine()->AppendSwitch(
      switches::kSingleProcess);
  process_node->InitializeChildProcessCoordination(
      base::BindLambdaForTesting(
          [&](base::ReadOnlySharedMemoryRegion global_region,
              base::ReadOnlySharedMemoryRegion process_region) {
            EXPECT_FALSE(global_region.IsValid());
            EXPECT_FALSE(process_region.IsValid());
          })
          .Then(task_env().QuitClosure()));
  task_env().RunUntilQuit();
}

}  // namespace performance_manager
