diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index e8a291b37414e..ec29109a476a5 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -86,6 +86,7 @@ ../../../flutter/fml/closure_unittests.cc ../../../flutter/fml/command_line_unittest.cc ../../../flutter/fml/container_unittests.cc +../../../flutter/fml/cpu_affinity_unittests.cc ../../../flutter/fml/endianness_unittests.cc ../../../flutter/fml/file_unittest.cc ../../../flutter/fml/hash_combine_unittests.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 24db8a78eeb23..d0baecd47acbe 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -870,6 +870,8 @@ ORIGIN: ../../../flutter/fml/concurrent_message_loop.cc + ../../../flutter/LICEN ORIGIN: ../../../flutter/fml/concurrent_message_loop.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/concurrent_message_loop_factory.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/container.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/fml/cpu_affinity.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/fml/cpu_affinity.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/dart/dart_converter.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/dart/dart_converter.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/delayed_task.cc + ../../../flutter/LICENSE @@ -916,6 +918,8 @@ ORIGIN: ../../../flutter/fml/message_loop_task_queues_benchmark.cc + ../../../fl ORIGIN: ../../../flutter/fml/native_library.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/paths.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/paths.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/fml/platform/android/cpu_affinity.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/fml/platform/android/cpu_affinity.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/platform/android/jni_util.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/platform/android/jni_util.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/fml/platform/android/jni_weak_ref.cc + ../../../flutter/LICENSE @@ -3614,6 +3618,8 @@ FILE: ../../../flutter/fml/concurrent_message_loop.cc FILE: ../../../flutter/fml/concurrent_message_loop.h FILE: ../../../flutter/fml/concurrent_message_loop_factory.cc FILE: ../../../flutter/fml/container.h +FILE: ../../../flutter/fml/cpu_affinity.cc +FILE: ../../../flutter/fml/cpu_affinity.h FILE: ../../../flutter/fml/dart/dart_converter.cc FILE: ../../../flutter/fml/dart/dart_converter.h FILE: ../../../flutter/fml/delayed_task.cc @@ -3660,6 +3666,8 @@ FILE: ../../../flutter/fml/message_loop_task_queues_benchmark.cc FILE: ../../../flutter/fml/native_library.h FILE: ../../../flutter/fml/paths.cc FILE: ../../../flutter/fml/paths.h +FILE: ../../../flutter/fml/platform/android/cpu_affinity.cc +FILE: ../../../flutter/fml/platform/android/cpu_affinity.h FILE: ../../../flutter/fml/platform/android/jni_util.cc FILE: ../../../flutter/fml/platform/android/jni_util.h FILE: ../../../flutter/fml/platform/android/jni_weak_ref.cc diff --git a/fml/BUILD.gn b/fml/BUILD.gn index a585864159444..887d7637b7bb7 100644 --- a/fml/BUILD.gn +++ b/fml/BUILD.gn @@ -19,6 +19,8 @@ source_set("fml") { "concurrent_message_loop.cc", "concurrent_message_loop.h", "container.h", + "cpu_affinity.cc", + "cpu_affinity.h", "delayed_task.cc", "delayed_task.h", "eintr_wrapper.h", @@ -170,6 +172,8 @@ source_set("fml") { if (is_android) { sources += [ + "platform/android/cpu_affinity.cc", + "platform/android/cpu_affinity.h", "platform/android/jni_util.cc", "platform/android/jni_util.h", "platform/android/jni_weak_ref.cc", @@ -322,6 +326,7 @@ if (enable_unittests) { "closure_unittests.cc", "command_line_unittest.cc", "container_unittests.cc", + "cpu_affinity_unittests.cc", "endianness_unittests.cc", "file_unittest.cc", "hash_combine_unittests.cc", diff --git a/fml/cpu_affinity.cc b/fml/cpu_affinity.cc new file mode 100644 index 0000000000000..c4e89613b252a --- /dev/null +++ b/fml/cpu_affinity.cc @@ -0,0 +1,78 @@ +// 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. + +#include "flutter/fml/cpu_affinity.h" + +#include +#include +#include + +namespace fml { + +CPUSpeedTracker::CPUSpeedTracker(std::vector data) + : cpu_speeds_(std::move(data)) { + std::optional max_speed = std::nullopt; + std::optional min_speed = std::nullopt; + for (const auto& data : cpu_speeds_) { + if (!max_speed.has_value() || data.speed > max_speed.value()) { + max_speed = data.speed; + } + if (!min_speed.has_value() || data.speed < min_speed.value()) { + min_speed = data.speed; + } + } + if (!max_speed.has_value() || !min_speed.has_value() || + min_speed.value() == max_speed.value()) { + return; + } + + for (const auto& data : cpu_speeds_) { + if (data.speed == max_speed.value()) { + performance_.push_back(data.index); + } else { + not_performance_.push_back(data.index); + } + if (data.speed == min_speed.value()) { + efficiency_.push_back(data.index); + } + } + + valid_ = true; +} + +bool CPUSpeedTracker::IsValid() const { + return valid_; +} + +const std::vector& CPUSpeedTracker::GetIndices( + CpuAffinity affinity) const { + switch (affinity) { + case CpuAffinity::kPerformance: + return performance_; + case CpuAffinity::kEfficiency: + return efficiency_; + case CpuAffinity::kNotPerformance: + return not_performance_; + } +} + +// Get the size of the cpuinfo file by reading it until the end. This is +// required because files under /proc do not always return a valid size +// when using fseek(0, SEEK_END) + ftell(). Nor can they be mmap()-ed. +std::optional ReadIntFromFile(const std::string& path) { + // size_t data_length = 0u; + std::ifstream file; + file.open(path.c_str()); + + // Dont use stoi because if this data isnt a parseable number then it + // will abort, as we compile with exceptions disabled. + int64_t speed = 0; + file >> speed; + if (speed > 0) { + return speed; + } + return std::nullopt; +} + +} // namespace fml diff --git a/fml/cpu_affinity.h b/fml/cpu_affinity.h new file mode 100644 index 0000000000000..3ea45a4fa3d3b --- /dev/null +++ b/fml/cpu_affinity.h @@ -0,0 +1,68 @@ +// 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. + +#pragma once + +#include +#include +#include + +namespace fml { + +/// The CPU Affinity provides a hint to the operating system on which cores a +/// particular thread should be scheduled on. The operating system may or may +/// not honor these requests. +enum class CpuAffinity { + /// @brief Request CPU affinity for the performance cores. + /// + /// Generally speaking, only the UI and Raster thread should + /// use this option. + kPerformance, + + /// @brief Request CPU affinity for the efficiency cores. + kEfficiency, + + /// @brief Request affinity for all non-performance cores. + kNotPerformance, +}; + +struct CpuIndexAndSpeed { + // The index of the given CPU. + size_t index; + // CPU speed in kHZ + int64_t speed; +}; + +/// @brief A class that computes the correct CPU indices for a requested CPU +/// affinity. +/// +/// @note This is visible for testing. +class CPUSpeedTracker { + public: + explicit CPUSpeedTracker(std::vector data); + + /// @brief The class is valid if it has more than one CPU index and a distinct + /// set of efficiency or performance CPUs. + /// + /// If all CPUs are the same speed this returns false, and all requests + /// to set affinity are ignored. + bool IsValid() const; + + /// @brief Return the set of CPU indices for the requested CPU affinity. + /// + /// If the tracker is valid, this will always return a non-empty set. + const std::vector& GetIndices(CpuAffinity affinity) const; + + private: + bool valid_ = false; + std::vector cpu_speeds_; + std::vector efficiency_; + std::vector performance_; + std::vector not_performance_; +}; + +/// @note Visible for testing. +std::optional ReadIntFromFile(const std::string& path); + +} // namespace fml diff --git a/fml/cpu_affinity_unittests.cc b/fml/cpu_affinity_unittests.cc new file mode 100644 index 0000000000000..eb2f010bc66d6 --- /dev/null +++ b/fml/cpu_affinity_unittests.cc @@ -0,0 +1,90 @@ +// 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. + +#include "cpu_affinity.h" + +#include "fml/file.h" +#include "fml/mapping.h" +#include "gtest/gtest.h" +#include "logging.h" + +namespace fml { +namespace testing { + +TEST(CpuAffinity, NormalSlowMedFastCores) { + auto speeds = {CpuIndexAndSpeed{.index = 0, .speed = 1}, + CpuIndexAndSpeed{.index = 1, .speed = 2}, + CpuIndexAndSpeed{.index = 2, .speed = 3}}; + auto tracker = CPUSpeedTracker(speeds); + + ASSERT_TRUE(tracker.IsValid()); + ASSERT_EQ(tracker.GetIndices(CpuAffinity::kEfficiency)[0], 0u); + ASSERT_EQ(tracker.GetIndices(CpuAffinity::kPerformance)[0], 2u); + ASSERT_EQ(tracker.GetIndices(CpuAffinity::kNotPerformance).size(), 2u); + ASSERT_EQ(tracker.GetIndices(CpuAffinity::kNotPerformance)[0], 0u); + ASSERT_EQ(tracker.GetIndices(CpuAffinity::kNotPerformance)[1], 1u); +} + +TEST(CpuAffinity, NoCpuData) { + auto tracker = CPUSpeedTracker({}); + + ASSERT_FALSE(tracker.IsValid()); +} + +TEST(CpuAffinity, AllSameSpeed) { + auto speeds = {CpuIndexAndSpeed{.index = 0, .speed = 1}, + CpuIndexAndSpeed{.index = 1, .speed = 1}, + CpuIndexAndSpeed{.index = 2, .speed = 1}}; + auto tracker = CPUSpeedTracker(speeds); + + ASSERT_FALSE(tracker.IsValid()); +} + +TEST(CpuAffinity, SingleCore) { + auto speeds = {CpuIndexAndSpeed{.index = 0, .speed = 1}}; + auto tracker = CPUSpeedTracker(speeds); + + ASSERT_FALSE(tracker.IsValid()); +} + +TEST(CpuAffinity, FileParsing) { + fml::ScopedTemporaryDirectory base_dir; + ASSERT_TRUE(base_dir.fd().is_valid()); + + // Generate a fake CPU speed file + fml::DataMapping test_data(std::string("12345")); + ASSERT_TRUE(fml::WriteAtomically(base_dir.fd(), "test_file", test_data)); + + auto file = fml::OpenFileReadOnly(base_dir.fd(), "test_file"); + ASSERT_TRUE(file.is_valid()); + + // Open file and parse speed. + auto result = ReadIntFromFile(base_dir.path() + "/test_file"); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result.value_or(0), 12345); +} + +TEST(CpuAffinity, FileParsingWithNonNumber) { + fml::ScopedTemporaryDirectory base_dir; + ASSERT_TRUE(base_dir.fd().is_valid()); + + // Generate a fake CPU speed file + fml::DataMapping test_data(std::string("whoa this isnt a number")); + ASSERT_TRUE(fml::WriteAtomically(base_dir.fd(), "test_file", test_data)); + + auto file = fml::OpenFileReadOnly(base_dir.fd(), "test_file"); + ASSERT_TRUE(file.is_valid()); + + // Open file and parse speed. + auto result = ReadIntFromFile(base_dir.path() + "/test_file"); + ASSERT_FALSE(result.has_value()); +} + +TEST(CpuAffinity, MissingFileParsing) { + auto result = ReadIntFromFile("/does_not_exist"); + ASSERT_FALSE(result.has_value()); +} + +} // namespace testing +} // namespace fml diff --git a/fml/platform/android/cpu_affinity.cc b/fml/platform/android/cpu_affinity.cc new file mode 100644 index 0000000000000..737f8204b9b6e --- /dev/null +++ b/fml/platform/android/cpu_affinity.cc @@ -0,0 +1,59 @@ +// 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. + +#include "flutter/fml/platform/android/cpu_affinity.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fml { + +/// The CPUSpeedTracker is initialized once the first time a thread affinity is +/// requested. +std::once_flag gCPUTrackerFlag; +static CPUSpeedTracker* gCPUTracker; + +// For each CPU index provided, attempts to open the file +// /sys/devices/system/cpu/cpu$NUM/cpufreq/cpuinfo_max_freq and parse a number +// containing the CPU frequency. +void InitCPUInfo(size_t cpu_count) { + std::vector cpu_speeds; + + for (auto i = 0u; i < cpu_count; i++) { + auto path = "/sys/devices/system/cpu/cpu" + std::to_string(i) + + "/cpufreq/cpuinfo_max_freq"; + auto speed = ReadIntFromFile(path); + if (speed.has_value()) { + cpu_speeds.push_back({.index = i, .speed = speed.value()}); + } + } + gCPUTracker = new CPUSpeedTracker(cpu_speeds); +} + +bool RequestAffinity(CpuAffinity affinity) { + // Populate CPU Info if uninitialized. + auto count = std::thread::hardware_concurrency(); + std::call_once(gCPUTrackerFlag, [count]() { InitCPUInfo(count); }); + if (gCPUTracker == nullptr) { + return true; + } + + if (!gCPUTracker->IsValid()) { + return true; + } + + cpu_set_t set; + CPU_ZERO(&set); + for (const auto index : gCPUTracker->GetIndices(affinity)) { + CPU_SET(index, &set); + } + return sched_setaffinity(gettid(), sizeof(set), &set) == 0; +} + +} // namespace fml diff --git a/fml/platform/android/cpu_affinity.h b/fml/platform/android/cpu_affinity.h new file mode 100644 index 0000000000000..03c59ad5e9126 --- /dev/null +++ b/fml/platform/android/cpu_affinity.h @@ -0,0 +1,21 @@ +// 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. + +#pragma once + +#include "flutter/fml/cpu_affinity.h" + +namespace fml { + +/// @brief Request the given affinity for the current thread. +/// +/// Returns true if successfull, or if it was a no-op. This function is +/// only supported on Android devices. +/// +/// Affinity requests are based on documented CPU speed. This speed data +/// is parsed from cpuinfo_max_freq files, see also: +/// https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt +bool RequestAffinity(CpuAffinity affinity); + +} // namespace fml diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index 80eb4839fc439..39aef72727d9d 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -20,6 +20,7 @@ #include "flutter/fml/make_copyable.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/native_library.h" +#include "flutter/fml/platform/android/cpu_affinity.h" #include "flutter/fml/platform/android/jni_util.h" #include "flutter/lib/ui/painting/image_generator_registry.h" #include "flutter/shell/common/rasterizer.h" @@ -41,18 +42,21 @@ static void AndroidPlatformThreadConfigSetter( // set thread priority switch (config.priority) { case fml::Thread::ThreadPriority::BACKGROUND: { + fml::RequestAffinity(fml::CpuAffinity::kEfficiency); if (::setpriority(PRIO_PROCESS, 0, 10) != 0) { FML_LOG(ERROR) << "Failed to set IO task runner priority"; } break; } case fml::Thread::ThreadPriority::DISPLAY: { + fml::RequestAffinity(fml::CpuAffinity::kPerformance); if (::setpriority(PRIO_PROCESS, 0, -1) != 0) { FML_LOG(ERROR) << "Failed to set UI task runner priority"; } break; } case fml::Thread::ThreadPriority::RASTER: { + fml::RequestAffinity(fml::CpuAffinity::kPerformance); // Android describes -8 as "most important display threads, for // compositing the screen and retrieving input events". Conservatively // set the raster thread to slightly lower priority than it. @@ -66,6 +70,7 @@ static void AndroidPlatformThreadConfigSetter( break; } default: + fml::RequestAffinity(fml::CpuAffinity::kNotPerformance); if (::setpriority(PRIO_PROCESS, 0, 0) != 0) { FML_LOG(ERROR) << "Failed to set priority"; }