From 8760424d76bcb94efa0d7e39197c3758a161e7ea Mon Sep 17 00:00:00 2001 From: vigneshv Date: Mon, 27 Apr 2020 21:15:49 +0100 Subject: [PATCH] av1_extension: Add a heuristic to determine default thread count Android scheduler has performance issues when a device has a combiation of big/medium/little cores. Add a heuristic to set the default number of threads used for deocding to the number of "performance" (i.e. big) cores. PiperOrigin-RevId: 308683989 --- RELEASENOTES.md | 2 + .../exoplayer2/ext/av1/Gav1Decoder.java | 23 ++- .../ext/av1/Libgav1VideoRenderer.java | 15 +- extensions/av1/src/main/jni/CMakeLists.txt | 4 +- extensions/av1/src/main/jni/cpu_info.cc | 153 ++++++++++++++++++ extensions/av1/src/main/jni/cpu_info.h | 13 ++ extensions/av1/src/main/jni/gav1_jni.cc | 5 + 7 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 extensions/av1/src/main/jni/cpu_info.cc create mode 100644 extensions/av1/src/main/jni/cpu_info.h diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 89d630527a..95e2a0581f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Text * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles ([#6950](https://github.com/google/ExoPlayer/pull/6950)). +* AV1 extension: Add a heuristic to determine the default number of threads + used for AV1 playback using the extension. ### 2.11.4 (2020-04-08) ### diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java index 687ac47f2a..cdff8581f1 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.av1; +import static java.lang.Runtime.getRuntime; + import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -44,7 +46,9 @@ import java.nio.ByteBuffer; * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. * @param initialInputBufferSize The initial size of each input buffer, in bytes. - * @param threads Number of threads libgav1 will use to decode. + * @param threads Number of threads libgav1 will use to decode. If {@link + * Libgav1VideoRenderer#THREAD_COUNT_AUTODETECT} is passed, then this class will auto detect + * the number of threads to be used. * @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder. */ public Gav1Decoder( @@ -56,6 +60,16 @@ import java.nio.ByteBuffer; if (!Gav1Library.isAvailable()) { throw new Gav1DecoderException("Failed to load decoder native library."); } + + if (threads == Libgav1VideoRenderer.THREAD_COUNT_AUTODETECT) { + // Try to get the optimal number of threads from the AV1 heuristic. + threads = gav1GetThreads(); + if (threads <= 0) { + // If that is not available, default to the number of available processors. + threads = getRuntime().availableProcessors(); + } + } + gav1DecoderContext = gav1Init(threads); if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) { throw new Gav1DecoderException( @@ -231,4 +245,11 @@ import java.nio.ByteBuffer; * @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured. */ private native int gav1CheckError(long context); + + /** + * Returns the optimal number of threads to be used for AV1 decoding. + * + * @return Optimal number of threads if there was no error, 0 if an error occurred. + */ + private native int gav1GetThreads(); } diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java index 3d10c2579b..122a94b7b1 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.ext.av1; -import static java.lang.Runtime.getRuntime; - import android.os.Handler; import android.view.Surface; import androidx.annotation.Nullable; @@ -55,6 +53,13 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; */ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { + /** + * Attempts to use as many threads as performance processors available on the device. If the + * number of performance processors cannot be detected, the number of available processors is + * used. + */ + public static final int THREAD_COUNT_AUTODETECT = 0; + private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4; private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4; /* Default size based on 720p resolution video compressed by a factor of two. */ @@ -94,7 +99,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { eventHandler, eventListener, maxDroppedFramesToNotify, - /* threads= */ getRuntime().availableProcessors(), + THREAD_COUNT_AUTODETECT, DEFAULT_NUM_OF_INPUT_BUFFERS, DEFAULT_NUM_OF_OUTPUT_BUFFERS); } @@ -109,7 +114,9 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param threads Number of threads libgav1 will use to decode. + * @param threads Number of threads libgav1 will use to decode. If + * {@link #THREAD_COUNT_AUTODETECT} is passed, then the number of threads to use is + * auto-detected based on CPU capabilities. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. */ diff --git a/extensions/av1/src/main/jni/CMakeLists.txt b/extensions/av1/src/main/jni/CMakeLists.txt index c7989d4ef2..075773a70e 100644 --- a/extensions/av1/src/main/jni/CMakeLists.txt +++ b/extensions/av1/src/main/jni/CMakeLists.txt @@ -44,7 +44,9 @@ add_subdirectory("${libgav1_root}" # Build libgav1JNI. add_library(gav1JNI SHARED - gav1_jni.cc) + gav1_jni.cc + cpu_info.cc + cpu_info.h) # Locate NDK log library. find_library(android_log_lib log) diff --git a/extensions/av1/src/main/jni/cpu_info.cc b/extensions/av1/src/main/jni/cpu_info.cc new file mode 100644 index 0000000000..8f4a405f4f --- /dev/null +++ b/extensions/av1/src/main/jni/cpu_info.cc @@ -0,0 +1,153 @@ +#include "cpu_info.h" // NOLINT + +#include + +#include +#include +#include +#include +#include + +namespace gav1_jni { +namespace { + +// Note: The code in this file needs to use the 'long' type because it is the +// return type of the Standard C Library function strtol(). The linter warnings +// are suppressed with NOLINT comments since they are integers at runtime. + +// Returns the number of online processor cores. +int GetNumberOfProcessorsOnline() { + // See https://developer.android.com/ndk/guides/cpu-features. + long num_cpus = sysconf(_SC_NPROCESSORS_ONLN); // NOLINT + if (num_cpus < 0) { + return 0; + } + // It is safe to cast num_cpus to int. sysconf(_SC_NPROCESSORS_ONLN) returns + // the return value of get_nprocs(), which is an int. + return static_cast(num_cpus); +} + +} // namespace + +// These CPUs support heterogeneous multiprocessing. +#if defined(__arm__) || defined(__aarch64__) + +// A helper function used by GetNumberOfPerformanceCoresOnline(). +// +// Returns the cpuinfo_max_freq value (in kHz) of the given CPU. Returns 0 on +// failure. +long GetCpuinfoMaxFreq(int cpu_index) { // NOLINT + char buffer[128]; + const int rv = snprintf( + buffer, sizeof(buffer), + "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu_index); + if (rv < 0 || rv >= sizeof(buffer)) { + return 0; + } + FILE* file = fopen(buffer, "r"); + if (file == nullptr) { + return 0; + } + char* const str = fgets(buffer, sizeof(buffer), file); + fclose(file); + if (str == nullptr) { + return 0; + } + const long freq = strtol(str, nullptr, 10); // NOLINT + if (freq <= 0 || freq == LONG_MAX) { + return 0; + } + return freq; +} + +// Returns the number of performance CPU cores that are online. The number of +// efficiency CPU cores is subtracted from the total number of CPU cores. Uses +// cpuinfo_max_freq to determine whether a CPU is a performance core or an +// efficiency core. +// +// This function is not perfect. For example, the Snapdragon 632 SoC used in +// Motorola Moto G7 has performance and efficiency cores with the same +// cpuinfo_max_freq but different cpuinfo_min_freq. This function fails to +// differentiate the two kinds of cores and reports all the cores as +// performance cores. +int GetNumberOfPerformanceCoresOnline() { + // Get the online CPU list. Some examples of the online CPU list are: + // "0-7" + // "0" + // "0-1,2,3,4-7" + FILE* file = fopen("/sys/devices/system/cpu/online", "r"); + if (file == nullptr) { + return 0; + } + char online[512]; + char* const str = fgets(online, sizeof(online), file); + fclose(file); + file = nullptr; + if (str == nullptr) { + return 0; + } + + // Count the number of the slowest CPUs. Some SoCs such as Snapdragon 855 + // have performance cores with different max frequencies, so only the slowest + // CPUs are efficiency cores. If we count the number of the fastest CPUs, we + // will fail to count the second fastest performance cores. + long slowest_cpu_freq = LONG_MAX; // NOLINT + int num_slowest_cpus = 0; + int num_cpus = 0; + const char* cp = online; + int range_begin = -1; + while (true) { + char* str_end; + const int cpu = static_cast(strtol(cp, &str_end, 10)); // NOLINT + if (str_end == cp) { + break; + } + cp = str_end; + if (*cp == '-') { + range_begin = cpu; + } else { + if (range_begin == -1) { + range_begin = cpu; + } + + num_cpus += cpu - range_begin + 1; + for (int i = range_begin; i <= cpu; ++i) { + const long freq = GetCpuinfoMaxFreq(i); // NOLINT + if (freq <= 0) { + return 0; + } + if (freq < slowest_cpu_freq) { + slowest_cpu_freq = freq; + num_slowest_cpus = 0; + } + if (freq == slowest_cpu_freq) { + ++num_slowest_cpus; + } + } + + range_begin = -1; + } + if (*cp == '\0') { + break; + } + ++cp; + } + + // If there are faster CPU cores than the slowest CPU cores, exclude the + // slowest CPU cores. + if (num_slowest_cpus < num_cpus) { + num_cpus -= num_slowest_cpus; + } + return num_cpus; +} + +#else + +// Assume symmetric multiprocessing. +int GetNumberOfPerformanceCoresOnline() { + return GetNumberOfProcessorsOnline(); +} + +#endif + +} // namespace gav1_jni diff --git a/extensions/av1/src/main/jni/cpu_info.h b/extensions/av1/src/main/jni/cpu_info.h new file mode 100644 index 0000000000..77f869a93e --- /dev/null +++ b/extensions/av1/src/main/jni/cpu_info.h @@ -0,0 +1,13 @@ +#ifndef EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ +#define EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ + +namespace gav1_jni { + +// Returns the number of performance cores that are available for AV1 decoding. +// This is a heuristic that works on most common android devices. Returns 0 on +// error or if the number of performance cores cannot be determined. +int GetNumberOfPerformanceCoresOnline(); + +} // namespace gav1_jni + +#endif // EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ diff --git a/extensions/av1/src/main/jni/gav1_jni.cc b/extensions/av1/src/main/jni/gav1_jni.cc index e0cef86d22..714ab499b1 100644 --- a/extensions/av1/src/main/jni/gav1_jni.cc +++ b/extensions/av1/src/main/jni/gav1_jni.cc @@ -32,6 +32,7 @@ #include // NOLINT #include +#include "cpu_info.h" // NOLINT #include "gav1/decoder.h" #define LOG_TAG "gav1_jni" @@ -774,5 +775,9 @@ DECODER_FUNC(jint, gav1CheckError, jlong jContext) { return kStatusOk; } +DECODER_FUNC(jint, gav1GetThreads) { + return gav1_jni::GetNumberOfPerformanceCoresOnline(); +} + // TODO(b/139902005): Add functions for getting libgav1 version and build // configuration once libgav1 ABI provides this information.