// Copyright 2019 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/freezing/frozen_frame_aggregator.h"

#include "components/performance_manager/freezing/frozen_data.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/graph/graph_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/public/graph/node_data_describer_registry.h"

namespace performance_manager {

using LifecycleState = performance_manager::mojom::LifecycleState;

namespace {

const char kDescriberName[] = "FrozenFrameAggregator";

bool IsFrozen(const FrameNodeImpl* frame_node) {
  return frame_node->GetLifecycleState() == LifecycleState::kFrozen;
}

}  // namespace

FrozenFrameAggregator::FrozenFrameAggregator() = default;
FrozenFrameAggregator::~FrozenFrameAggregator() = default;

void FrozenFrameAggregator::OnFrameNodeAdded(const FrameNode* frame_node) {
  auto* frame_impl = FrameNodeImpl::FromNode(frame_node);
  DCHECK(!IsFrozen(frame_impl));  // A newly created node can never be frozen.
  AddOrRemoveFrame(frame_impl, 1);
}

void FrozenFrameAggregator::OnBeforeFrameNodeRemoved(
    const FrameNode* frame_node) {
  AddOrRemoveFrame(FrameNodeImpl::FromNode(frame_node), -1);
}

void FrozenFrameAggregator::OnCurrentFrameChanged(
    const FrameNode* previous_frame_node,
    const FrameNode* current_frame_node) {
  if (previous_frame_node) {
    auto* frame_impl = FrameNodeImpl::FromNode(previous_frame_node);
    CHECK(!frame_impl->IsCurrent());
    int32_t current_frame_delta = -1;
    int32_t frozen_frame_delta = IsFrozen(frame_impl) ? -1 : 0;
    UpdateFrameCounts(frame_impl, current_frame_delta, frozen_frame_delta);
  }
  if (current_frame_node) {
    auto* frame_impl = FrameNodeImpl::FromNode(current_frame_node);
    CHECK(frame_impl->IsCurrent());
    int32_t current_frame_delta = 1;
    int32_t frozen_frame_delta = IsFrozen(frame_impl) ? 1 : 0;
    UpdateFrameCounts(frame_impl, current_frame_delta, frozen_frame_delta);
  }
}

void FrozenFrameAggregator::OnFrameLifecycleStateChanged(
    const FrameNode* frame_node) {
  auto* frame_impl = FrameNodeImpl::FromNode(frame_node);
  if (!frame_impl->IsCurrent()) {
    return;
  }
  int32_t frozen_frame_delta = IsFrozen(frame_impl) ? 1 : -1;
  UpdateFrameCounts(frame_impl, 0, frozen_frame_delta);
}

void FrozenFrameAggregator::OnPassedToGraph(Graph* graph) {
  RegisterObservers(graph);
  graph->GetNodeDataDescriberRegistry()->RegisterDescriber(this,
                                                           kDescriberName);
}

void FrozenFrameAggregator::OnTakenFromGraph(Graph* graph) {
  graph->GetNodeDataDescriberRegistry()->UnregisterDescriber(this);
  UnregisterObservers(graph);
}

void FrozenFrameAggregator::OnPageNodeAdded(const PageNode* page_node) {
  DCHECK_EQ(LifecycleState::kRunning, page_node->GetLifecycleState());
  FrozenData::Create(PageNodeImpl::FromNode(page_node));
}

void FrozenFrameAggregator::OnProcessNodeAdded(
    const ProcessNode* process_node) {
  FrozenData::Create(ProcessNodeImpl::FromNode(process_node));
}

base::DictValue FrozenFrameAggregator::DescribePageNodeData(
    const PageNode* node) const {
  FrozenData& data = FrozenData::Get(PageNodeImpl::FromNode(node));
  return data.Describe();
}

base::DictValue FrozenFrameAggregator::DescribeProcessNodeData(
    const ProcessNode* node) const {
  FrozenData& data = FrozenData::Get(ProcessNodeImpl::FromNode(node));
  return data.Describe();
}

void FrozenFrameAggregator::RegisterObservers(Graph* graph) {
  // This observer presumes that it's been added before any nodes exist in the
  // graph.
  // TODO(chrisha): Add graph introspection functions to Graph.
  DCHECK(graph->HasOnlySystemNode());
  graph->AddFrameNodeObserver(this);
  graph->AddPageNodeObserver(this);
  graph->AddProcessNodeObserver(this);
}

void FrozenFrameAggregator::UnregisterObservers(Graph* graph) {
  graph->RemoveFrameNodeObserver(this);
  graph->RemovePageNodeObserver(this);
  graph->RemoveProcessNodeObserver(this);
}

void FrozenFrameAggregator::AddOrRemoveFrame(FrameNodeImpl* frame_node,
                                             int32_t delta) {
  int32_t current_frame_delta = 0;
  int32_t frozen_frame_delta = 0;
  if (frame_node->IsCurrent()) {
    current_frame_delta = delta;
    if (IsFrozen(frame_node))
      frozen_frame_delta = delta;
  }

  UpdateFrameCounts(frame_node, current_frame_delta, frozen_frame_delta);
}

void FrozenFrameAggregator::UpdateFrameCounts(FrameNodeImpl* frame_node,
                                              int32_t current_frame_delta,
                                              int32_t frozen_frame_delta) {
  // If a non-current frame is added or removed the deltas can be zero. In this
  // case the logic can be aborted early to save some effort.
  if (current_frame_delta == 0 && frozen_frame_delta == 0)
    return;

  auto* page_node = frame_node->page_node();
  auto* process_node = frame_node->process_node();
  FrozenData& page_data = FrozenData::Get(page_node);
  FrozenData& process_data = FrozenData::Get(process_node);

  // We should only have frames attached to renderer processes.
  DCHECK_EQ(content::PROCESS_TYPE_RENDERER, process_node->GetProcessType());

  // Set the page lifecycle state based on the state of the frame tree.
  if (page_data.ChangeFrameCounts(current_frame_delta, frozen_frame_delta)) {
    page_node->SetLifecycleState(base::PassKey<FrozenFrameAggregator>(),
                                 page_data.AsLifecycleState());
  }

  // Update the process state, and notify when all frames in the tree are
  // frozen.
  if (process_data.ChangeFrameCounts(current_frame_delta, frozen_frame_delta) &&
      process_data.IsFrozen()) {
    process_node->OnAllFramesInProcessFrozen(
        base::PassKey<FrozenFrameAggregator>());
  }
}

}  // namespace performance_manager
