// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "flutter/shell/platform/darwin/ios/framework/Source/profiler_metrics_ios.h"

#import <Foundation/Foundation.h>

#import "flutter/shell/platform/darwin/ios/framework/Source/IOKit.h"

namespace {

// RAII holder for `thread_array_t` this is so any early returns in
// `ProfilerMetricsIOS::CpuUsage` don't leak them.
class MachThreads {
 public:
  thread_array_t threads = NULL;
  mach_msg_type_number_t thread_count = 0;

  MachThreads() = default;

  ~MachThreads() {
    kern_return_t kernel_return_code = vm_deallocate(
        mach_task_self(), reinterpret_cast<vm_offset_t>(threads), thread_count * sizeof(thread_t));
    FML_CHECK(kernel_return_code == KERN_SUCCESS) << "Failed to deallocate thread infos.";
  }

 private:
  FML_DISALLOW_COPY_AND_ASSIGN(MachThreads);
};

}  // namespace

namespace flutter {
namespace {

#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG || \
    FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_PROFILE

template <typename T>
T ClearValue() {
  return nullptr;
}

template <>
io_object_t ClearValue<io_object_t>() {
  return 0;
}

template <typename T>
/// Generic RAII wrapper like unique_ptr but gives access to its handle.
class Scoped {
 public:
  typedef void (*Deleter)(T);
  explicit Scoped(Deleter deleter) : object_(ClearValue<T>()), deleter_(deleter) {}
  Scoped(T object, Deleter deleter) : object_(object), deleter_(deleter) {}
  ~Scoped() {
    if (object_) {
      deleter_(object_);
    }
  }
  T* handle() {
    if (object_) {
      deleter_(object_);
      object_ = ClearValue<T>();
    }
    return &object_;
  }
  T get() { return object_; }
  void reset(T new_value) {
    if (object_) {
      deleter_(object_);
    }
    object_ = new_value;
  }

 private:
  FML_DISALLOW_COPY_ASSIGN_AND_MOVE(Scoped);
  T object_;
  Deleter deleter_;
};

void DeleteCF(CFMutableDictionaryRef value) {
  CFRelease(value);
}

void DeleteIO(io_object_t value) {
  IOObjectRelease(value);
}

std::optional<GpuUsageInfo> FindGpuUsageInfo(io_iterator_t iterator) {
  for (Scoped<io_registry_entry_t> regEntry(IOIteratorNext(iterator), DeleteIO); regEntry.get();
       regEntry.reset(IOIteratorNext(iterator))) {
    Scoped<CFMutableDictionaryRef> serviceDictionary(DeleteCF);
    if (IORegistryEntryCreateCFProperties(regEntry.get(), serviceDictionary.handle(),
                                          kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) {
      continue;
    }

    NSDictionary* dictionary =
        ((__bridge NSDictionary*)serviceDictionary.get())[@"PerformanceStatistics"];
    NSNumber* utilization = dictionary[@"Device Utilization %"];
    if (utilization) {
      return (GpuUsageInfo){.percent_usage = [utilization doubleValue]};
    }
  }
  return std::nullopt;
}

[[maybe_unused]] std::optional<GpuUsageInfo> FindSimulatorGpuUsageInfo() {
  Scoped<io_iterator_t> iterator(DeleteIO);
  if (IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceNameMatching("IntelAccelerator"),
                                   iterator.handle()) == kIOReturnSuccess) {
    return FindGpuUsageInfo(iterator.get());
  }
  return std::nullopt;
}

[[maybe_unused]] std::optional<GpuUsageInfo> FindDeviceGpuUsageInfo() {
  Scoped<io_iterator_t> iterator(DeleteIO);
  if (IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceNameMatching("sgx"),
                                   iterator.handle()) == kIOReturnSuccess) {
    for (Scoped<io_registry_entry_t> regEntry(IOIteratorNext(iterator.get()), DeleteIO);
         regEntry.get(); regEntry.reset(IOIteratorNext(iterator.get()))) {
      Scoped<io_iterator_t> innerIterator(DeleteIO);
      if (IORegistryEntryGetChildIterator(regEntry.get(), kIOServicePlane,
                                          innerIterator.handle()) == kIOReturnSuccess) {
        std::optional<GpuUsageInfo> result = FindGpuUsageInfo(innerIterator.get());
        if (result.has_value()) {
          return result;
        }
      }
    }
  }
  return std::nullopt;
}

#endif  // FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG ||
        // FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_PROFILE

std::optional<GpuUsageInfo> PollGpuUsage() {
#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_RELEASE || \
     FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_JIT_RELEASE)
  return std::nullopt;
#elif TARGET_IPHONE_SIMULATOR
  return FindSimulatorGpuUsageInfo();
#elif TARGET_OS_IOS
  return FindDeviceGpuUsageInfo();
#endif  // TARGET_IPHONE_SIMULATOR
}
}  // namespace

ProfileSample ProfilerMetricsIOS::GenerateSample() {
  return {.cpu_usage = CpuUsage(), .memory_usage = MemoryUsage(), .gpu_usage = PollGpuUsage()};
}

std::optional<CpuUsageInfo> ProfilerMetricsIOS::CpuUsage() {
  kern_return_t kernel_return_code;
  MachThreads mach_threads = MachThreads();

  // Get threads in the task
  kernel_return_code =
      task_threads(mach_task_self(), &mach_threads.threads, &mach_threads.thread_count);
  if (kernel_return_code != KERN_SUCCESS) {
    FML_LOG(ERROR) << "Error retrieving task information: "
                   << mach_error_string(kernel_return_code);
    return std::nullopt;
  }

  double total_cpu_usage = 0.0;
  uint32_t num_threads = mach_threads.thread_count;

  // Add the CPU usage for each thread. It should be noted that there may be some CPU usage missing
  // from this calculation. If a thread ends between calls to this routine, then its info will be
  // lost. We could solve this by installing a callback using pthread_key_create. The callback would
  // report the thread is ending and allow the code to get the CPU usage. But we need to call
  // pthread_setspecific in each thread to set the key's value to a non-null value for the callback
  // to work. If we really need this information and if we have a good mechanism for calling
  // pthread_setspecific in every thread, then we can include that value in the CPU usage.
  for (mach_msg_type_number_t i = 0; i < mach_threads.thread_count; i++) {
    thread_basic_info_data_t basic_thread_info;
    mach_msg_type_number_t thread_info_count = THREAD_BASIC_INFO_COUNT;
    kernel_return_code =
        thread_info(mach_threads.threads[i], THREAD_BASIC_INFO,
                    reinterpret_cast<thread_info_t>(&basic_thread_info), &thread_info_count);
    switch (kernel_return_code) {
      case KERN_SUCCESS: {
        const double current_thread_cpu_usage =
            basic_thread_info.cpu_usage / static_cast<float>(TH_USAGE_SCALE);
        total_cpu_usage += current_thread_cpu_usage;
        break;
      }
      case MACH_SEND_TIMEOUT:
      case MACH_SEND_TIMED_OUT:
      case MACH_SEND_INVALID_DEST:
        // Ignore as this thread been destroyed. The possible return codes are not really well
        // documented. This handling is inspired from the following sources:
        // - https://opensource.apple.com/source/xnu/xnu-4903.221.2/tests/task_inspect.c.auto.html
        // - https://github.com/apple/swift-corelibs-libdispatch/blob/main/src/queue.c#L6617
        num_threads--;
        break;
      default:
        FML_LOG(ERROR) << "Error retrieving thread information: "
                       << mach_error_string(kernel_return_code);
        return std::nullopt;
    }
  }

  flutter::CpuUsageInfo cpu_usage_info = {.num_threads = num_threads,
                                          .total_cpu_usage = total_cpu_usage * 100.0};
  return cpu_usage_info;
}

std::optional<MemoryUsageInfo> ProfilerMetricsIOS::MemoryUsage() {
  kern_return_t kernel_return_code;
  task_vm_info_data_t task_memory_info;
  mach_msg_type_number_t task_memory_info_count = TASK_VM_INFO_COUNT;

  kernel_return_code =
      task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast<task_info_t>(&task_memory_info),
                &task_memory_info_count);
  if (kernel_return_code != KERN_SUCCESS) {
    FML_LOG(ERROR) << " Error retrieving task memory information: "
                   << mach_error_string(kernel_return_code);
    return std::nullopt;
  }

  // `phys_footprint` is Apple's recommended way to measure app's memory usage. It provides the
  // best approximate to xcode memory gauge. According to its source code explanation, the physical
  // footprint mainly consists of app's internal memory data and IOKit mappings. `resident_size`
  // is the total physical memory used by the app, so we simply do `resident_size - phys_footprint`
  // to obtain the shared memory usage.
  const double dirty_memory_usage =
      static_cast<double>(task_memory_info.phys_footprint) / 1024.0 / 1024.0;
  const double owned_shared_memory_usage =
      static_cast<double>(task_memory_info.resident_size) / 1024.0 / 1024.0 - dirty_memory_usage;
  flutter::MemoryUsageInfo memory_usage_info = {
      .dirty_memory_usage = dirty_memory_usage,
      .owned_shared_memory_usage = owned_shared_memory_usage};
  return memory_usage_info;
}

}  // namespace flutter
