From cb527c24d8b788761fcdf00977fe95ccbaffcb68 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Thu, 29 Nov 2018 17:15:59 +0000 Subject: [PATCH] Add LoadStabilizer class --- CMakeLists.txt | 2 + include/oboe/Oboe.h | 1 + include/oboe/StabilizedCallback.h | 70 ++++++++++++ samples/MegaDrone/README.md | 2 + .../MegaDrone/src/main/cpp/AudioEngine.cpp | 4 +- samples/MegaDrone/src/main/cpp/AudioEngine.h | 1 + src/common/StabilizedCallback.cpp | 108 ++++++++++++++++++ src/common/Trace.cpp | 75 ++++++++++++ src/common/Trace.h | 31 +++++ 9 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 include/oboe/StabilizedCallback.h create mode 100644 src/common/StabilizedCallback.cpp create mode 100644 src/common/Trace.cpp create mode 100644 src/common/Trace.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f65c2a26..533ada111 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ set (oboe_sources src/opensles/EngineOpenSLES.cpp src/opensles/OpenSLESUtilities.cpp src/opensles/OutputMixerOpenSLES.cpp + src/common/StabilizedCallback.cpp + src/common/Trace.cpp ) add_library(oboe STATIC ${oboe_sources}) diff --git a/include/oboe/Oboe.h b/include/oboe/Oboe.h index e59fa8925..c3a0bcabd 100644 --- a/include/oboe/Oboe.h +++ b/include/oboe/Oboe.h @@ -32,5 +32,6 @@ #include "oboe/AudioStreamBuilder.h" #include "oboe/Utilities.h" #include "oboe/Version.h" +#include "oboe/StabilizedCallback.h" #endif //OBOE_OBOE_H diff --git a/include/oboe/StabilizedCallback.h b/include/oboe/StabilizedCallback.h new file mode 100644 index 000000000..b31bb11ed --- /dev/null +++ b/include/oboe/StabilizedCallback.h @@ -0,0 +1,70 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STABILIZEDCALLBACK_H +#define OBOE_STABILIZEDCALLBACK_H + +#include +#include "oboe/AudioStream.h" + +namespace oboe { + +class StabilizedCallback : public AudioStreamCallback { + +public: + StabilizedCallback(AudioStreamCallback *callback); + + DataCallbackResult + onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override; + + void onErrorBeforeClose(AudioStream *oboeStream, Result error) override { + return mCallback->onErrorBeforeClose(oboeStream, error); + } + + void onErrorAfterClose(AudioStream *oboeStream, Result error) override { + return mCallback->onErrorAfterClose(oboeStream, error); + } + +private: + + AudioStreamCallback *mCallback = nullptr; + int64_t mFrameCount = 0; + int64_t mEpochTimeNanos = 0; + double mOpsPerNano = 1; + + void generateLoad(int64_t durationNanos); +}; + +/** + * cpu_relax is an architecture specific method of telling the CPU that you don't want it to + * do much work. asm volatile keeps the compiler from optimising these instructions out. + */ +#if defined(__i386__) || defined(__x86_64__) +#define cpu_relax() asm volatile("rep; nop" ::: "memory"); + +#elif defined(__arm__) || defined(__mips__) + #define cpu_relax() asm volatile("":::"memory") + +#elif defined(__aarch64__) +#define cpu_relax() asm volatile("yield" ::: "memory") + +#else +#error "cpu_relax is not defined for this architecture" +#endif + +} + +#endif //OBOE_STABILIZEDCALLBACK_H diff --git a/samples/MegaDrone/README.md b/samples/MegaDrone/README.md index ae727684e..5865fdbff 100644 --- a/samples/MegaDrone/README.md +++ b/samples/MegaDrone/README.md @@ -12,6 +12,8 @@ This sample demonstrates how to obtain the lowest latency and optimal computatio 4) Setting the buffer size to 2 bursts 5) Using the `-Ofast` compiler optimization flag, even when building the `Debug` variant 6) Using [`getExclusiveCores`](https://developer.android.com/reference/android/os/Process#getExclusiveCores()) (API 24+) and thread affinity to bind the audio thread to the best available CPU core(s) +7) Using a `StabilizedCallback` which aims to spend a fixed percentage of the callback time to avoid CPU frequency scaling ([video explanation](https://www.youtube.com/watch?v=C0BPXZIvG-Q&feature=youtu.be&t=1158)) + This code was presented at [AES Milan](http://www.aes.org/events/144/) and [Droidcon Berlin](https://www.de.droidcon.com/) as part of a talk on Oboe. diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp index 0471a1208..2a19fdee3 100644 --- a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp +++ b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp @@ -25,7 +25,9 @@ void AudioEngine::start(std::vector cpuIds) { mCpuIds = cpuIds; AudioStreamBuilder builder; - builder.setCallback(this); + + mStabilizedCallback = new StabilizedCallback(this); + builder.setCallback(mStabilizedCallback); builder.setPerformanceMode(PerformanceMode::LowLatency); builder.setSharingMode(SharingMode::Exclusive); diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.h b/samples/MegaDrone/src/main/cpp/AudioEngine.h index e6e2cb839..98aa0c9f5 100644 --- a/samples/MegaDrone/src/main/cpp/AudioEngine.h +++ b/samples/MegaDrone/src/main/cpp/AudioEngine.h @@ -38,6 +38,7 @@ class AudioEngine : public AudioStreamCallback { private: + StabilizedCallback *mStabilizedCallback = nullptr; AudioStream *mStream = nullptr; std::unique_ptr mSynth; std::vector mCpuIds; // IDs of CPU cores which the audio callback should be bound to diff --git a/src/common/StabilizedCallback.cpp b/src/common/StabilizedCallback.cpp new file mode 100644 index 000000000..855d2924b --- /dev/null +++ b/src/common/StabilizedCallback.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe/StabilizedCallback.h" +#include "common/AudioClock.h" +#include "common/Trace.h" + +constexpr int32_t kLoadGenerationStepSizeNanos = 1000; +constexpr float kPercentageOfCallbackToUse = 0.8; + +using namespace oboe; + +StabilizedCallback::StabilizedCallback(AudioStreamCallback *callback) : mCallback(callback){ + Trace::initialize(); +} + +/** + * An audio callback which attempts to do work for a fixed amount of time. + * + * @param oboeStream + * @param audioData + * @param numFrames + * @return + */ +DataCallbackResult +StabilizedCallback::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) { + + int64_t startTimeNanos = AudioClock::getNanoseconds(); + + if (mFrameCount == 0){ + mEpochTimeNanos = startTimeNanos; + } + + int64_t durationSinceEpochNanos = startTimeNanos - mEpochTimeNanos; + + // In an ideal world the callback start time will be exactly the same as the duration of the + // frames already read/written into the stream. In reality the callback can start early + // or late. By finding the delta we can calculate the target duration for our stabilized + // callback. + int64_t idealStartTimeNanos = (mFrameCount * kNanosPerSecond) / oboeStream->getSampleRate(); + int64_t lateStartNanos = durationSinceEpochNanos - idealStartTimeNanos; + + if (lateStartNanos < 0){ + // This was an early start which indicates that our previous epoch was a late callback. + // Update our epoch to this more accurate time. + mEpochTimeNanos = startTimeNanos; + mFrameCount = 0; + } + + int64_t numFramesAsNanos = (numFrames * kNanosPerSecond) / oboeStream->getSampleRate(); + int64_t targetDurationNanos = (int64_t) + (numFramesAsNanos * kPercentageOfCallbackToUse) - lateStartNanos; + + Trace::beginSection("Actual load"); + DataCallbackResult result = mCallback->onAudioReady(oboeStream, audioData, numFrames); + Trace::endSection(); + + int64_t executionDurationNanos = AudioClock::getNanoseconds() - startTimeNanos; + int64_t stabilizingLoadDurationNanos = targetDurationNanos - executionDurationNanos; + + Trace::beginSection("Stabilized load for %lldns", stabilizingLoadDurationNanos); + generateLoad(stabilizingLoadDurationNanos); + Trace::endSection(); + + // TODO: Could this be done with oboeStream->getFramesRead or getFramesWritten()? + // Wraparound: At 48000 frames per second mFrameCount wraparound will occur after 6m years, + // significantly longer than the average lifetime of an Android phone. + mFrameCount += numFrames; + return result; +} + +void StabilizedCallback::generateLoad(int64_t durationNanos) { + + int64_t currentTimeNanos = AudioClock::getNanoseconds(); + int64_t deadlineTimeNanos = currentTimeNanos + durationNanos; + + // opsPerStep gives us an estimated number of operations which need to be run to fully utilize + // the CPU for a fixed amount of time (specified by kLoadGenerationStepSizeNanos). + // After each step the opsPerStep value is re-calculated based on the actual time taken to + // execute those operations. + int opsPerStep = (int)(mOpsPerNano * kLoadGenerationStepSizeNanos); + int64_t stepDurationNanos = 0; + int64_t previousTimeNanos = 0; + + while (currentTimeNanos <= deadlineTimeNanos){ + + for (int i = 0; i < opsPerStep; i++) cpu_relax(); + + previousTimeNanos = currentTimeNanos; + currentTimeNanos = AudioClock::getNanoseconds(); + stepDurationNanos = currentTimeNanos - previousTimeNanos; + mOpsPerNano = (int)(opsPerStep / stepDurationNanos); + opsPerStep = (int)(mOpsPerNano * kLoadGenerationStepSizeNanos); + } +} \ No newline at end of file diff --git a/src/common/Trace.cpp b/src/common/Trace.cpp new file mode 100644 index 000000000..5ed445b5d --- /dev/null +++ b/src/common/Trace.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "Trace.h" +#include "OboeDebug.h" + +static char buffer[256]; + +// Tracing functions +static void *(*ATrace_beginSection)(const char *sectionName); + +static void *(*ATrace_endSection)(); + +typedef void *(*fp_ATrace_beginSection)(const char *sectionName); + +typedef void *(*fp_ATrace_endSection)(); + +bool Trace::mIsTracingSupported = false; + +void Trace::beginSection(const char *format, ...){ + + if (mIsTracingSupported) { + va_list va; + va_start(va, format); + vsprintf(buffer, format, va); + ATrace_beginSection(buffer); + va_end(va); + } else { + LOGE("Tracing is either not initialized (call Trace::initialize()) " + "or not supported on this device"); + } +} + +void Trace::endSection() { + + if (mIsTracingSupported) { + ATrace_endSection(); + } +} + +void Trace::initialize() { + + // Using dlsym allows us to use tracing on API 21+ without needing android/trace.h which wasn't + // published until API 23 + void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); + if (lib == nullptr) { + LOGE("Could not open libandroid.so to dynamically load tracing symbols"); + } else { + ATrace_beginSection = + reinterpret_cast( + dlsym(lib, "ATrace_beginSection")); + ATrace_endSection = + reinterpret_cast( + dlsym(lib, "ATrace_endSection")); + + if (ATrace_beginSection != nullptr && ATrace_endSection != nullptr){ + mIsTracingSupported = true; + } + } +} \ No newline at end of file diff --git a/src/common/Trace.h b/src/common/Trace.h new file mode 100644 index 000000000..c7965f956 --- /dev/null +++ b/src/common/Trace.h @@ -0,0 +1,31 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_TRACE_H +#define OBOE_TRACE_H + +class Trace { + +public: + static void beginSection(const char *format, ...); + static void endSection(); + static void initialize(); + +private: + static bool mIsTracingSupported; +}; + +#endif //OBOE_TRACE_H \ No newline at end of file