Skip to content
This repository has been archived by the owner on Feb 25, 2025. It is now read-only.

[Android] Add support for setting thread affinity based on core speed. #45673

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions fml/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
78 changes: 78 additions & 0 deletions fml/cpu_affinity.cc
Original file line number Diff line number Diff line change
@@ -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 <fstream>
#include <optional>
#include <string>

namespace fml {

CPUSpeedTracker::CPUSpeedTracker(std::vector<CpuIndexAndSpeed> data)
: cpu_speeds_(std::move(data)) {
std::optional<int64_t> max_speed = std::nullopt;
std::optional<int64_t> 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<size_t>& 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<int64_t> ReadIntFromFile(const std::string& path) {
// size_t data_length = 0u;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented out code

std::ifstream file;
file.open(path.c_str());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file.fail() will be true if the open fails.

https://cplusplus.com/reference/fstream/ifstream/open/


// 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading the docs right, then file.fail() will be true if an int couldn't be read.

https://cplusplus.com/reference/istream/istream/operator%3E%3E/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, the tests I added, which include tests for missing files and non-numbers still pass without checking - I assume because in these cases we don't read anything out of the stream?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, if the open or read fails, then speed will just stay 0 and this check will fail.

return speed;
}
return std::nullopt;
}

} // namespace fml
68 changes: 68 additions & 0 deletions fml/cpu_affinity.h
Original file line number Diff line number Diff line change
@@ -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 <optional>
#include <string>
#include <vector>

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<CpuIndexAndSpeed> 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<size_t>& GetIndices(CpuAffinity affinity) const;

private:
bool valid_ = false;
std::vector<CpuIndexAndSpeed> cpu_speeds_;
std::vector<size_t> efficiency_;
std::vector<size_t> performance_;
std::vector<size_t> not_performance_;
};

/// @note Visible for testing.
std::optional<int64_t> ReadIntFromFile(const std::string& path);

} // namespace fml
90 changes: 90 additions & 0 deletions fml/cpu_affinity_unittests.cc
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions fml/platform/android/cpu_affinity.cc
Original file line number Diff line number Diff line change
@@ -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 <pthread.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>
#include <mutex>
#include <optional>
#include <thread>

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<CpuIndexAndSpeed> 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
21 changes: 21 additions & 0 deletions fml/platform/android/cpu_affinity.h
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linux too right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the underlying APIs, but I only added support to Android platform code as testing the linux embedding as well as arbitrary 3rd party linux embedders seems like a bit of a nightmare.

///
/// 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
Loading