// 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 <stdint.h>

#include <algorithm>

#include "base/functional/bind.h"
#include "base/process/process_handle.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/memory_instrumentation.h"
#include "testing/gmock/include/gmock/gmock.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/android_info.h"
// TODO: Remove this definition by targeting version 22+ NDK at build time.
// This was copied from <malloc.h> in Bionic master.
extern "C" int mallopt(int __option, int __value) __attribute__((weak));
#endif

using testing::Le;
using testing::Ge;
using testing::AllOf;
using memory_instrumentation::GlobalMemoryDump;
using memory_instrumentation::mojom::ProcessType;

namespace content {

class MemoryInstrumentationTest : public ContentBrowserTest {
 protected:
  void Navigate(Shell* shell) {
    EXPECT_TRUE(NavigateToURL(shell, GetTestUrl("", "title1.html")));
  }
};

uint64_t GetPrivateFootprintKb() {
  uint64_t private_footprint_kb = 0;
  base::RunLoop run_loop;
  memory_instrumentation::MemoryInstrumentation::GetInstance()
      ->RequestPrivateMemoryFootprint(
          base::GetCurrentProcId(),
          base::BindOnce(
              [](base::OnceClosure quit_closure, uint64_t* private_footprint_kb,
                 memory_instrumentation::mojom::RequestOutcome outcome,
                 std::unique_ptr<GlobalMemoryDump> result) {
                // The global dump should only contain the current process
                // (browser).
                EXPECT_EQ(
                    memory_instrumentation::mojom::RequestOutcome::kSuccess,
                    outcome);
                ASSERT_EQ(std::distance(result->process_dumps().begin(),
                                        result->process_dumps().end()),
                          1);
                const auto& process_dump = *result->process_dumps().begin();
                ASSERT_EQ(ProcessType::BROWSER, process_dump.process_type());

                *private_footprint_kb =
                    process_dump.os_dump().private_footprint_kb;
                std::move(quit_closure).Run();
              },
              run_loop.QuitClosure(), &private_footprint_kb));
  run_loop.Run();
  return private_footprint_kb;
}

// *SAN fake some sys calls we need meaning we never get dumps for the
// processes.
#if defined(MEMORY_SANITIZER) || defined(ADDRESS_SANITIZER) || \
    defined(THREAD_SANITIZER)
#define MAYBE_PrivateFootprintComputation DISABLED_PrivateFootprintComputation
#else
#define MAYBE_PrivateFootprintComputation PrivateFootprintComputation
#endif

// Despite the location, this test is not tracing related.
// TODO(hjd): Move this once we have a resource_coordinator folder in browser.
IN_PROC_BROWSER_TEST_F(MemoryInstrumentationTest,
                       MAYBE_PrivateFootprintComputation) {
#if BUILDFLAG(IS_ANDROID)
  // The allocator in Android N and above will defer madvising large allocations
  // until the purge interval, which is set at 1 second. If we are on N or
  // above, check whether we can use mallopt(M_PURGE) to trigger an immediate
  // purge. If we can't, skip the test.
  if (base::android::android_info::sdk_int() >=
      base::android::android_info::SDK_VERSION_NOUGAT) {
    // M_PURGE is supported on most devices running P, but not all of them. So
    // we can't check the API level but must instead attempt to trigger a purge
    // and check whether or not it succeeded.
    if (!mallopt || mallopt(M_PURGE, 0) == 0) {
      DVLOG(0) << "Skipping test - unable to trigger a purge.";
      return;
    }
  }
#endif

  Navigate(shell());

  // We have to pick a big size (>=64mb) to avoid an implementation detail of
  // malloc on MacOS which doesn't free or mark as reusable small allocations
  // after a free.
  const int64_t kAllocSize = 65 * 1024 * 1024;
  const int64_t kAllocSizeKb = kAllocSize / 1024;

  int64_t before_kb = GetPrivateFootprintKb();

  std::vector<char> buffer(kAllocSize, 1);
  // Read from the buffer to ensure it's committed.
  volatile char first = buffer.front();
  volatile char last = buffer.back();
  EXPECT_EQ(first + last, 2);

  int64_t during_kb = GetPrivateFootprintKb();

  buffer.clear();
  buffer.shrink_to_fit();

#if BUILDFLAG(IS_ANDROID)
  if (mallopt)
    mallopt(M_PURGE, 0);
#endif

  int64_t after_kb = GetPrivateFootprintKb();

  EXPECT_THAT(after_kb - before_kb,
              AllOf(Ge(-kAllocSizeKb / 10), Le(kAllocSizeKb / 10)));
  EXPECT_THAT(during_kb - before_kb,
              AllOf(Ge(kAllocSizeKb - 3000), Le(kAllocSizeKb + 3000)));
  EXPECT_THAT(during_kb - after_kb,
              AllOf(Ge(kAllocSizeKb - 3000), Le(kAllocSizeKb + 3000)));
}

}  // namespace content
