chromium/tools/win/IdleWakeups/idle_wakeups.cpp

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

#include "stdafx.h"

#include <algorithm>
#include <map>
#include <vector>

#include "power_sampler.h"
#include "system_information_sampler.h"

// Unit for raw CPU usage data from Windows.
constexpr int kTicksPerSecond = 10000000;

// Result data structure contains a final set of values calculated based on
// comparison of two snapshots. These are the values that the tool prints
// in the output.
struct Result {
  ULONG idle_wakeups_per_sec;
  DWORD handle_count;
  double cpu_usage_percent;
  double cpu_usage_seconds;
  ULONGLONG memory;  // Private commit
  double power;
};

typedef std::vector<Result> ResultVector;

// The following functions are used for sorting of ResultVector.
ULONG GetIdleWakeupsPerSec(const Result& r) {
  return r.idle_wakeups_per_sec;
}
DWORD GetHandleCount(const Result& r) {
  return r.handle_count;
}
double GetCpuUsagePercent(const Result& r) {
  return r.cpu_usage_percent;
}
double GetCpuUsageSeconds(const Result& r) {
  return r.cpu_usage_seconds;
}
ULONGLONG GetMemory(const Result& r) {
  return r.memory;
}
double GetPower(const Result& r) {
  return r.power;
}

template <typename T>
T GetMedian(ResultVector* results, T (*getter)(const Result&)) {
  std::sort(results->begin(), results->end(),
            [&](const Result& lhs, const Result& rhs) {
              return getter(lhs) < getter(rhs);
            });

  size_t median_index = results->size() / 2;
  if (results->size() % 2 != 0) {
    return getter((*results)[median_index]);
  } else {
    return (getter((*results)[median_index - 1]) +
            getter((*results)[median_index])) /
           2;
  }
}

template <typename T>
T GetAverage(const ResultVector& results, T (*getter)(const Result&)) {
  // |sum| is a 64-bit type (uint64_t if |T| is an integral type, double if not)
  // to minimize the risk of overflow.
  typedef typename std::conditional<std::is_integral<T>::value, uint64_t,
                                    double>::type SumType;
  SumType sum = SumType();
  const size_t size = results.size();
  for (size_t i = 0; i < size; i++) {
    sum += getter(results[i]);
  }
  return static_cast<T>(sum / size);
}

// Count newly created processes: those in |processes| but not
// |previous_processes|.
size_t GetNumProcessesCreated(const ProcessDataMap& previous_processes,
                              const ProcessDataMap& processes) {
  size_t num_processes_created = 0;
  for (auto& process : processes) {
    if (previous_processes.find(process.first) == previous_processes.end())
      num_processes_created++;
  }
  return num_processes_created;
}

// This class holds the app state and contains a number of utilities for
// collecting and diffing snapshots of data, handling processes, etc.
class IdleWakeups {
 public:
  IdleWakeups();
  ~IdleWakeups();

  Result DiffSnapshots(const ProcessDataSnapshot& prev_snapshot,
                       const ProcessDataSnapshot& snapshot);

  void OpenProcesses(const ProcessDataSnapshot& snapshot);
  void CloseProcesses();

 private:
  HANDLE GetProcessHandle(ProcessId process_id);
  void OpenProcess(ProcessId process_id);
  void CloseProcess(ProcessId process_id);
  bool GetFinishedProcessCpuTime(ProcessId process_id, ULONGLONG* cpu_usage);

  static ULONG CountContextSwitches(const ProcessData& process_data);
  static ULONG DiffContextSwitches(const ProcessData& prev_process_data,
                                   const ProcessData& process_data);

  std::map<ProcessId, HANDLE> process_id_to_handle_map;

  IdleWakeups& operator=(const IdleWakeups&) = delete;
  IdleWakeups(const IdleWakeups&) = delete;
};

IdleWakeups::IdleWakeups() {}

IdleWakeups::~IdleWakeups() {
  CloseProcesses();
}

void IdleWakeups::OpenProcesses(const ProcessDataSnapshot& snapshot) {
  for (auto& pair : snapshot.processes) {
    OpenProcess(pair.first);
  }
}

void IdleWakeups::CloseProcesses() {
  for (auto& pair : process_id_to_handle_map) {
    CloseHandle(pair.second);
  }
  process_id_to_handle_map.clear();
}

HANDLE IdleWakeups::GetProcessHandle(ProcessId process_id) {
  return process_id_to_handle_map[process_id];
}

void IdleWakeups::OpenProcess(ProcessId process_id) {
  process_id_to_handle_map[process_id] = ::OpenProcess(
      PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)(ULONGLONG)process_id);
}

void IdleWakeups::CloseProcess(ProcessId process_id) {
  HANDLE handle = GetProcessHandle(process_id);
  CloseHandle(handle);
  process_id_to_handle_map.erase(process_id);
}

ULONG IdleWakeups::CountContextSwitches(const ProcessData& process_data) {
  ULONG context_switches = 0;

  for (const auto& thread_data : process_data.threads) {
    context_switches += thread_data.context_switches;
  }

  return context_switches;
}

ULONG IdleWakeups::DiffContextSwitches(const ProcessData& prev_process_data,
                                       const ProcessData& process_data) {
  ULONG context_switches = 0;
  size_t prev_index = 0;

  for (const auto& thread_data : process_data.threads) {
    ULONG prev_context_switches = 0;

    for (; prev_index < prev_process_data.threads.size(); ++prev_index) {
      const auto& prev_thread_data = prev_process_data.threads[prev_index];
      if (prev_thread_data.thread_id == thread_data.thread_id) {
        prev_context_switches = prev_thread_data.context_switches;
        ++prev_index;
        break;
      }

      if (prev_thread_data.thread_id > thread_data.thread_id)
        break;
    }

    context_switches += thread_data.context_switches - prev_context_switches;
  }

  return context_switches;
}

bool IdleWakeups::GetFinishedProcessCpuTime(ProcessId process_id,
                                            ULONGLONG* cpu_time) {
  HANDLE process_handle = GetProcessHandle(process_id);

  FILETIME creation_time, exit_time, kernel_time, user_time;
  if (GetProcessTimes(process_handle, &creation_time, &exit_time, &kernel_time,
                      &user_time)) {
    ULARGE_INTEGER ul_kernel_time, ul_user_time;
    ul_kernel_time.LowPart = kernel_time.dwLowDateTime;
    ul_kernel_time.HighPart = kernel_time.dwHighDateTime;
    ul_user_time.LowPart = user_time.dwLowDateTime;
    ul_user_time.HighPart = user_time.dwHighDateTime;
    *cpu_time = ul_kernel_time.QuadPart + ul_user_time.QuadPart;
    return true;
  }

  *cpu_time = 0;
  return false;
}

Result IdleWakeups::DiffSnapshots(const ProcessDataSnapshot& prev_snapshot,
                                  const ProcessDataSnapshot& snapshot) {
  ULONG idle_wakeups_delta = 0;
  ULONGLONG cpu_usage_delta = 0;
  ULONGLONG total_memory = 0;
  DWORD total_handle_count = 0;

  ProcessDataMap::const_iterator prev_it = prev_snapshot.processes.begin();

  for (const auto& it : snapshot.processes) {
    ProcessId process_id = it.first;
    const ProcessData& process_data = it.second;
    const ProcessData* prev_process_data_to_diff = nullptr;
    ULONGLONG prev_process_cpu_time = 0;

    for (; prev_it != prev_snapshot.processes.end(); ++prev_it) {
      ProcessId prev_process_id = prev_it->first;
      const ProcessData& prev_process_data = prev_it->second;

      if (prev_process_id == process_id) {
        prev_process_data_to_diff = &prev_process_data;
        prev_process_cpu_time = prev_process_data.cpu_time;
        ++prev_it;
        break;
      }

      if (prev_process_id > process_id)
        break;

      // Prev process disappeared.
      ULONGLONG last_known_cpu_time;
      if (GetFinishedProcessCpuTime(prev_process_id, &last_known_cpu_time)) {
        cpu_usage_delta += last_known_cpu_time - prev_process_data.cpu_time;
      }
      CloseProcess(prev_process_id);
    }

    if (prev_process_data_to_diff) {
      idle_wakeups_delta +=
          DiffContextSwitches(*prev_process_data_to_diff, process_data);
    } else {
      // New process that we haven't seen before.
      OpenProcess(process_id);
      idle_wakeups_delta += CountContextSwitches(process_data);
    }

    cpu_usage_delta += process_data.cpu_time - prev_process_cpu_time;
    total_memory += process_data.memory / 1024;
    total_handle_count += process_data.handle_count;
  }

  double time_delta = snapshot.timestamp - prev_snapshot.timestamp;
  Result result;
  result.idle_wakeups_per_sec =
      static_cast<ULONG>(idle_wakeups_delta / time_delta);
  result.cpu_usage_percent =
      (double)cpu_usage_delta * 100 / (time_delta * kTicksPerSecond);
  result.cpu_usage_seconds = (double)cpu_usage_delta / kTicksPerSecond;
  result.memory = total_memory;
  result.handle_count = total_handle_count;

  return result;
}

HANDLE ctrl_c_pressed = NULL;

BOOL WINAPI HandlerFunction(DWORD ctrl_type) {
  if (ctrl_type == CTRL_C_EVENT) {
    printf("Ctrl+C pressed...\n");
    SetEvent(ctrl_c_pressed);
    return TRUE;
  }

  return FALSE;
}

const DWORD sleep_time_sec = 2;

void PrintHeader() {
  printf(
      "------------------------------------------------------------------------"
      "--------------------\n");
  printf(
      "                       Context switches/sec    CPU usage   Private "
      "Commit    Power   Handles\n");
  printf(
      "------------------------------------------------------------------------"
      "--------------------\n");
}

#define RESULT_FORMAT_STRING          \
  "    %20lu    %8.2f%c    %7.2f MiB" \
  "    %5.2f W   %7u\n"

int wmain(int argc, wchar_t* argv[]) {
  ctrl_c_pressed = CreateEvent(NULL, FALSE, FALSE, NULL);
  SetConsoleCtrlHandler(HandlerFunction, TRUE);

  PowerSampler power_sampler;
  IdleWakeups the_app;

  // Parse command line for target process name and optional --cpu-seconds and
  // --stop-on-exit flags.
  wchar_t* target_process_name = nullptr;
  bool cpu_usage_in_seconds = false;
  bool stop_on_exit = false;
  bool tabbed_summary_only = false;
  for (int i = 1; i < argc; i++) {
    if (wcscmp(argv[i], L"--cpu-seconds") == 0)
      cpu_usage_in_seconds = true;
    else if (wcscmp(argv[i], L"--stop-on-exit") == 0)
      stop_on_exit = true;
    else if (wcscmp(argv[i], L"--tabbed") == 0)
      tabbed_summary_only = true;
    else if (!target_process_name)
      target_process_name = argv[i];

    // Stop parsing if all possible args have been found.
    if (cpu_usage_in_seconds && stop_on_exit && tabbed_summary_only &&
        target_process_name) {
      break;
    }
  }
  const char cpu_usage_unit = cpu_usage_in_seconds ? 's' : '%';
  SystemInformationSampler system_information_sampler(
      target_process_name ? target_process_name : L"chrome.exe");

  // Take the initial snapshot.
  std::unique_ptr<ProcessDataSnapshot> previous_snapshot =
      system_information_sampler.TakeSnapshot();

  the_app.OpenProcesses(*previous_snapshot);
  const size_t initial_number_of_processes =
      previous_snapshot->processes.size();
  size_t final_number_of_processes = initial_number_of_processes;

  double cumulative_cpu_usage_seconds = 0.0;
  size_t cumulative_processes_created = 0;
  int num_idle_snapshots = 0;

  ResultVector results;

  if (!tabbed_summary_only) {
    printf("Capturing perf data for all processes matching %ls\n",
           system_information_sampler.target_process_name_filter());

    PrintHeader();
  }

  bool target_process_seen = false;
  for (;;) {
    if (WaitForSingleObject(ctrl_c_pressed, sleep_time_sec * 1000) ==
        WAIT_OBJECT_0)
      break;

    std::unique_ptr<ProcessDataSnapshot> snapshot =
        system_information_sampler.TakeSnapshot();
    size_t number_of_processes = snapshot->processes.size();
    final_number_of_processes = number_of_processes;

    cumulative_processes_created += GetNumProcessesCreated(
        previous_snapshot->processes, snapshot->processes);

    Result result = the_app.DiffSnapshots(*previous_snapshot, *snapshot);
    previous_snapshot = std::move(snapshot);

    power_sampler.SampleCPUPowerState();
    result.power = power_sampler.get_power(L"Processor");

    if (!tabbed_summary_only) {
      printf("%9zu processes" RESULT_FORMAT_STRING, number_of_processes,
             result.idle_wakeups_per_sec,
             cpu_usage_in_seconds ? result.cpu_usage_seconds
                                  : result.cpu_usage_percent,
             cpu_usage_unit, result.memory / 1024.0, result.power,
             result.handle_count);
    }

    if (number_of_processes > 0) {
      cumulative_cpu_usage_seconds += result.cpu_usage_seconds;
      results.push_back(result);
      target_process_seen = true;
    } else {
      num_idle_snapshots++;
      if (stop_on_exit && target_process_seen)
        break;
    }
  }

  CloseHandle(ctrl_c_pressed);

  if (results.empty())
    return 0;

  Result average_result;
  average_result.idle_wakeups_per_sec =
      GetAverage(results, GetIdleWakeupsPerSec);
  average_result.cpu_usage_percent = GetAverage(results, GetCpuUsagePercent);
  average_result.cpu_usage_seconds = GetAverage(results, GetCpuUsageSeconds);
  average_result.memory = GetAverage(results, GetMemory);
  average_result.power = GetAverage(results, GetPower);
  average_result.handle_count = GetAverage(results, GetHandleCount);

  const size_t cumulative_processes_destroyed = initial_number_of_processes +
                                                cumulative_processes_created -
                                                final_number_of_processes;

  if (tabbed_summary_only) {
    printf(
        "Processes created\tProcesses destroyed\t"
        "Context switches/sec, average\tCPU usage (%%), average\t"
        "CPU usage (s)\tPrivate commit (MiB), average\t"
        "Power (W), average\n");
    printf("%zu\t%zu\t%20lu\t%8.2f\t%8.2f\t%7.2f\t%5.2f\n",
           cumulative_processes_created, cumulative_processes_destroyed,
           average_result.idle_wakeups_per_sec,
           average_result.cpu_usage_percent, cumulative_cpu_usage_seconds,
           average_result.memory / 1024.0, average_result.power);
    return 0;
  }

  PrintHeader();

  printf("            Average" RESULT_FORMAT_STRING,
         average_result.idle_wakeups_per_sec,
         cpu_usage_in_seconds ? average_result.cpu_usage_seconds
                              : average_result.cpu_usage_percent,
         cpu_usage_unit, average_result.memory / 1024.0, average_result.power,
         average_result.handle_count);

  Result median_result;
  median_result.idle_wakeups_per_sec =
      GetMedian(&results, GetIdleWakeupsPerSec);
  median_result.cpu_usage_percent = GetMedian(&results, GetCpuUsagePercent);
  median_result.cpu_usage_seconds = GetMedian(&results, GetCpuUsageSeconds);
  median_result.memory = GetMedian(&results, GetMemory);
  median_result.power = GetMedian(&results, GetPower);
  median_result.handle_count = GetMedian(&results, GetHandleCount);

  printf("             Median" RESULT_FORMAT_STRING,
         median_result.idle_wakeups_per_sec,
         cpu_usage_in_seconds ? median_result.cpu_usage_seconds
                              : median_result.cpu_usage_percent,
         cpu_usage_unit, median_result.memory / 1024.0, median_result.power,
         median_result.handle_count);

  if (cpu_usage_in_seconds) {
    printf("                Sum    %32.2f%c\n", cumulative_cpu_usage_seconds,
           cpu_usage_unit);
  }

  printf("\n");
  if (num_idle_snapshots > 0)
    printf("Idle snapshots:      %d\n", num_idle_snapshots);
  printf("Processes created:   %zu\n", cumulative_processes_created);
  printf("Processes destroyed: %zu\n", cumulative_processes_destroyed);

  return 0;
}