/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "PowerCounters.h"
#include "nsXULAppAPI.h"  // for XRE_IsParentProcess
#include "nsString.h"

#include <windows.h>
#include <devioctl.h>
#include <setupapi.h>  // for SetupDi*
// LogSeverity, defined by setupapi.h to DWORD, messes with other code.
#undef LogSeverity

#undef NTDDI_VERSION
#define NTDDI_VERSION NTDDI_WINBLUE
#include <emi.h>

#ifndef NTDDI_WIN10_RS5
// EMI v2 API exists in SDK 10.0.17763 (Windows 10 1809 / Redstone 5) and later.
// Our build machines are still on SDK 10.0.17134.
// Remove this block when updating the SDK (bug 1774628).
typedef EMI_METADATA EMI_METADATA_V1;
typedef EMI_MEASUREMENT_DATA EMI_CHANNEL_MEASUREMENT_DATA;
#  define EMI_VERSION_V2 2

typedef struct {
  EMI_MEASUREMENT_UNIT MeasurementUnit;
  USHORT ChannelNameSize;
  WCHAR ChannelName[ANYSIZE_ARRAY];
} EMI_CHANNEL_V2;

typedef struct {
  WCHAR HardwareOEM[EMI_NAME_MAX];
  WCHAR HardwareModel[EMI_NAME_MAX];
  USHORT HardwareRevision;
  USHORT ChannelCount;
  EMI_CHANNEL_V2 Channels[ANYSIZE_ARRAY];
} EMI_METADATA_V2;

#  define EMI_CHANNEL_V2_LENGTH(_ChannelNameSize) \
    (FIELD_OFFSET(EMI_CHANNEL_V2, ChannelName) + (_ChannelNameSize))

#  define EMI_CHANNEL_V2_NEXT_CHANNEL(_Channel) \
    ((EMI_CHANNEL_V2*)((PUCHAR)(_Channel) +     \
                       EMI_CHANNEL_V2_LENGTH((_Channel)->ChannelNameSize)))
#endif

using namespace mozilla;

// This is a counter to collect power utilization during profiling.
// It cannot be a raw `ProfilerCounter` because we need to manually add/remove
// it while the profiler lock is already held.
class PowerMeterChannel final : public BaseProfilerCount {
 public:
  explicit PowerMeterChannel(const WCHAR* aChannelName, ULONGLONG aInitialValue,
                             ULONGLONG aInitialTime)
      : BaseProfilerCount(nullptr, nullptr, nullptr, "power",
                          "Power utilization"),
        mChannelName(NS_ConvertUTF16toUTF8(aChannelName)),
        mPreviousValue(aInitialValue),
        mPreviousTime(aInitialTime),
        mIsSampleNew(true) {
    if (mChannelName.Equals("RAPL_Package0_PKG")) {
      mLabel = "Power: CPU package";
      mDescription = mChannelName.get();
    } else if (mChannelName.Equals("RAPL_Package0_PP0")) {
      mLabel = "Power: CPU cores";
      mDescription = mChannelName.get();
    } else if (mChannelName.Equals("RAPL_Package0_PP1")) {
      mLabel = "Power: iGPU";
      mDescription = mChannelName.get();
    } else if (mChannelName.Equals("RAPL_Package0_DRAM")) {
      mLabel = "Power: DRAM";
      mDescription = mChannelName.get();
    } else {
      unsigned int coreId;
      if (sscanf(mChannelName.get(), "RAPL_Package0_Core%u_CORE", &coreId) ==
          1) {
        mLabelString = "Power: CPU core ";
        mLabelString.AppendInt(coreId);
        mLabel = mLabelString.get();
        mDescription = mChannelName.get();
      } else {
        mLabel = mChannelName.get();
      }
    }
  }

  CountSample Sample() override {
    CountSample result;
    result.count = mCounter;
    result.number = 0;
    result.isSampleNew = mIsSampleNew;
    mIsSampleNew = false;
    return result;
  }

  void AddSample(ULONGLONG aAbsoluteEnergy, ULONGLONG aAbsoluteTime) {
    // aAbsoluteTime is the time since the system start in 100ns increments.
    if (aAbsoluteTime == mPreviousTime) {
      return;
    }

    if (aAbsoluteEnergy > mPreviousValue) {
      int64_t increment = aAbsoluteEnergy - mPreviousValue;
      mCounter += increment;
      mPreviousValue += increment;
      mPreviousTime = aAbsoluteTime;
    }

    mIsSampleNew = true;
  }

 private:
  int64_t mCounter;
  nsCString mChannelName;

  // Used as a storage when the label can not be a literal string.
  nsCString mLabelString;

  ULONGLONG mPreviousValue;
  ULONGLONG mPreviousTime;
  bool mIsSampleNew;
};

class PowerMeterDevice {
 public:
  explicit PowerMeterDevice(LPCTSTR aDevicePath) {
    mHandle = ::CreateFile(aDevicePath, GENERIC_READ,
                           FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
                           OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (mHandle == INVALID_HANDLE_VALUE) {
      return;
    }

    EMI_VERSION version = {0};
    DWORD dwOut;

    if (!::DeviceIoControl(mHandle, IOCTL_EMI_GET_VERSION, nullptr, 0, &version,
                           sizeof(version), &dwOut, nullptr) ||
        (version.EmiVersion != EMI_VERSION_V1 &&
         version.EmiVersion != EMI_VERSION_V2)) {
      return;
    }

    EMI_METADATA_SIZE size = {0};
    if (!::DeviceIoControl(mHandle, IOCTL_EMI_GET_METADATA_SIZE, nullptr, 0,
                           &size, sizeof(size), &dwOut, nullptr) ||
        !size.MetadataSize) {
      return;
    }

    UniquePtr<uint8_t[]> metadata(new (std::nothrow)
                                      uint8_t[size.MetadataSize]);
    if (!metadata) {
      return;
    }

    if (version.EmiVersion == EMI_VERSION_V2) {
      EMI_METADATA_V2* metadata2 =
          reinterpret_cast<EMI_METADATA_V2*>(metadata.get());
      if (!::DeviceIoControl(mHandle, IOCTL_EMI_GET_METADATA, nullptr, 0,
                             metadata2, size.MetadataSize, &dwOut, nullptr)) {
        return;
      }

      if (!mChannels.reserve(metadata2->ChannelCount)) {
        return;
      }

      mDataBuffer =
          MakeUnique<EMI_CHANNEL_MEASUREMENT_DATA[]>(metadata2->ChannelCount);
      if (!mDataBuffer) {
        return;
      }

      if (!::DeviceIoControl(
              mHandle, IOCTL_EMI_GET_MEASUREMENT, nullptr, 0, mDataBuffer.get(),
              sizeof(EMI_CHANNEL_MEASUREMENT_DATA[metadata2->ChannelCount]),
              &dwOut, nullptr)) {
        return;
      }

      EMI_CHANNEL_V2* channel = &metadata2->Channels[0];
      for (int i = 0; i < metadata2->ChannelCount; ++i) {
        EMI_CHANNEL_MEASUREMENT_DATA* channel_data = &mDataBuffer[i];
        mChannels.infallibleAppend(new PowerMeterChannel(
            channel->ChannelName, channel_data->AbsoluteEnergy,
            channel_data->AbsoluteTime));
        channel = EMI_CHANNEL_V2_NEXT_CHANNEL(channel);
      }
    } else if (version.EmiVersion == EMI_VERSION_V1) {
      EMI_METADATA_V1* metadata1 =
          reinterpret_cast<EMI_METADATA_V1*>(metadata.get());
      if (!::DeviceIoControl(mHandle, IOCTL_EMI_GET_METADATA, nullptr, 0,
                             metadata1, size.MetadataSize, &dwOut, nullptr)) {
        return;
      }

      mDataBuffer = MakeUnique<EMI_CHANNEL_MEASUREMENT_DATA[]>(1);
      if (!mDataBuffer) {
        return;
      }

      if (!::DeviceIoControl(
              mHandle, IOCTL_EMI_GET_MEASUREMENT, nullptr, 0, mDataBuffer.get(),
              sizeof(EMI_CHANNEL_MEASUREMENT_DATA), &dwOut, nullptr)) {
        return;
      }

      (void)mChannels.append(new PowerMeterChannel(
          metadata1->MeteredHardwareName, mDataBuffer[0].AbsoluteEnergy,
          mDataBuffer[0].AbsoluteTime));
    }
  }

  ~PowerMeterDevice() {
    if (mHandle != INVALID_HANDLE_VALUE) {
      ::CloseHandle(mHandle);
    }
  }

  void Sample() {
    MOZ_ASSERT(HasChannels());
    MOZ_ASSERT(mDataBuffer);

    DWORD dwOut;
    if (!::DeviceIoControl(
            mHandle, IOCTL_EMI_GET_MEASUREMENT, nullptr, 0, mDataBuffer.get(),
            sizeof(EMI_CHANNEL_MEASUREMENT_DATA[mChannels.length()]), &dwOut,
            nullptr)) {
      return;
    }

    for (size_t i = 0; i < mChannels.length(); ++i) {
      EMI_CHANNEL_MEASUREMENT_DATA* channel_data = &mDataBuffer[i];
      mChannels[i]->AddSample(channel_data->AbsoluteEnergy,
                              channel_data->AbsoluteTime);
    }
  }

  bool HasChannels() { return mChannels.length() != 0; }
  void AppendCountersTo(PowerCounters::CountVector& aCounters) {
    if (aCounters.reserve(aCounters.length() + mChannels.length())) {
      for (auto& channel : mChannels) {
        aCounters.infallibleAppend(channel.get());
      }
    }
  }

 private:
  Vector<UniquePtr<PowerMeterChannel>, 4> mChannels;
  HANDLE mHandle = INVALID_HANDLE_VALUE;
  UniquePtr<EMI_CHANNEL_MEASUREMENT_DATA[]> mDataBuffer;
};

PowerCounters::PowerCounters() {
  class MOZ_STACK_CLASS HDevInfoHolder final {
   public:
    explicit HDevInfoHolder(HDEVINFO aHandle) : mHandle(aHandle) {}

    ~HDevInfoHolder() { ::SetupDiDestroyDeviceInfoList(mHandle); }

   private:
    HDEVINFO mHandle;
  };

  if (!XRE_IsParentProcess()) {
    // Energy meters are global, so only sample them on the parent.
    return;
  }

  // Energy Metering Device Interface
  // {45BD8344-7ED6-49cf-A440-C276C933B053}
  //
  // Using GUID_DEVICE_ENERGY_METER does not compile as the symbol does not
  // exist before Windows 10.
  GUID my_GUID_DEVICE_ENERGY_METER = {
      0x45bd8344,
      0x7ed6,
      0x49cf,
      {0xa4, 0x40, 0xc2, 0x76, 0xc9, 0x33, 0xb0, 0x53}};

  HDEVINFO hdev =
      ::SetupDiGetClassDevs(&my_GUID_DEVICE_ENERGY_METER, nullptr, nullptr,
                            DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
  if (hdev == INVALID_HANDLE_VALUE) {
    return;
  }

  HDevInfoHolder hdevHolder(hdev);

  DWORD i = 0;
  SP_DEVICE_INTERFACE_DATA did = {0};
  did.cbSize = sizeof(did);

  while (::SetupDiEnumDeviceInterfaces(
      hdev, nullptr, &my_GUID_DEVICE_ENERGY_METER, i++, &did)) {
    DWORD bufferSize = 0;
    ::SetupDiGetDeviceInterfaceDetail(hdev, &did, nullptr, 0, &bufferSize,
                                      nullptr);
    if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
      continue;
    }

    UniquePtr<uint8_t[]> buffer(new (std::nothrow) uint8_t[bufferSize]);
    if (!buffer) {
      continue;
    }

    PSP_DEVICE_INTERFACE_DETAIL_DATA pdidd =
        reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA>(buffer.get());
    MOZ_ASSERT(uintptr_t(buffer.get()) %
                   alignof(PSP_DEVICE_INTERFACE_DETAIL_DATA) ==
               0);
    pdidd->cbSize = sizeof(*pdidd);
    if (!::SetupDiGetDeviceInterfaceDetail(hdev, &did, pdidd, bufferSize,
                                           &bufferSize, nullptr)) {
      continue;
    }

    UniquePtr<PowerMeterDevice> pmd =
        MakeUnique<PowerMeterDevice>(pdidd->DevicePath);
    if (!pmd->HasChannels() ||
        !mPowerMeterDevices.emplaceBack(std::move(pmd))) {
      NS_WARNING("PowerMeterDevice without measurement channel (or OOM)");
    }
  }

  for (auto& device : mPowerMeterDevices) {
    device->AppendCountersTo(mCounters);
  }
}

PowerCounters::~PowerCounters() { mCounters.clear(); }

void PowerCounters::Sample() {
  for (auto& device : mPowerMeterDevices) {
    device->Sample();
  }
}
