// Copyright 2023 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/compiler/Dialect/HAL/IR/HALDialect.h"
#include "iree/compiler/Dialect/HAL/IR/HALOps.h"
#include "iree/compiler/Dialect/HAL/Transforms/Passes.h"
#include "iree/compiler/Dialect/Stream/IR/StreamOps.h"
#include "iree/compiler/Dialect/Util/IR/UtilDialect.h"
#include "iree/compiler/Dialect/Util/IR/UtilOps.h"
#include "iree/compiler/Utils/FlatbufferUtils.h"
#include "iree/schemas/instruments/dispatch.h"
#include "iree/schemas/instruments/dispatch_def_builder.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/IR/Attributes.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Diagnostics.h"
#include "mlir/Pass/Pass.h"

namespace mlir::iree_compiler::IREE::HAL {

#define GEN_PASS_DEF_MATERIALIZEDISPATCHINSTRUMENTATIONPASS
#include "iree/compiler/Dialect/HAL/Transforms/Passes.h.inc"

namespace {

static std::string getAttrStr(Attribute attr) {
  if (!attr) {
    return "";
  }
  std::string result;
  llvm::raw_string_ostream os(result);
  attr.print(os, /*elideType=*/true);
  return result;
}

static std::string getOpStr(Operation *op) {
  std::string result;
  llvm::raw_string_ostream os(result);
  OpPrintingFlags flags;
  flags.useLocalScope();
  flags.assumeVerified();
  op->print(os, flags);
  return result;
}

// Returns a data vector containing a iree_idbts_chunk_header_t with |type|.
// The declared |contentLength| excludes padding.
static Value createChunkHeader(Location loc, iree_idbts_chunk_type_t type,
                               uint64_t contentLength, OpBuilder &builder) {
  iree_idbts_chunk_header_t header;
  header.magic = IREE_IDBTS_CHUNK_MAGIC;
  header.type = type;
  header.version = 0;
  header.content_length = contentLength;

  auto dataAttr = DenseElementsAttr::getFromRawBuffer(
      VectorType::get({sizeof(header)}, builder.getI8Type()),
      ArrayRef<char>(reinterpret_cast<const char *>(&header), sizeof(header)));

  return IREE::Util::BufferConstantOp::create(
      builder, loc, /*name=*/nullptr, dataAttr, builder.getIndexAttr(16),
      /*mimeType=*/nullptr);
}

// Returns a zero padding vector if |unalignedLength| needs alignment or null.
static Value createPadding(Location loc, uint64_t unalignedLength,
                           OpBuilder &builder) {
  uint64_t padding = llvm::alignTo(unalignedLength, 16) - unalignedLength;
  if (!padding) {
    return nullptr;
  }
  auto i8Type = builder.getI8Type();
  auto zeroAttr = IntegerAttr::get(i8Type, 0);
  auto dataAttr = DenseElementsAttr::get(
      VectorType::get({(int64_t)padding}, i8Type), zeroAttr);
  return IREE::Util::BufferConstantOp::create(
      builder, loc, /*name=*/nullptr, dataAttr, builder.getIndexAttr(16),
      /*mimeType=*/nullptr);
}

static void appendListItems(Location loc, Value list, ArrayRef<Value> items,
                            OpBuilder &builder) {
  Value oldLength = IREE::Util::ListSizeOp::create(builder, loc, list);
  Value newLength = arith::AddIOp::create(
      builder, loc, oldLength,
      arith::ConstantIndexOp::create(builder, loc, items.size()));
  IREE::Util::ListResizeOp::create(builder, loc, list, newLength);
  for (size_t i = 0; i < items.size(); ++i) {
    Value idx =
        arith::AddIOp::create(builder, loc, oldLength,
                              arith::ConstantIndexOp::create(builder, loc, i));
    IREE::Util::ListSetOp::create(builder, loc, list, idx, items[i]);
  }
}

//===----------------------------------------------------------------------===//
// --iree-hal-materialize-dispatch-instrumentation
//===----------------------------------------------------------------------===//

struct MaterializeDispatchInstrumentationPass
    : IREE::HAL::impl::MaterializeDispatchInstrumentationPassBase<
          MaterializeDispatchInstrumentationPass> {
  using IREE::HAL::impl::MaterializeDispatchInstrumentationPassBase<
      MaterializeDispatchInstrumentationPass>::
      MaterializeDispatchInstrumentationPassBase;
  void runOnOperation() override {
    mlir::ModuleOp moduleOp = getOperation();
    if (moduleOp.getBody()->empty()) {
      return;
    }

    auto moduleBuilder = OpBuilder(&moduleOp.getBody()->front());
    auto i8Type = moduleBuilder.getI8Type();
    auto i32Type = moduleBuilder.getI32Type();
    auto indexType = moduleBuilder.getIndexType();

    // Used for all instrumentation.
    auto loc = moduleBuilder.getUnknownLoc();

    // Currently statically sized to avoid disturbing too much by adding
    // additional arguments to dispatches that need to be marshaled.
    // We need to use the base power of two size for storage then add some
    // padding for overflows and the write head location.
    //
    // [power of two storage buffer]
    // [56 bytes of padding, may get overflow data]
    // [8 bytes of write head]
    int64_t totalBufferSize =
        bufferSize.value + IREE_INSTRUMENT_DISPATCH_PADDING;
    auto bufferSizeAttr = moduleBuilder.getIndexAttr(totalBufferSize);
    auto bufferType = MemRefType::get({totalBufferSize}, i8Type);

    // Create global device-side instrumentation resource.
    auto globalOp = IREE::Util::GlobalOp::create(
        moduleBuilder, loc, "__dispatch_instrumentation",
        /*isMutable=*/false,
        moduleBuilder.getType<IREE::Stream::ResourceType>(
            IREE::Stream::Lifetime::External));
    {
      // Ensure initializer comes after the global.
      OpBuilder::InsertionGuard moduleGuard(moduleBuilder);
      moduleBuilder.setInsertionPointAfter(globalOp);
      auto initializerOp =
          IREE::Util::InitializerOp::create(moduleBuilder, loc);
      auto initializerBuilder =
          OpBuilder::atBlockBegin(initializerOp.addEntryBlock());
      Value bufferSize =
          arith::ConstantOp::create(initializerBuilder, loc, bufferSizeAttr);
      Value buffer = IREE::Stream::ResourceAllocOp::create(
          initializerBuilder, loc, globalOp.getType(), bufferSize,
          /*uninitialized=*/true, /*affinity=*/nullptr);
      globalOp.createStoreOp(loc, buffer, initializerBuilder);
      IREE::Util::ReturnOp::create(initializerBuilder, loc);
    }

    FlatbufferBuilder metadataBuilder;

    // Update all executable export signatures to include the instrumentation
    // binding. We don't actually use it yet but ensure that it's available
    // during translation. We keep track of which exports we instrument as some
    // may be external declarations we can't modify.
    SmallVector<iree_instruments_DispatchFunctionDef_ref_t>
        dispatchFunctionRefs;
    DenseMap<SymbolRefAttr, uint32_t> instrumentedExports;
    auto bindingType = moduleBuilder.getType<IREE::Stream::BindingType>();
    auto alignmentKey = moduleBuilder.getStringAttr("stream.alignment");
    auto alignment64 = moduleBuilder.getIndexAttr(64);
    for (auto executableOp : moduleOp.getOps<IREE::Stream::ExecutableOp>()) {
      for (auto exportOp :
           executableOp.getOps<IREE::Stream::ExecutableExportOp>()) {
        auto funcOp = exportOp.lookupFunctionRef();
        if (!funcOp) {
          continue;
        }

        // Capture the source before we mess with it.
        auto originalSource = getOpStr(funcOp);

        // Mark as instrumented.
        instrumentedExports[SymbolRefAttr::get(
            executableOp.getNameAttr(), {SymbolRefAttr::get(exportOp)})] =
            instrumentedExports.size();

        // Update function signature to add the ringbuffer and dispatch ID.
        SmallVector<Type> argTypes(funcOp.getArgumentTypes());
        argTypes.push_back(bindingType);
        argTypes.push_back(i32Type);
        funcOp.setType(FunctionType::get(&getContext(), argTypes,
                                         funcOp.getResultTypes()));
        auto bindingArg = funcOp.front().addArgument(bindingType, loc);
        auto dispatchIdArg = funcOp.front().addArgument(i32Type, loc);
        funcOp.setArgAttrs(
            bindingArg.getArgNumber(),
            DictionaryAttr::get(&getContext(),
                                {NamedAttribute(alignmentKey, alignment64)}));

        // Insert the workgroup instrumentation. Note that this happens before
        // codegen would do tile-and-distribute, but that's ok as it should just
        // ignore these ops when doing that. This only works because we aren't
        // capturing workgroup ID yet with this instrumentation op and instead
        // leave that until late in the codegen pipeline.
        auto funcBuilder = OpBuilder::atBlockBegin(&funcOp.front());
        Value zero = arith::ConstantIndexOp::create(funcBuilder, loc, 0);
        auto subspanOp = IREE::Stream::BindingSubspanOp::create(
            funcBuilder, loc, bufferType, bindingArg, /*byteOffset=*/zero,
            ValueRange{});
        IREE::HAL::InstrumentWorkgroupOp::create(
            funcBuilder, loc, indexType, subspanOp.getResult(), dispatchIdArg);

        // Build function metadata.
        auto nameRef = metadataBuilder.createString(exportOp.getName());
        auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(exportOp);
        auto targetRef = metadataBuilder.createString(getAttrStr(targetAttr));
        auto sourceRef = metadataBuilder.createString(originalSource);
        iree_instruments_DispatchFunctionDef_start(metadataBuilder);
        iree_instruments_DispatchFunctionDef_name_add(metadataBuilder, nameRef);
        iree_instruments_DispatchFunctionDef_target_add(metadataBuilder,
                                                        targetRef);
        iree_instruments_DispatchFunctionDef_source_add(metadataBuilder,
                                                        sourceRef);
        dispatchFunctionRefs.push_back(
            iree_instruments_DispatchFunctionDef_end(metadataBuilder));
      }
    }

    // Find all dispatches to exports that we've instrumented and pass along the
    // instrumentation buffer.
    SmallVector<iree_instruments_DispatchSiteDef_ref_t> dispatchSiteRefs;
    uint32_t dispatchSiteCount = 0;
    for (auto funcOp : moduleOp.getOps<mlir::FunctionOpInterface>()) {
      funcOp.walk([&](IREE::Stream::CmdExecuteOp executeOp) {
        auto parentBuilder = OpBuilder(executeOp);

        // Load the ringbuffer and capture it for use within the execute region.
        auto loadedValue =
            globalOp.createLoadOp(loc, parentBuilder).getLoadedGlobalValue();
        Value zero = arith::ConstantIndexOp::create(parentBuilder, loc, 0);
        Value bufferSize =
            arith::ConstantOp::create(parentBuilder, loc, bufferSizeAttr);
        executeOp.getResourceOperandsMutable().append(loadedValue);
        executeOp.getResourceOperandSizesMutable().append(bufferSize);
        auto bufferArg =
            executeOp.getBody().addArgument(loadedValue.getType(), loc);

        // Walk dispatches and pass them the ringbuffer and their unique ID.
        executeOp.walk([&](IREE::Stream::CmdDispatchOp dispatchOp) {
          // NOTE: we just choose the first instrumented export for attribution
          // as that's good enough for all current use cases. If we start
          // specializing really early we may want to fix that.
          std::optional<uint32_t> functionId;
          for (auto entryPointAttr : dispatchOp.getEntryPointRefs()) {
            auto it = instrumentedExports.find(entryPointAttr);
            if (it != instrumentedExports.end()) {
              // Found the first instrumented export.
              functionId = it->second;
              break;
            }
          }
          if (!functionId) {
            return; // not instrumented
          }

          // Append dispatch site ID to correlate this op with where it lives in
          // the program and what is being dispatched. Note that multiple
          // dispatch ops may reference the same dispatch function after
          // deduplication.
          uint32_t dispatchSiteId = dispatchSiteCount++;
          dispatchOp.getUniformOperandsMutable().append(
              arith::ConstantIntOp::create(parentBuilder, loc, dispatchSiteId,
                                           32)
                  .getResult());

          // Record dispatch site to the host-side metadata.
          iree_instruments_DispatchSiteDef_start(metadataBuilder);
          // TODO(benvanik): source loc to identify the site.
          iree_instruments_DispatchSiteDef_function_add(metadataBuilder,
                                                        *functionId);
          dispatchSiteRefs.push_back(
              iree_instruments_DispatchSiteDef_end(metadataBuilder));

          // Append ringbuffer for storing the instrumentation data.
          dispatchOp.getResourcesMutable().append(bufferArg);
          dispatchOp.getResourceOffsetsMutable().append(zero);
          dispatchOp.getResourceLengthsMutable().append(bufferSize);
          dispatchOp.getResourceSizesMutable().append(bufferSize);
          SmallVector<Attribute> accesses(
              dispatchOp.getResourceAccesses().getValue());
          accesses.push_back(IREE::Stream::ResourceAccessBitfieldAttr::get(
              &getContext(), IREE::Stream::ResourceAccessBitfield::Read |
                                 IREE::Stream::ResourceAccessBitfield::Write));
          dispatchOp.setResourceAccessesAttr(
              parentBuilder.getArrayAttr(accesses));
        });
      });
    }

    auto dispatchFunctionsRef = iree_instruments_DispatchFunctionDef_vec_create(
        metadataBuilder, dispatchFunctionRefs.data(),
        dispatchFunctionRefs.size());
    auto dispatchSitesRef = iree_instruments_DispatchSiteDef_vec_create(
        metadataBuilder, dispatchSiteRefs.data(), dispatchSiteRefs.size());
    iree_instruments_DispatchInstrumentDef_start_as_root(metadataBuilder);
    iree_instruments_DispatchInstrumentDef_version_add(metadataBuilder, 0);
    iree_instruments_DispatchInstrumentDef_flags_add(metadataBuilder, 0);
    iree_instruments_DispatchInstrumentDef_functions_add(metadataBuilder,
                                                         dispatchFunctionsRef);
    iree_instruments_DispatchInstrumentDef_sites_add(metadataBuilder,
                                                     dispatchSitesRef);
    iree_instruments_DispatchInstrumentDef_end_as_root(metadataBuilder);
    auto metadataAttr = metadataBuilder.getBufferAttr(&getContext());

    // Create query function for getting the instrumentation data.
    auto listType = moduleBuilder.getType<IREE::Util::ListType>(
        moduleBuilder.getType<IREE::Util::VariantType>());
    auto queryOp = IREE::Util::FuncOp::create(
        moduleBuilder, loc, "__query_instruments",
        moduleBuilder.getFunctionType({listType}, {}));
    {
      queryOp.setPublic();
      auto *entryBlock = queryOp.addEntryBlock();
      auto queryBuilder = OpBuilder::atBlockBegin(entryBlock);
      auto listArg = entryBlock->getArgument(0);

      SmallVector<Value> iovecs;

      iovecs.push_back(
          createChunkHeader(loc, IREE_IDBTS_CHUNK_TYPE_DISPATCH_METADATA,
                            metadataAttr.size(), queryBuilder));

      // Grab the read-only dispatch metadata.
      iovecs.push_back(IREE::Util::BufferConstantOp::create(
          queryBuilder, loc,
          queryBuilder.getStringAttr("dispatch_instrument.fb"), metadataAttr,
          queryBuilder.getIndexAttr(16),
          queryBuilder.getStringAttr("application/x-flatbuffers")));

      if (Value metadataPadding =
              createPadding(loc, metadataAttr.size(), queryBuilder)) {
        iovecs.push_back(metadataPadding);
      }

      iovecs.push_back(
          createChunkHeader(loc, IREE_IDBTS_CHUNK_TYPE_DISPATCH_RINGBUFFER,
                            totalBufferSize, queryBuilder));

      // Export the device buffer containing the instrument data.
      Value buffer =
          globalOp.createLoadOp(loc, queryBuilder).getLoadedGlobalValue();
      Value bufferSize =
          arith::ConstantOp::create(queryBuilder, loc, bufferSizeAttr);
      auto bufferViewType = moduleBuilder.getType<IREE::HAL::BufferViewType>();
      auto exportOp = IREE::Stream::TensorExportOp::create(
          queryBuilder, loc, bufferViewType, buffer,
          RankedTensorType::get({totalBufferSize}, queryBuilder.getI8Type()),
          ValueRange{}, bufferSize,
          /*affinity=*/nullptr);
      iovecs.push_back(exportOp.getResult());

      if (Value ringbufferPadding =
              createPadding(loc, totalBufferSize, queryBuilder)) {
        iovecs.push_back(ringbufferPadding);
      }

      appendListItems(loc, listArg, iovecs, queryBuilder);
      IREE::Util::ReturnOp::create(queryBuilder, loc);
    }
  }
};

} // namespace

} // namespace mlir::iree_compiler::IREE::HAL
