chromium/tools/mac/power/power_sampler/resource_coalition_sampler_unittest.cc

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

#include "tools/mac/power/power_sampler/resource_coalition_sampler.h"

#include <stdint.h>

#include <memory>
#include <optional>
#include <utility>

#include "base/containers/contains.h"
#include "base/process/process_handle.h"
#include "components/power_metrics/energy_impact_mac.h"
#include "components/power_metrics/mach_time_mac.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace power_sampler {

namespace {

using testing::UnorderedElementsAre;

constexpr base::ProcessId kTestPid = 42;
constexpr base::ProcessId kTestCoalitionId = 123;
constexpr mach_timebase_info_data_t kIntelTimebase = {1, 1};
mach_timebase_info_data_t kM1Timebase = {125, 3};

constexpr power_metrics::EnergyImpactCoefficients kTestEnergyImpactCoefficients{
    .kcpu_wakeups = base::Microseconds(1000).InSecondsF(),
    .kqos_default = 3000,
    .kqos_background = 6000,
    .kqos_utility = 9000,
    .kqos_legacy = 12000,
    .kqos_user_initiated = 15000,
    .kqos_user_interactive = 18000,
    .kgpu_time = 21000,
};

coalition_resource_usage GetTestCoalitionResourceUsage(uint32_t multiplier) {
  coalition_resource_usage ret{
      .tasks_started = 1 * multiplier,
      .tasks_exited = 2 * multiplier,
      .time_nonempty = 3 * multiplier,
      .cpu_time = 4 * multiplier,
      .interrupt_wakeups = 5 * multiplier,
      .platform_idle_wakeups = 6 * multiplier,
      .bytesread = 7 * multiplier,
      .byteswritten = 8 * multiplier,
      .gpu_time = 9 * multiplier,
      .cpu_time_billed_to_me = 10 * multiplier,
      .cpu_time_billed_to_others = 11 * multiplier,
      .energy = 12 * multiplier,
      .logical_immediate_writes = 13 * multiplier,
      .logical_deferred_writes = 14 * multiplier,
      .logical_invalidated_writes = 15 * multiplier,
      .logical_metadata_writes = 16 * multiplier,
      .logical_immediate_writes_to_external = 17 * multiplier,
      .logical_deferred_writes_to_external = 18 * multiplier,
      .logical_invalidated_writes_to_external = 19 * multiplier,
      .logical_metadata_writes_to_external = 20 * multiplier,
      .energy_billed_to_me = 21 * multiplier,
      .energy_billed_to_others = 22 * multiplier,
      .cpu_ptime = 23 * multiplier,
      .cpu_time_eqos_len = COALITION_NUM_THREAD_QOS_TYPES,
      .cpu_instructions = 31 * multiplier,
      .cpu_cycles = 32 * multiplier,
      .fs_metadata_writes = 33 * multiplier,
      .pm_writes = 34 * multiplier,
  };

  ret.cpu_time_eqos[THREAD_QOS_DEFAULT] = 24 * multiplier;
  ret.cpu_time_eqos[THREAD_QOS_MAINTENANCE] = 25 * multiplier;
  ret.cpu_time_eqos[THREAD_QOS_BACKGROUND] = 26 * multiplier;
  ret.cpu_time_eqos[THREAD_QOS_UTILITY] = 27 * multiplier;
  ret.cpu_time_eqos[THREAD_QOS_LEGACY] = 28 * multiplier;
  ret.cpu_time_eqos[THREAD_QOS_USER_INITIATED] = 29 * multiplier;
  ret.cpu_time_eqos[THREAD_QOS_USER_INTERACTIVE] = 30 * multiplier;

  return ret;
}

double NsToM1Timebase(int64_t ns) {
  return static_cast<double>(ns * kM1Timebase.numer) / kM1Timebase.denom;
}

}  // namespace

class ResourceCoalitionSamplerTest : public testing::Test {
 protected:
  void SetUp() override {
    set_expected_process_id(-1);
    set_coalition_id(std::nullopt);
    set_resource_usage(std::nullopt);
  }

  static void set_expected_process_id(base::ProcessId expected_process_id) {
    expected_process_id_ = expected_process_id;
  }

  static void set_coalition_id(std::optional<uint64_t> coalition_id) {
    coalition_id_ = coalition_id;
  }

  static void set_resource_usage(
      std::optional<coalition_resource_usage> resource_usage) {
    resource_usage_ = resource_usage;
  }

  // The gmock *ElementsAre* matchers are too exacting for the double values
  // in our samples, but this poor man's substitute will do for our needs.
  template <size_t N>
  void ExpectSampleMatchesArray(
      const Sampler::Sample& sample,
      const std::pair<std::string, double> (&datums)[N]) {
    EXPECT_EQ(N, sample.size());
    for (size_t i = 0; i < N; ++i) {
      const auto& name = datums[i].first;
      const double value = datums[i].second;

      auto it = sample.find(name);
      EXPECT_TRUE(it != sample.end()) << " for " << name;
      if (it != sample.end())
        EXPECT_DOUBLE_EQ(it->second, value) << " for " << name;
    }
  }

  std::unique_ptr<ResourceCoalitionSampler> CreateSampler(
      base::ProcessId pid,
      base::TimeTicks now,
      mach_timebase_info_data_t timebase,
      std::optional<power_metrics::EnergyImpactCoefficients>
          energy_impact_coefficients = kTestEnergyImpactCoefficients) {
    std::unique_ptr<ResourceCoalitionSampler> sampler =
        ResourceCoalitionSampler::Create(pid, now, &GetStaticProcessCoalitionId,
                                         &GetStaticCoalitionResourceUsage,
                                         timebase);
    if (sampler)
      sampler->energy_impact_coefficients_ = energy_impact_coefficients;
    return sampler;
  }

 private:
  static std::optional<uint64_t> GetStaticProcessCoalitionId(
      base::ProcessId pid) {
    EXPECT_EQ(pid, expected_process_id_);
    return coalition_id_;
  }

  static std::unique_ptr<coalition_resource_usage>
  GetStaticCoalitionResourceUsage(int64_t coalition_id) {
    EXPECT_EQ(coalition_id, coalition_id_);
    if (!resource_usage_.has_value())
      return nullptr;
    return std::make_unique<coalition_resource_usage>(resource_usage_.value());
  }

  static base::ProcessId expected_process_id_;
  static std::optional<int64_t> coalition_id_;
  static std::optional<coalition_resource_usage> resource_usage_;
};

// static
base::ProcessId ResourceCoalitionSamplerTest::expected_process_id_ = -1;
std::optional<int64_t> ResourceCoalitionSamplerTest::coalition_id_;
std::optional<coalition_resource_usage>
    ResourceCoalitionSamplerTest::resource_usage_;

TEST_F(ResourceCoalitionSamplerTest, CreateFailsWhenNoCoalitionId) {
  set_expected_process_id(kTestPid);
  EXPECT_EQ(nullptr,
            CreateSampler(kTestPid, base::TimeTicks(), kIntelTimebase));
}

TEST_F(ResourceCoalitionSamplerTest, CreateSucceedsWithCoalitonId) {
  set_expected_process_id(kTestPid);
  set_coalition_id(kTestCoalitionId);
  EXPECT_NE(nullptr,
            CreateSampler(kTestPid, base::TimeTicks(), kIntelTimebase));
}

TEST_F(ResourceCoalitionSamplerTest, NameAndGetDatumNameUnits) {
  set_expected_process_id(kTestPid);
  set_coalition_id(kTestCoalitionId);
  std::unique_ptr<ResourceCoalitionSampler> sampler(
      CreateSampler(kTestPid, base::TimeTicks(), kIntelTimebase));
  ASSERT_NE(nullptr, sampler);

  EXPECT_EQ("resource_coalition", sampler->GetName());

  auto datum_name_units = sampler->GetDatumNameUnits();
  EXPECT_THAT(
      datum_name_units,
      UnorderedElementsAre(
          std::make_pair("tasks_started", "tasks/s"),
          std::make_pair("tasks_exited", "tasks/s"),
          std::make_pair("time_nonempty", "ns/s"),
          std::make_pair("cpu_time", "ns/s"),
          std::make_pair("interrupt_wakeups", "wakeups/s"),
          std::make_pair("platform_idle_wakeups", "wakeups/s"),
          std::make_pair("bytesread", "bytes/s"),
          std::make_pair("byteswritten", "bytes/s"),
          std::make_pair("gpu_time", "ns/s"),
          std::make_pair("cpu_time_billed_to_me", "ns/s"),
          std::make_pair("cpu_time_billed_to_others", "ns/s"),
          std::make_pair("energy", "nw"),
          std::make_pair("logical_immediate_writes", "writes/s"),
          std::make_pair("logical_deferred_writes", "writes/s"),
          std::make_pair("logical_invalidated_writes", "writes/s"),
          std::make_pair("logical_metadata_writes", "writes/s"),
          std::make_pair("logical_immediate_writes_to_external", "writes/s"),
          std::make_pair("logical_deferred_writes_to_external", "writes/s"),
          std::make_pair("logical_invalidated_writes_to_external", "writes/s"),
          std::make_pair("logical_metadata_writes_to_external", "writes/s"),
          std::make_pair("energy_billed_to_me", "nw"),
          std::make_pair("energy_billed_to_others", "nw"),
          std::make_pair("cpu_ptime", "ns/s"),
          std::make_pair("cpu_time_qos_default", "ns/s"),
          std::make_pair("cpu_time_qos_background", "ns/s"),
          std::make_pair("cpu_time_qos_utility", "ns/s"),
          std::make_pair("cpu_time_qos_legacy", "ns/s"),
          std::make_pair("cpu_time_qos_maintenance", "ns/s"),
          std::make_pair("cpu_time_qos_user_initiated", "ns/s"),
          std::make_pair("cpu_time_qos_user_interactive", "ns/s"),
          std::make_pair("cpu_instructions", "instructions/s"),
          std::make_pair("cpu_cycles", "cycles/s"),
          std::make_pair("fs_metadata_writes", "writes/s"),
          std::make_pair("pm_writes", "writes/s"),
          std::make_pair("energy_impact", "EnergyImpact/s")));
}

TEST_F(ResourceCoalitionSamplerTest, GetSample_Available_IntelTimebase) {
  set_expected_process_id(kTestPid);
  set_coalition_id(kTestCoalitionId);
  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/1 * base::Time::kSecondsPerMinute));

  std::unique_ptr<ResourceCoalitionSampler> sampler(
      CreateSampler(kTestPid, base::TimeTicks(), kIntelTimebase));

  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/2 * base::Time::kSecondsPerMinute));
  Sampler::Sample sample =
      sampler->GetSample(base::TimeTicks() + base::Minutes(1));
  ExpectSampleMatchesArray(
      sample, {std::make_pair("tasks_started", 1),
               std::make_pair("tasks_exited", 2),
               std::make_pair("time_nonempty", 3),
               std::make_pair("cpu_time", 4),
               std::make_pair("interrupt_wakeups", 5),
               std::make_pair("platform_idle_wakeups", 6),
               std::make_pair("bytesread", 7),
               std::make_pair("byteswritten", 8),
               std::make_pair("gpu_time", 9),
               std::make_pair("cpu_time_billed_to_me", 10),
               std::make_pair("cpu_time_billed_to_others", 11),
               std::make_pair("energy", 12),
               std::make_pair("logical_immediate_writes", 13),
               std::make_pair("logical_deferred_writes", 14),
               std::make_pair("logical_invalidated_writes", 15),
               std::make_pair("logical_metadata_writes", 16),
               std::make_pair("logical_immediate_writes_to_external", 17),
               std::make_pair("logical_deferred_writes_to_external", 18),
               std::make_pair("logical_invalidated_writes_to_external", 19),
               std::make_pair("logical_metadata_writes_to_external", 20),
               std::make_pair("energy_billed_to_me", 21),
               std::make_pair("energy_billed_to_others", 22),
               std::make_pair("cpu_ptime", 23),
               std::make_pair("cpu_time_qos_default", 24),
               std::make_pair("cpu_time_qos_maintenance", 25),
               std::make_pair("cpu_time_qos_background", 26),
               std::make_pair("cpu_time_qos_utility", 27),
               std::make_pair("cpu_time_qos_legacy", 28),
               std::make_pair("cpu_time_qos_user_initiated", 29),
               std::make_pair("cpu_time_qos_user_interactive", 30),
               std::make_pair("cpu_instructions", 31),
               std::make_pair("cpu_cycles", 32),
               std::make_pair("fs_metadata_writes", 33),
               std::make_pair("pm_writes", 34),
               std::make_pair("energy_impact", 0.7971)});

  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/4 * base::Time::kSecondsPerMinute));
  sample = sampler->GetSample(base::TimeTicks() + base::Minutes(2));
  ExpectSampleMatchesArray(
      sample, {std::make_pair("tasks_started", 2),
               std::make_pair("tasks_exited", 4),
               std::make_pair("time_nonempty", 6),
               std::make_pair("cpu_time", 8),
               std::make_pair("interrupt_wakeups", 10),
               std::make_pair("platform_idle_wakeups", 12),
               std::make_pair("bytesread", 14),
               std::make_pair("byteswritten", 16),
               std::make_pair("gpu_time", 18),
               std::make_pair("cpu_time_billed_to_me", 20),
               std::make_pair("cpu_time_billed_to_others", 22),
               std::make_pair("energy", 24),
               std::make_pair("logical_immediate_writes", 26),
               std::make_pair("logical_deferred_writes", 28),
               std::make_pair("logical_invalidated_writes", 30),
               std::make_pair("logical_metadata_writes", 32),
               std::make_pair("logical_immediate_writes_to_external", 34),
               std::make_pair("logical_deferred_writes_to_external", 36),
               std::make_pair("logical_invalidated_writes_to_external", 38),
               std::make_pair("logical_metadata_writes_to_external", 40),
               std::make_pair("energy_billed_to_me", 42),
               std::make_pair("energy_billed_to_others", 44),
               std::make_pair("cpu_ptime", 46),
               std::make_pair("cpu_time_qos_default", 48),
               std::make_pair("cpu_time_qos_maintenance", 50),
               std::make_pair("cpu_time_qos_background", 52),
               std::make_pair("cpu_time_qos_utility", 54),
               std::make_pair("cpu_time_qos_legacy", 56),
               std::make_pair("cpu_time_qos_user_initiated", 58),
               std::make_pair("cpu_time_qos_user_interactive", 60),
               std::make_pair("cpu_instructions", 62),
               std::make_pair("cpu_cycles", 64),
               std::make_pair("fs_metadata_writes", 66),
               std::make_pair("pm_writes", 68),
               std::make_pair("energy_impact", 1.5942)});
}

TEST_F(ResourceCoalitionSamplerTest, GetSample_Available_M1Timebase) {
  set_expected_process_id(kTestPid);
  set_coalition_id(kTestCoalitionId);
  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/1 * base::Time::kSecondsPerMinute));

  std::unique_ptr<ResourceCoalitionSampler> sampler(
      CreateSampler(kTestPid, base::TimeTicks(), kM1Timebase));

  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/2 * base::Time::kSecondsPerMinute));
  Sampler::Sample sample =
      sampler->GetSample(base::TimeTicks() + base::Minutes(1));
  ExpectSampleMatchesArray(
      sample,
      {std::make_pair("tasks_started", 1),
       std::make_pair("tasks_exited", 2),
       std::make_pair("time_nonempty", 3),
       std::make_pair("cpu_time", NsToM1Timebase(4)),
       std::make_pair("interrupt_wakeups", 5),
       std::make_pair("platform_idle_wakeups", 6),
       std::make_pair("bytesread", 7),
       std::make_pair("byteswritten", 8),
       std::make_pair("gpu_time", NsToM1Timebase(9)),
       std::make_pair("cpu_time_billed_to_me", NsToM1Timebase(10)),
       std::make_pair("cpu_time_billed_to_others", NsToM1Timebase(11)),
       std::make_pair("energy", 12),
       std::make_pair("logical_immediate_writes", 13),
       std::make_pair("logical_deferred_writes", 14),
       std::make_pair("logical_invalidated_writes", 15),
       std::make_pair("logical_metadata_writes", 16),
       std::make_pair("logical_immediate_writes_to_external", 17),
       std::make_pair("logical_deferred_writes_to_external", 18),
       std::make_pair("logical_invalidated_writes_to_external", 19),
       std::make_pair("logical_metadata_writes_to_external", 20),
       std::make_pair("energy_billed_to_me", 21),
       std::make_pair("energy_billed_to_others", 22),
       std::make_pair("cpu_ptime", NsToM1Timebase(23)),
       std::make_pair("cpu_time_qos_default", NsToM1Timebase(24)),
       std::make_pair("cpu_time_qos_maintenance", NsToM1Timebase(25)),
       std::make_pair("cpu_time_qos_background", NsToM1Timebase(26)),
       std::make_pair("cpu_time_qos_utility", NsToM1Timebase(27)),
       std::make_pair("cpu_time_qos_legacy", NsToM1Timebase(28)),
       std::make_pair("cpu_time_qos_user_initiated", NsToM1Timebase(29)),
       std::make_pair("cpu_time_qos_user_interactive", NsToM1Timebase(30)),
       std::make_pair("cpu_instructions", 31),
       std::make_pair("cpu_cycles", 32),
       std::make_pair("fs_metadata_writes", 33),
       std::make_pair("pm_writes", 34),
       std::make_pair("energy_impact", 8.8125)});

  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/4 * base::Time::kSecondsPerMinute));
  sample = sampler->GetSample(base::TimeTicks() + base::Minutes(2));
  ExpectSampleMatchesArray(
      sample,
      {std::make_pair("tasks_started", 2),
       std::make_pair("tasks_exited", 4),
       std::make_pair("time_nonempty", 6),
       std::make_pair("cpu_time", NsToM1Timebase(8)),
       std::make_pair("interrupt_wakeups", 10),
       std::make_pair("platform_idle_wakeups", 12),
       std::make_pair("bytesread", 14),
       std::make_pair("byteswritten", 16),
       std::make_pair("gpu_time", NsToM1Timebase(18)),
       std::make_pair("cpu_time_billed_to_me", NsToM1Timebase(20)),
       std::make_pair("cpu_time_billed_to_others", NsToM1Timebase(22)),
       std::make_pair("energy", 24),
       std::make_pair("logical_immediate_writes", 26),
       std::make_pair("logical_deferred_writes", 28),
       std::make_pair("logical_invalidated_writes", 30),
       std::make_pair("logical_metadata_writes", 32),
       std::make_pair("logical_immediate_writes_to_external", 34),
       std::make_pair("logical_deferred_writes_to_external", 36),
       std::make_pair("logical_invalidated_writes_to_external", 38),
       std::make_pair("logical_metadata_writes_to_external", 40),
       std::make_pair("energy_billed_to_me", 42),
       std::make_pair("energy_billed_to_others", 44),
       std::make_pair("cpu_ptime", NsToM1Timebase(46)),
       std::make_pair("cpu_time_qos_default", NsToM1Timebase(48)),
       std::make_pair("cpu_time_qos_maintenance", NsToM1Timebase(50)),
       std::make_pair("cpu_time_qos_background", NsToM1Timebase(52)),
       std::make_pair("cpu_time_qos_utility", NsToM1Timebase(54)),
       std::make_pair("cpu_time_qos_legacy", NsToM1Timebase(56)),
       std::make_pair("cpu_time_qos_user_initiated", NsToM1Timebase(58)),
       std::make_pair("cpu_time_qos_user_interactive", NsToM1Timebase(60)),
       std::make_pair("cpu_instructions", 62),
       std::make_pair("cpu_cycles", 64),
       std::make_pair("fs_metadata_writes", 66),
       std::make_pair("pm_writes", 68),
       std::make_pair("energy_impact", 17.625)});
}

TEST_F(ResourceCoalitionSamplerTest,
       GetSample_NoEnergyImpactWithoutCoefficients) {
  set_expected_process_id(kTestPid);
  set_coalition_id(kTestCoalitionId);
  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/1 * base::Time::kSecondsPerMinute));

  std::unique_ptr<ResourceCoalitionSampler> sampler(
      CreateSampler(kTestPid, base::TimeTicks(), kIntelTimebase,
                    /* energy_impact_coefficients=*/std::nullopt));

  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/2 * base::Time::kSecondsPerMinute));
  Sampler::Sample sample =
      sampler->GetSample(base::TimeTicks() + base::Minutes(1));
  EXPECT_FALSE(base::Contains(sample, "energy_impact"));
}

TEST_F(ResourceCoalitionSamplerTest, GetSample_NotAvailable) {
  set_expected_process_id(kTestPid);
  set_coalition_id(kTestCoalitionId);
  set_resource_usage(GetTestCoalitionResourceUsage(/* multiplier=*/1));

  std::unique_ptr<ResourceCoalitionSampler> sampler(
      CreateSampler(kTestPid, base::TimeTicks(), kIntelTimebase));

  // Previous `coalition_resource_usage` is available but not the current one.
  set_resource_usage(std::nullopt);
  Sampler::Sample sample =
      sampler->GetSample(base::TimeTicks() + base::Minutes(1));
  EXPECT_TRUE(sample.empty());

  // Current `coalition_resource_usage` is available but not the previous one.
  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/2 * base::Time::kSecondsPerMinute));
  sample = sampler->GetSample(base::TimeTicks() + base::Minutes(2));
  EXPECT_TRUE(sample.empty());

  // Both current and previous `coalition_resource_usage` are available.
  set_resource_usage(GetTestCoalitionResourceUsage(
      /* multiplier=*/3 * base::Time::kSecondsPerMinute));
  sample = sampler->GetSample(base::TimeTicks() + base::Minutes(3));
  ExpectSampleMatchesArray(
      sample, {std::make_pair("tasks_started", 1),
               std::make_pair("tasks_exited", 2),
               std::make_pair("time_nonempty", 3),
               std::make_pair("cpu_time", 4),
               std::make_pair("interrupt_wakeups", 5),
               std::make_pair("platform_idle_wakeups", 6),
               std::make_pair("bytesread", 7),
               std::make_pair("byteswritten", 8),
               std::make_pair("gpu_time", 9),
               std::make_pair("cpu_time_billed_to_me", 10),
               std::make_pair("cpu_time_billed_to_others", 11),
               std::make_pair("energy", 12),
               std::make_pair("logical_immediate_writes", 13),
               std::make_pair("logical_deferred_writes", 14),
               std::make_pair("logical_invalidated_writes", 15),
               std::make_pair("logical_metadata_writes", 16),
               std::make_pair("logical_immediate_writes_to_external", 17),
               std::make_pair("logical_deferred_writes_to_external", 18),
               std::make_pair("logical_invalidated_writes_to_external", 19),
               std::make_pair("logical_metadata_writes_to_external", 20),
               std::make_pair("energy_billed_to_me", 21),
               std::make_pair("energy_billed_to_others", 22),
               std::make_pair("cpu_ptime", 23),
               std::make_pair("cpu_time_qos_default", 24),
               std::make_pair("cpu_time_qos_maintenance", 25),
               std::make_pair("cpu_time_qos_background", 26),
               std::make_pair("cpu_time_qos_utility", 27),
               std::make_pair("cpu_time_qos_legacy", 28),
               std::make_pair("cpu_time_qos_user_initiated", 29),
               std::make_pair("cpu_time_qos_user_interactive", 30),
               std::make_pair("cpu_instructions", 31),
               std::make_pair("cpu_cycles", 32),
               std::make_pair("fs_metadata_writes", 33),
               std::make_pair("pm_writes", 34),
               std::make_pair("energy_impact", 0.7971)});
}

}  // namespace power_sampler