// Copyright (c) 2012 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/callback.h"
#include "base/command_line.h"
#include "base/path_service.h"
#include "content/public/browser/gpu_data_manager.h"
#include "content/public/browser/gpu_data_manager_observer.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/shell.h"
#include "content/test/content_browser_test.h"
#include "content/test/content_browser_test_utils.h"
#include "gpu/command_buffer/service/gpu_switches.h"
#include "gpu/config/gpu_test_config.h"
#include "net/base/net_util.h"

namespace content {

// Run the tests with a memory limit of 256MB, and give
// and extra 4MB of wiggle-room for over-allocation.
const char* kMemoryLimitMBSwitch = "256";
const size_t kMemoryLimitMB = 256;
const size_t kSingleTabLimitMB = 128;
const size_t kWiggleRoomMB = 4;

// Observer to report GPU memory usage when requested.
class GpuMemoryBytesAllocatedObserver : public GpuDataManagerObserver {
 public:
  GpuMemoryBytesAllocatedObserver()
      : bytes_allocated_(0) {
  }

  virtual ~GpuMemoryBytesAllocatedObserver() {
  }

  virtual void OnVideoMemoryUsageStatsUpdate(
      const GPUVideoMemoryUsageStats& video_memory_usage_stats) OVERRIDE {
    bytes_allocated_ = video_memory_usage_stats.bytes_allocated;
    message_loop_runner_->Quit();
  }

  size_t GetBytesAllocated() {
    message_loop_runner_ = new MessageLoopRunner;
    GpuDataManager::GetInstance()->AddObserver(this);
    GpuDataManager::GetInstance()->RequestVideoMemoryUsageStatsUpdate();
    message_loop_runner_->Run();
    GpuDataManager::GetInstance()->RemoveObserver(this);
    message_loop_runner_ = NULL;
    return bytes_allocated_;
  }

 private:
  size_t bytes_allocated_;
  scoped_refptr<MessageLoopRunner> message_loop_runner_;
};

class GpuMemoryTest : public ContentBrowserTest {
 public:
  GpuMemoryTest()
      : allow_tests_to_run_(false),
        has_used_first_shell_(false) {
  }
  virtual ~GpuMemoryTest() {
  }

  virtual void SetUpInProcessBrowserTestFixture() OVERRIDE {
    base::FilePath test_dir;
    ASSERT_TRUE(PathService::Get(DIR_TEST_DATA, &test_dir));
    gpu_test_dir_ = test_dir.AppendASCII("gpu");
  }

  virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
    command_line->AppendSwitch(switches::kEnableLogging);
    command_line->AppendSwitch(switches::kForceCompositingMode);
    command_line->AppendSwitchASCII(switches::kForceGpuMemAvailableMb,
                                    kMemoryLimitMBSwitch);
    // Only run this on GPU bots for now. These tests should work with
    // any GPU process, but may be slow.
    if (command_line->HasSwitch(switches::kUseGpuInTests)) {
      allow_tests_to_run_ = true;
    }
    // Don't enable these tests on Android just yet (they use lots of memory and
    // may not be stable).
#if defined(OS_ANDROID)
    allow_tests_to_run_ = false;
#endif
  }

  enum PageType {
    PAGE_CSS3D,
    PAGE_WEBGL,
  };

  // Load a page and consume a specified amount of GPU memory.
  void LoadPage(Shell* tab_to_load,
                PageType page_type,
                size_t mb_to_use) {
    base::FilePath url;
    switch (page_type) {
      case PAGE_CSS3D:
        url = gpu_test_dir_.AppendASCII("mem_css3d.html");
        break;
      case PAGE_WEBGL:
        url = gpu_test_dir_.AppendASCII("mem_webgl.html");
        break;
    }

    NavigateToURL(tab_to_load, net::FilePathToFileURL(url));
    std::ostringstream js_call;
    js_call << "useGpuMemory(";
    js_call << mb_to_use;
    js_call << ");";
    std::string message;
    ASSERT_TRUE(ExecuteScriptInFrameAndExtractString(
        tab_to_load->web_contents(), std::string(), js_call.str(), &message));
    EXPECT_EQ("DONE_USE_GPU_MEMORY", message);
  }

  // Create a new tab.
  Shell* CreateNewTab() {
    // The ContentBrowserTest will create one shell by default, use that one
    // first so that we don't confuse the memory manager into thinking there
    // are more windows than there are.
    Shell* new_tab = has_used_first_shell_ ? CreateBrowser() : shell();
    has_used_first_shell_ = true;
    tabs_.insert(new_tab);
    visible_tabs_.insert(new_tab);
    return new_tab;
  }

  void SetTabBackgrounded(Shell* tab_to_background) {
    ASSERT_TRUE(
        visible_tabs_.find(tab_to_background) != visible_tabs_.end());
    visible_tabs_.erase(tab_to_background);
    tab_to_background->web_contents()->WasHidden();
  }

  bool MemoryUsageInRange(size_t low, size_t high) {
    FinishGpuMemoryChanges();
    size_t memory_usage_bytes = GetMemoryUsageMbytes();

    // If it's not immediately the case that low <= usage <= high, then
    // allow
    // Because we haven't implemented the full delay in FinishGpuMemoryChanges,
    // keep re-reading the GPU memory usage for 2 seconds before declaring
    // failure.
    base::Time start_time = base::Time::Now();
    while (low > memory_usage_bytes || memory_usage_bytes > high) {
      memory_usage_bytes = GetMemoryUsageMbytes();
      base::TimeDelta delta = base::Time::Now() - start_time;
      if (delta.InMilliseconds() >= 2000)
        break;
    }

    return (low <= memory_usage_bytes && memory_usage_bytes <= high);
  }

  bool AllowTestsToRun() const {
    return allow_tests_to_run_;
  }

 private:
  void FinishGpuMemoryChanges() {
    // This should wait until all effects of memory management complete.
    // We will need to wait until all
    // 1. pending commits from the main thread to the impl thread in the
    //    compositor complete (for visible compositors).
    // 2. allocations that the renderer's impl thread will make due to the
    //    compositor and WebGL are completed.
    // 3. pending GpuMemoryManager::Manage() calls to manage are made.
    // 4. renderers' OnMemoryAllocationChanged callbacks in response to
    //    manager are made.
    // Each step in this sequence can cause trigger the next (as a 1-2-3-4-1
    // cycle), so we will need to pump this cycle until it stabilizes.

    // Pump the cycle 8 times (in principle it could take an infinite number
    // of iterations to settle).
    for (size_t pump_it = 0; pump_it < 8; ++pump_it) {
      // Wait for a RequestAnimationFrame to complete from all visible tabs
      // for stage 1 of the cycle.
      for (std::set<Shell*>::iterator it = visible_tabs_.begin();
           it != visible_tabs_.end();
           ++it) {
        std::string js_call(
            "window.webkitRequestAnimationFrame(function() {"
            "  domAutomationController.setAutomationId(1);"
            "  domAutomationController.send(\"DONE_RAF\");"
            "})");
        std::string message;
        ASSERT_TRUE(ExecuteScriptInFrameAndExtractString(
            (*it)->web_contents(), std::string(), js_call, &message));
        EXPECT_EQ("DONE_RAF", message);
      }
      // TODO(ccameron): send an IPC from Browser -> Renderer (delay it until
      // painting finishes) -> GPU process (delay it until any pending manages
      // happen) -> All Renderers -> Browser to flush parts 2, 3, and 4.
    }
  }

  size_t GetMemoryUsageMbytes() {
    GpuMemoryBytesAllocatedObserver observer;
    observer.GetBytesAllocated();
    return observer.GetBytesAllocated() / 1048576;
  }

  bool allow_tests_to_run_;
  std::set<Shell*> tabs_;
  std::set<Shell*> visible_tabs_;
  bool has_used_first_shell_;
  base::FilePath gpu_test_dir_;
};

// When trying to load something that doesn't fit into our total GPU memory
// limit, we shouldn't exceed that limit.
IN_PROC_BROWSER_TEST_F(GpuMemoryTest, SingleWindowDoesNotExceedLimit) {
  if (!AllowTestsToRun())
    return;

  Shell* tab = CreateNewTab();
  LoadPage(tab, PAGE_CSS3D, kMemoryLimitMB);
  // Make sure that the CSS3D page maxes out a single tab's budget (otherwise
  // the test doesn't test anything) but still stays under the limit.
  EXPECT_TRUE(MemoryUsageInRange(
     kSingleTabLimitMB - kWiggleRoomMB,
     kMemoryLimitMB + kWiggleRoomMB));
}

}  // namespace content