// Copyright 2022 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "iree/hal/utils/resource_set.h"

#include "iree/base/internal/debugging.h"

enum {
  IREE_HAL_RESOURCE_SET_MAX_USABLE_BLOCK_SIZE = 64 * 1024,
};

// Computes the total capacity in resources of a chunk allocated with a total
// |storage_size| (including the header).
static uint16_t iree_hal_resource_set_chunk_capacity(
    iree_host_size_t storage_size) {
  return iree_min((storage_size - sizeof(iree_hal_resource_set_chunk_t)) /
                      sizeof(iree_hal_resource_t*),
                  IREE_HAL_RESOURCE_SET_CHUNK_MAX_CAPACITY);
}

IREE_API_EXPORT iree_status_t iree_hal_resource_set_allocate(
    iree_arena_block_pool_t* block_pool, iree_hal_resource_set_t** out_set) {
  IREE_TRACE_ZONE_BEGIN(z0);

  // We could allow larger sizes (would require widening the capacity/count
  // fields in the chunk) but in real usage having even 64k is a bit too much.
  if (IREE_UNLIKELY(block_pool->usable_block_size >
                    IREE_HAL_RESOURCE_SET_MAX_USABLE_BLOCK_SIZE)) {
    IREE_RETURN_AND_END_ZONE_IF_ERROR(
        z0,
        iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                         "resource set block pool usable block size %" PRIhsz
                         " exceeds maximum %d",
                         block_pool->usable_block_size,
                         IREE_HAL_RESOURCE_SET_MAX_USABLE_BLOCK_SIZE));
  }

  // Acquire block and place the set struct at the head.
  iree_arena_block_t* block = NULL;
  iree_hal_resource_set_t* set = NULL;
  IREE_RETURN_AND_END_ZONE_IF_ERROR(
      z0, iree_arena_block_pool_acquire(block_pool, &block, (void**)&set));
  memset(set, 0, sizeof(*set));
  set->block_pool = block_pool;

  // Inline the first chunk into the block using all of the remaining space.
  // This is a special case chunk that is released back to the pool with the
  // resource set and lets us avoid an additional allocation.
  // The total capacity in resources will be less than those of chunks allocated
  // from the block pool as there's reserved space at the front for the
  // iree_hal_resource_set_t.
  iree_hal_resource_set_chunk_t* inlined_chunk =
      (iree_hal_resource_set_chunk_t*)((uint8_t*)set + sizeof(*set));
  inlined_chunk->next_chunk = NULL;
  inlined_chunk->capacity = iree_hal_resource_set_chunk_capacity(
      block_pool->usable_block_size -
      iree_host_align(sizeof(iree_hal_resource_set_t), iree_max_align_t));
  inlined_chunk->count = 0;
  set->chunk_head = inlined_chunk;

  *out_set = set;
  IREE_TRACE_ZONE_END(z0);
  return iree_ok_status();
}

static void iree_hal_resource_set_release_blocks(iree_hal_resource_set_t* set) {
  // Release all resources in all chunks and stitch together the blocks in a
  // linked list. We do this first so that we can release all of the chunks back
  // to the block pool in one operation. Ideally we'd maintain the linked list
  // in our chunks but there's some weirdness with prefix/suffix header/footers
  // that isn't worth the complexity.
  iree_arena_block_t* block_head = NULL;
  iree_arena_block_t* block_tail = NULL;
  iree_hal_resource_set_chunk_t* chunk = set->chunk_head;
  while (chunk) {
    iree_hal_resource_set_chunk_t* next_chunk = chunk->next_chunk;

    // Release all resources in the chunk.
    for (iree_host_size_t i = 0; i < chunk->count; ++i) {
      iree_hal_resource_release(chunk->resources[i]);
    }

    // Consume the chunk and add it to the block pool release linked list.
    // Note that the block metadata is located in different places depending on
    // whether this is the inline chunk or not.
    iree_arena_block_t* block = iree_arena_block_trailer(
        set->block_pool,
        iree_hal_resource_set_chunk_is_stored_inline(set, chunk)
            ? (void*)set
            : (void*)chunk);
    block->next = block_head;
    block_head = block;
    if (!block_tail) block_tail = block;

    chunk = next_chunk;
  }

  // Release all blocks back to the block pool in one operation.
  // NOTE: this invalidates the |set| memory.
  iree_arena_block_pool_t* block_pool = set->block_pool;
  iree_arena_block_pool_release(block_pool, block_head, block_tail);
}

IREE_API_EXPORT void iree_hal_resource_set_free(iree_hal_resource_set_t* set) {
  if (!set) return;
  IREE_TRACE_ZONE_BEGIN(z0);

#if defined(IREE_SANITIZER_ADDRESS)
  // Unpoison the set so we can access the chunk list.
  IREE_ASAN_UNPOISON_MEMORY_REGION(set, sizeof(iree_hal_resource_set_t));
  // Unpoison all chunks so that we can free them.
  iree_hal_resource_set_chunk_t* chunk = set->chunk_head;
  while (chunk) {
    // NOTE: as the chunk header is stored in the poisoned memory we need to
    // unpoison based only on the base pointer and the shared set information.
    IREE_ASAN_UNPOISON_MEMORY_REGION(
        iree_hal_resource_set_chunk_is_stored_inline(set, chunk) ? (void*)set
                                                                 : (void*)chunk,
        set->block_pool->usable_block_size);
    // Once unpoisoned we can read the memory to get the next chunk.
    chunk = chunk->next_chunk;
  }
#endif  // IREE_SANITIZER_ADDRESS

  // Release all resources and the arena block used by the set.
  // The set pointer is invalid after this call returns.
  iree_hal_resource_set_release_blocks(set);

  IREE_TRACE_ZONE_END(z0);
}

IREE_API_EXPORT void iree_hal_resource_set_freeze(
    iree_hal_resource_set_t* set) {
  if (!set) return;
#if defined(IREE_SANITIZER_ADDRESS)
  // Poison all chunks until the resource set is freed.
  iree_hal_resource_set_chunk_t* chunk = set->chunk_head;
  while (chunk) {
    iree_hal_resource_set_chunk_t* next_chunk = chunk->next_chunk;
    IREE_ASAN_POISON_MEMORY_REGION(
        chunk, set->block_pool->usable_block_size -
                   (iree_hal_resource_set_chunk_is_stored_inline(set, chunk)
                        ? iree_host_align(sizeof(iree_hal_resource_set_t),
                                          iree_max_align_t)
                        : 0));
    chunk = next_chunk;
  }
  // Poison the set.
  IREE_ASAN_POISON_MEMORY_REGION(set, sizeof(iree_hal_resource_set_t));
#endif  // IREE_SANITIZER_ADDRESS
}

// Retains |resource| and adds it to the main |set| list.
static iree_status_t iree_hal_resource_set_insert_retain(
    iree_hal_resource_set_t* set, iree_hal_resource_t* resource) {
  iree_hal_resource_set_chunk_t* chunk = set->chunk_head;
  if (IREE_UNLIKELY(chunk->count + 1 > chunk->capacity)) {
    // Ran out of room in the current chunk - acquire a new one and link it into
    // the list of chunks.
    iree_arena_block_t* block = NULL;
    IREE_RETURN_IF_ERROR(
        iree_arena_block_pool_acquire(set->block_pool, &block, (void**)&chunk));
    chunk->next_chunk = set->chunk_head;
    set->chunk_head = chunk;
    chunk->capacity = iree_hal_resource_set_chunk_capacity(
        set->block_pool->usable_block_size);
    chunk->count = 0;
  }

  // Retain and insert into the chunk.
  chunk->resources[chunk->count++] = resource;
  iree_hal_resource_retain(resource);
  return iree_ok_status();
}

// Scans the lookaside for the resource pointer and updates the order if found.
// If the resource was not found then it will be inserted into the main list as
// well as the MRU.
//
// This performs a full scan over the MRU and if the resource is found will
// move the resource to the front of the list before returning. Otherwise the
// resource will be retained in the main source-of-truth list.
//
// Example (hit):
//   +----+----+----+----+
//   | AA | BB | CC | DD |  resource: CC
//   +----+----+----+----+
//   scan mru to find CC:
//     found at mru[2]
//     shift prefix down 1:
//       +----+----+----+----+
//       | AA | AA | BB | DD |
//       +----+----+----+----+
//     insert resource at front:
//       +----+----+----+----+
//       | CC | AA | BB | DD |
//       +----+----+----+----+
//
// Example (miss):
//   +----+----+----+----+
//   | AA | BB | CC | DD |  resource: EE
//   +----+----+----+----+
//   scan mru to find EE: not found
//   shift set down 1:
//     +----+----+----+----+
//     | AA | AA | BB | CC |
//     +----+----+----+----+
//   insert resource at front:
//     +----+----+----+----+
//     | EE | AA | BB | CC |
//     +----+----+----+----+
//   insert resource into main list
//
// The intent here is that we can model this behavior with SIMD ops to perform
// both the scan and update using comparison, extraction, and permutation. The
// best and worst case flows will load the entire MRU into registers from a
// single cache line, do all the scanning and shifting in registers, and then
// store back to the single cache line.
//
// The scalar path below keeps the contract simple while preserving the
// fixed-cost MRU shape required by command recording paths.
// Notes:
//   As the MRU is a fixed size we can unroll it entirely and avoid any looping.
//   On a 32-bit system with uint32x4_t we only need 4 registers.
//   On a 64-bit system with uint64x2_t we also only need 4 registers - though
//   the MRU has half as many entries and we may want to go >1 cache line.
//
//   If we wanted to process more than one resource at a time we can specialize
//   the code paths to handle 1/2/4/etc resources and process in batches with
//   an optional remainder. This would increase the ratio of work performed on
//   the loaded MRU registers before we do the shift/store.
//
//   The tree sequence we likely want is something like:
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vdupq_n_u32
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vceqq_u32
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vorrq_u32
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vmaxvq_u32
//    or
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vdupq_n_u64
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vceqq_u64
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vorrq_u64
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vreinterpretq_u64_u32
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vmaxvq_u32
//   This would yield whether the pointer was found, but instead of maxing at
//   the end we can use the produced mask to extract out a single register with
//   which positions are hits and use that to then permute the registers into
//   the proper order. At the end we could use a table instruction to remap and
//   extract out a byte/bitmap of the indices that we need to insert into the
//   main set.
//
//   The shifting can be performed with
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vextq_u32
//    https://developer.arm.com/architectures/instruction-sets/intrinsics/vextq_u64
//   This takes n low elements of LHS and rest from RHS and we can cascade them
//   to shift down the whole MRU.
//
//   We can use SIMDE as a rosetta stone for getting neon/avx/wasm/etc:
//   https://github.com/simd-everywhere/simde/blob/master/simde/arm/neon/ceq.h#L591
static iree_status_t iree_hal_resource_set_insert_1(
    iree_hal_resource_set_t* set, iree_hal_resource_t* resource) {
  if (set->mru[0] == resource) return iree_ok_status();

  // Scan and hope for a hit.
  for (iree_host_size_t i = 1; i < IREE_ARRAYSIZE(set->mru); ++i) {
    if (set->mru[i] != resource) continue;
    // Hit - keep the list sorted by most->least recently used.
    // We shift the MRU down to make room at index 0 and store the
    // resource there.
    memmove(&set->mru[1], &set->mru[0], sizeof(set->mru[0]) * i);
    set->mru[0] = resource;
    return iree_ok_status();
  }

  // Miss - insert into the main list (slow path).
  // Note that we do this before updating the MRU in case allocation fails - we
  // don't want to keep the pointer around unless we've really retained it.
  IREE_RETURN_IF_ERROR(iree_hal_resource_set_insert_retain(set, resource));

  // Shift the MRU down and insert the new item at the head.
  memmove(&set->mru[1], &set->mru[0],
          sizeof(set->mru[0]) * (IREE_ARRAYSIZE(set->mru) - 1));
  set->mru[0] = resource;

  return iree_ok_status();
}

IREE_API_EXPORT iree_status_t
iree_hal_resource_set_insert(iree_hal_resource_set_t* set,
                             iree_host_size_t count, const void* resources) {
  if (!set) return iree_ok_status();
  iree_hal_resource_t* const* resource_ptrs =
      (iree_hal_resource_t* const*)resources;
  for (iree_host_size_t i = 0; i < count; ++i) {
    iree_hal_resource_t* resource = resource_ptrs[i];
    if (resource) {
      IREE_RETURN_IF_ERROR(iree_hal_resource_set_insert_1(set, resource));
    }
  }
  return iree_ok_status();
}

IREE_API_EXPORT iree_status_t iree_hal_resource_set_insert_strided(
    iree_hal_resource_set_t* set, iree_host_size_t count, const void* elements,
    iree_host_size_t offset, iree_host_size_t stride) {
  if (!set) return iree_ok_status();
  // For now we process one at a time. We should have a stride that lets us
  // amortize the cost of doing the MRU update and insertion allocation by
  // say slicing off 4/8/16/32 resources at a time etc. Today each miss that
  // requires a full insertion goes down the whole path of checking chunk
  // capacity and such.
  const uint8_t* elements_ptr = (const uint8_t*)elements;
  for (iree_host_size_t i = 0; i < count; ++i) {
    iree_hal_resource_t* resource =
        *(iree_hal_resource_t**)(elements_ptr + i * stride + offset);
    if (resource) {
      IREE_RETURN_IF_ERROR(iree_hal_resource_set_insert_1(set, resource));
    }
  }
  return iree_ok_status();
}
