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

#include "gpu/command_buffer/service/passthrough_program_cache.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <string>

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/memory_pressure_listener_registry.h"
#include "base/memory_coordinator/memory_coordinator_features.h"
#include "base/memory_coordinator/test_memory_consumer_registry.h"
#include "base/memory_coordinator/utils.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "gpu/command_buffer/common/gles2_cmd_format.h"
#include "gpu/command_buffer/common/shm_count.h"
#include "gpu/command_buffer/service/decoder_client.h"
#include "gpu/command_buffer/service/gl_utils.h"
#include "gpu/command_buffer/service/gpu_service_test.h"
#include "gpu/command_buffer/service/test_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_mock.h"

using ::testing::_;
using ::testing::Invoke;
using ::testing::SetArgPointee;

namespace gpu {
namespace gles2 {

class PassthroughProgramCacheTest : public GpuServiceTest,
                                    public DecoderClient {
 public:
  static const size_t kCacheSizeBytes = 1024;
  static const bool kDisableGpuDiskCache = false;

  PassthroughProgramCacheTest()
      : cache_(kCacheSizeBytes, kDisableGpuDiskCache), blob_count_(0) {}
  ~PassthroughProgramCacheTest() override {}

  void OnConsoleMessage(int32_t id, const std::string& message) override {}
  void CacheBlob(gpu::GpuDiskCacheType type,
                 const std::string& key,
                 const std::string& blob) override {}
  void OnFenceSyncRelease(uint64_t release) override {}
  void OnDescheduleUntilFinished() override {}
  void OnRescheduleAfterFinished() override {}
  void ScheduleGrContextCleanup() override {}
  void HandleReturnData(base::span<const uint8_t> data) override {}

  int32_t blob_count() { return blob_count_; }

 protected:
  std::string MakeKey(size_t len, uint8_t start_byte) {
    std::string binary_key(len, '.');

    for (size_t i = 0; i < len; ++i)
      binary_key[i] = static_cast<uint8_t>((start_byte + i) & 0xFF);

    return binary_key;
  }

  std::string MakeBlob(size_t len, uint8_t start_byte) {
    return MakeKey(len, start_byte);
  }

  void Set(const std::string& binary_key, const std::string& binary_blob) {
    PassthroughProgramCache::BlobCacheSet(
        binary_key.data(), static_cast<EGLsizeiANDROID>(binary_key.size()),
        binary_blob.data(), static_cast<EGLsizeiANDROID>(binary_blob.size()));
  }

  std::string Get(const std::string& binary_key) {
    EGLsizeiANDROID key_size = static_cast<EGLsizeiANDROID>(binary_key.size());
    EGLsizeiANDROID blob_size = PassthroughProgramCache::BlobCacheGet(
        binary_key.data(), key_size, nullptr, 0);

    if (blob_size <= 0)
      return "";

    // Directly use std::string for the blob.
    std::string binary_blob;
    binary_blob.resize(blob_size);
    EGLsizeiANDROID blob_size_after = PassthroughProgramCache::BlobCacheGet(
        binary_key.data(), key_size, binary_blob.data(), blob_size);

    EXPECT_EQ(blob_size, blob_size_after);

    return binary_blob;
  }

  void SimulateMemoryPressure(base::MemoryPressureLevel level) {
    int percentage = 100;
    switch (level) {
      case base::MEMORY_PRESSURE_LEVEL_NONE:
        percentage = 100;
        break;
      case base::MEMORY_PRESSURE_LEVEL_MODERATE:
        percentage = base::kModerateMemoryPressureThreshold;
        break;
      case base::MEMORY_PRESSURE_LEVEL_CRITICAL:
        percentage = base::kCriticalMemoryPressureThreshold;
        break;
    }
    base::RunLoop run_loop;
    test_memory_consumer_registry_.NotifyUpdateMemoryLimitAsync(
        percentage, base::DoNothing());
    test_memory_consumer_registry_.NotifyReleaseMemoryAsync(
        run_loop.QuitClosure());
    run_loop.Run();
  }

  base::MemoryPressureListenerRegistry memory_pressure_listener_registry_;
  base::TestMemoryConsumerRegistry test_memory_consumer_registry_;
  PassthroughProgramCache cache_;
  int32_t blob_count_;
};

TEST_F(PassthroughProgramCacheTest, LoadProgram) {
  const int kKeyLength = 10;
  const int kBlobLength = 20;

  for (uint8_t key_start = 0; key_start < 5; ++key_start) {
    std::string binary_key = MakeKey(kKeyLength, key_start);
    std::string binary_blob = MakeBlob(kBlobLength, key_start + 10);

    // Encode the strings to pretend like they came from disk.
    std::string key_string_64 = base::Base64Encode(binary_key);
    std::string value_string_64 = base::Base64Encode(binary_blob);

    cache_.LoadProgram(key_string_64, value_string_64);

    // Make sure the blob was inserted.
    EXPECT_EQ(binary_blob, Get(binary_key));

    // Test that similar keys but with different length are not retrieved
    // (especially making sure the '\0' character at the beginning of the key is
    // not causing confusion).
    for (size_t i = 0; i <= kKeyLength * 2; ++i) {
      if (i != kKeyLength) {
        EXPECT_EQ("", Get(MakeKey(i, key_start)));
      }
    }
  }
}

TEST_F(PassthroughProgramCacheTest, BlobSet) {
  const int kKeyLength = 10;
  const int kBlobLength = 20;

  for (uint8_t key_start = 0; key_start < 5; ++key_start) {
    std::string binary_key = MakeKey(kKeyLength, key_start);
    std::string binary_blob = MakeBlob(kBlobLength, key_start + 25);

    Set(binary_key, binary_blob);

    // Make sure the blob was inserted.
    EXPECT_EQ(binary_blob, Get(binary_key));

    // Test that similar keys but with different length are not retrieved
    // (especially making sure the '\0' character at the beginning of the key is
    // not causing confusion).
    for (size_t i = 0; i <= kKeyLength * 2; ++i) {
      if (i != kKeyLength) {
        EXPECT_EQ("", Get(MakeKey(i, key_start)));
      }
    }
  }
}

TEST_F(PassthroughProgramCacheTest, Eviction) {
  const int kKeyLength = 10;
  const int kBlobLength = 20;

  std::string binary_key = MakeKey(kKeyLength, 0);
  std::string binary_blob = MakeBlob(kBlobLength, 0);

  Set(binary_key, binary_blob);
  EXPECT_EQ(binary_blob, Get(binary_key));

  std::string evicting_binary_key = MakeKey(kKeyLength, 1);
  std::string evicting_binary_blob =
      MakeBlob(kCacheSizeBytes - kBlobLength + 1, 0);

  Set(evicting_binary_key, evicting_binary_blob);

  // Make sure the new blob is inserted.
  EXPECT_EQ(evicting_binary_blob, Get(evicting_binary_key));
  // And the old blob is removed.
  EXPECT_EQ("", Get(binary_key));
}

TEST_F(PassthroughProgramCacheTest, Clear) {
  const int kKeyLength = 10;
  const int kBlobLength = 20;

  std::string binary_key = MakeKey(kKeyLength, 1);
  std::string binary_blob = MakeBlob(kBlobLength, 1);

  Set(binary_key, binary_blob);
  EXPECT_EQ(binary_blob, Get(binary_key));

  cache_.Clear();

  // Make sure the blob is removed.
  EXPECT_EQ("", Get(binary_key));
}

TEST_F(PassthroughProgramCacheTest, OverwriteOnNewSave) {
  const int kKeyLength = 10;
  const int kBlobLength = 20;
  const int kBlobLength2 = 15;
  const int kBlobLength3 = 25;

  std::string binary_key = MakeKey(kKeyLength, 0);
  std::string binary_blob = MakeBlob(kBlobLength, 0);
  std::string binary_blob2 = MakeBlob(kBlobLength2, 78);
  std::string binary_blob3 = MakeBlob(kBlobLength3, 113);

  Set(binary_key, binary_blob);
  EXPECT_EQ(binary_blob, Get(binary_key));

  Set(binary_key, binary_blob2);

  // Make sure the new blob replaces the old one.
  EXPECT_EQ(binary_blob2, Get(binary_key));

  Set(binary_key, binary_blob3);
  EXPECT_EQ(binary_blob3, Get(binary_key));
}

TEST_F(PassthroughProgramCacheTest, Trim) {
  const int kKeyLength = 10;
  const int kBlobLength = 20;

  std::string binary_key = MakeKey(kKeyLength, 10);
  std::string binary_blob = MakeBlob(kBlobLength, 11);

  std::string binary_key2 = MakeKey(kKeyLength, 12);
  std::string binary_blob2 = MakeBlob(kBlobLength, 13);

  Set(binary_key, binary_blob);
  Set(binary_key2, binary_blob2);

  EXPECT_EQ(binary_blob, Get(binary_key));
  EXPECT_EQ(binary_blob2, Get(binary_key2));

  // Trimming to exact size of the two blobs shouldn't evict anything.
  cache_.Trim(kBlobLength * 2);
  EXPECT_EQ(binary_blob, Get(binary_key));
  EXPECT_EQ(binary_blob2, Get(binary_key2));

  // Trimming to even a byte under that should evict the first (oldest) blob.
  cache_.Trim(kBlobLength * 2 - 1);
  EXPECT_EQ("", Get(binary_key));
  EXPECT_EQ(binary_blob2, Get(binary_key2));

  // Trimming to exact size of the blob that's left shouldn't evict anything.
  cache_.Trim(kBlobLength);
  EXPECT_EQ("", Get(binary_key));
  EXPECT_EQ(binary_blob2, Get(binary_key2));

  // Trimming more should evict the second blob too.
  cache_.Trim(kBlobLength - 1);
  EXPECT_EQ("", Get(binary_key));
  EXPECT_EQ("", Get(binary_key2));

  // Insert the blobs again and this time go from full directly to 0 limit.
  Set(binary_key, binary_blob);
  Set(binary_key2, binary_blob2);

  EXPECT_EQ(binary_blob, Get(binary_key));
  EXPECT_EQ(binary_blob2, Get(binary_key2));

  cache_.Trim(0);
  EXPECT_EQ("", Get(binary_key));
  EXPECT_EQ("", Get(binary_key2));
}

TEST_F(PassthroughProgramCacheTest, MemoryPressure) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(base::kStatefulMemoryPressure);

  const int kKeyLength = 10;
  // Compute a blob length giving us a cache capacity of 4 entries.
  const int kCacheCapacity = 4;
  const int kBlobLength = kCacheSizeBytes / kCacheCapacity;

  // Fill the cache.
  for (int i = 0; i < kCacheCapacity; i++) {
    std::string binary_key = MakeKey(kKeyLength, i);
    std::string binary_blob = MakeBlob(kBlobLength, i);
    Set(binary_key, binary_blob);
  }

  EXPECT_NE("", Get(MakeKey(kKeyLength, 0)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 1)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 2)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 3)));

  SimulateMemoryPressure(base::MEMORY_PRESSURE_LEVEL_MODERATE);

  // Cache size should be reduced to 1/4 of the max size under moderate
  // pressure.
  EXPECT_EQ("", Get(MakeKey(kKeyLength, 0)));
  EXPECT_EQ("", Get(MakeKey(kKeyLength, 1)));
  EXPECT_EQ("", Get(MakeKey(kKeyLength, 2)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 3)));

  // Adding an item removes the previous one.
  Set(MakeKey(kKeyLength, 4), MakeBlob(kBlobLength, 4));

  EXPECT_EQ("", Get(MakeKey(kKeyLength, 3)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 4)));

  SimulateMemoryPressure(base::MEMORY_PRESSURE_LEVEL_CRITICAL);

  EXPECT_EQ("", Get(MakeKey(kKeyLength, 4)));

  // Verify new insertions are rejected.
  Set(MakeKey(kKeyLength, 5), MakeBlob(kBlobLength, 5));

  EXPECT_EQ("", Get(MakeKey(kKeyLength, 5)));

  // Return memory pressure state to normal.
  SimulateMemoryPressure(base::MEMORY_PRESSURE_LEVEL_NONE);

  for (int i = 6; i < 6 + kCacheCapacity; i++) {
    std::string binary_key = MakeKey(kKeyLength, i);
    std::string binary_blob = MakeBlob(kBlobLength, i);
    Set(binary_key, binary_blob);
  }

  EXPECT_NE("", Get(MakeKey(kKeyLength, 6)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 7)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 8)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 9)));
}

TEST_F(PassthroughProgramCacheTest, MemoryPressureStateful) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(base::kStatefulMemoryPressure);

  const int kKeyLength = 10;
  // Compute a blob length giving us a capacity of 4 entries.
  const int kCacheCapacity = 4;
  const int kBlobLength = kCacheSizeBytes / kCacheCapacity;

  // Fill the cache.
  for (int i = 0; i < kCacheCapacity; i++) {
    std::string binary_key = MakeKey(kKeyLength, i);
    std::string binary_blob = MakeBlob(kBlobLength, i);
    Set(binary_key, binary_blob);
  }

  EXPECT_NE("", Get(MakeKey(kKeyLength, 0)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 1)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 2)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 3)));

  // Moderate pressure (50%).
  base::RunLoop run_loop;
  test_memory_consumer_registry_.NotifyUpdateMemoryLimitAsync(
      base::kModerateMemoryPressureThreshold, run_loop.QuitClosure());
  run_loop.Run();

  // Verify that NO memory is released yet!
  EXPECT_NE("", Get(MakeKey(kKeyLength, 0)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 1)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 2)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 3)));

  // Now release memory.
  base::RunLoop run_loop2;
  test_memory_consumer_registry_.NotifyReleaseMemoryAsync(
      run_loop2.QuitClosure());
  run_loop2.Run();

  // Now memory should be released!
  // At 50% limit, size should be 1/4 (1 item).
  EXPECT_EQ("", Get(MakeKey(kKeyLength, 0)));
  EXPECT_EQ("", Get(MakeKey(kKeyLength, 1)));
  EXPECT_EQ("", Get(MakeKey(kKeyLength, 2)));
  EXPECT_NE("", Get(MakeKey(kKeyLength, 3)));
}

}  // namespace gles2
}  // namespace gpu
