Skip to content

Commit

Permalink
Add setPerformanceHintEnabled(), Dynamic CPU Load test (google#1788)
Browse files Browse the repository at this point in the history
Add ADPF support to Oboe

OboeTester: test dynamically changing load
Add an Activity that varies the workload.
Also add CPU affinity UI.
Also try to measure the time it takes for the CPU frequency to recover.
Add Synth from SynthMark for more realistic workload.
Use dummy workload when not stereo.
Make workload an integer (#voices).
  • Loading branch information
philburk authored May 21, 2023
1 parent 40e9fed commit ad6cca7
Show file tree
Hide file tree
Showing 50 changed files with 2,912 additions and 113 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ project(oboe)
set (oboe_sources
src/aaudio/AAudioLoader.cpp
src/aaudio/AudioStreamAAudio.cpp
src/common/AdpfWrapper.cpp
src/common/AudioSourceCaller.cpp
src/common/AudioStream.cpp
src/common/AudioStreamBuilder.cpp
Expand Down
5 changes: 5 additions & 0 deletions apps/OboeTester/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@
android:label="@string/title_route_during_callback"
android:exported="true" />

<activity
android:name=".DynamicWorkloadActivity"
android:label="@string/title_dynamic_load"
android:exported="true" />

<service
android:name=".MidiTapTester"
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"
Expand Down
21 changes: 16 additions & 5 deletions apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,26 @@ class ActivityContext {
return stopAllStreams();
}

double getCpuLoad() {
float getCpuLoad() {
return oboeCallbackProxy.getCpuLoad();
}

float getAndResetMaxCpuLoad() {
return oboeCallbackProxy.getAndResetMaxCpuLoad();
}

std::string getCallbackTimeString() {
return oboeCallbackProxy.getCallbackTimeString();
}

void setWorkload(double workload) {
void setWorkload(int32_t workload) {
oboeCallbackProxy.setWorkload(workload);
}

void setHearWorkload(bool enabled) {
oboeCallbackProxy.setHearWorkload(enabled);
}

virtual oboe::Result startPlayback() {
return oboe::Result::OK;
}
Expand Down Expand Up @@ -281,6 +289,10 @@ class ActivityContext {

double getTimestampLatency(int32_t streamIndex);

void setCpuAffinityMask(uint32_t mask) {
oboeCallbackProxy.setCpuAffinityMask(mask);
}

protected:
std::shared_ptr<oboe::AudioStream> getInputStream();
std::shared_ptr<oboe::AudioStream> getOutputStream();
Expand Down Expand Up @@ -709,7 +721,8 @@ class ActivityTestDisconnect : public ActivityContext {
};

/**
* Switch between various
* Global context for native tests.
* Switch between various ActivityContexts.
*/
class NativeAudioContext {
public:
Expand Down Expand Up @@ -767,7 +780,6 @@ class NativeAudioContext {
ActivityDataPath mActivityDataPath;
ActivityTestDisconnect mActivityTestDisconnect;


private:

// WARNING - must match definitions in TestAudioActivity.java
Expand All @@ -786,7 +798,6 @@ class NativeAudioContext {

ActivityType mActivityType = ActivityType::Undefined;
ActivityContext *currentActivity = &mActivityTestOutput;

};

#endif //NATIVEOBOE_NATIVEAUDIOCONTEXT_H
56 changes: 24 additions & 32 deletions apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,7 @@
#include "common/OboeDebug.h"
#include "OboeStreamCallbackProxy.h"

// Linear congruential random number generator.
static uint32_t s_random16() {
static uint32_t seed = 1234;
seed = ((seed * 31421) + 6927) & 0x0FFFF;
return seed;
}

/**
* The random number generator is good for burning CPU because the compiler cannot
* easily optimize away the computation.
* @param workload number of times to execute the loop
* @return a white noise value between -1.0 and +1.0
*/
static float s_burnCPU(int32_t workload) {
uint32_t random = 0;
for (int32_t i = 0; i < workload; i++) {
for (int32_t j = 0; j < 10; j++) {
random = random ^ s_random16();
}
}
return (random - 32768) * (1.0 / 32768);
}
#include "synth/IncludeMeOnce.h"

bool OboeStreamCallbackProxy::mCallbackReturnStop = false;

Expand All @@ -58,31 +37,44 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Stop;
int64_t startTimeNanos = getNanoseconds();

if (mCpuAffinityMask != mPreviousMask) {
uint32_t mask = mCpuAffinityMask;
applyCpuAffinityMask(mask);
mPreviousMask = mask;
}

mCallbackCount++;
mFramesPerCallback = numFrames;

if (mCallbackReturnStop) {
return oboe::DataCallbackResult::Stop;
}

s_burnCPU((int32_t)(mWorkload * kWorkloadScaler * numFrames));

if (mCallback != nullptr) {
callbackResult = mCallback->onAudioReady(audioStream, audioData, numFrames);
}

// Update CPU load
double calculationTime = (double)(getNanoseconds() - startTimeNanos);
double inverseRealTime = audioStream->getSampleRate() / (1.0e9 * numFrames);
double currentCpuLoad = calculationTime * inverseRealTime; // avoid a divide
mCpuLoad = (mCpuLoad * 0.95) + (currentCpuLoad * 0.05); // simple low pass filter
mSynthWorkload.onCallback(mNumWorkloadVoices);
if (mNumWorkloadVoices > 0) {
float *buffer = (audioStream->getChannelCount() == 2 && mHearWorkload)
? static_cast<float *>(audioData) : nullptr;
mSynthWorkload.renderStereo(buffer, numFrames);
}

int64_t currentTimeNs = getNanoseconds();
int64_t currentTimeNanos = getNanoseconds();
// Sometimes we get a short callback when doing sample rate conversion.
// Just ignore those to avoid noise.
if (numFrames > (getFramesPerCallback() / 2)) {
int64_t calculationTime = currentTimeNanos - startTimeNanos;
float currentCpuLoad = calculationTime * 0.000000001f * audioStream->getSampleRate() / numFrames;
mCpuLoad = (mCpuLoad * 0.95f) + (currentCpuLoad * 0.05f); // simple low pass filter
mMaxCpuLoad = std::max(currentCpuLoad, mMaxCpuLoad.load());
}

if (mPreviousCallbackTimeNs != 0) {
mStatistics.add((currentTimeNs - mPreviousCallbackTimeNs) * kNsToMsScaler);
mStatistics.add((currentTimeNanos - mPreviousCallbackTimeNs) * kNsToMsScaler);
}
mPreviousCallbackTimeNs = currentTimeNs;
mPreviousCallbackTimeNs = currentTimeNanos;

return callbackResult;
}
142 changes: 132 additions & 10 deletions apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@

#include <unistd.h>
#include <sys/types.h>
#include <sys/sysinfo.h>

#include "oboe/Oboe.h"
#include "synth/Synthesizer.h"
#include "synth/SynthTools.h"

class DoubleStatistics {
public:
Expand Down Expand Up @@ -65,12 +68,79 @@ class DoubleStatistics {
std::atomic<double> maximum { 0 };
};

/**
* Manage the synthesizer workload that burdens the CPU.
* Adjust the number of voices according to the requested workload.
* Trigger noteOn and noteOff messages.
*/
class SynthWorkload {
public:
SynthWorkload() {
mSynth.setup(marksynth::kSynthmarkSampleRate, marksynth::kSynthmarkMaxVoices);
}

void onCallback(double workload) {
// If workload changes then restart notes.
if (workload != mPreviousWorkload) {
mSynth.allNotesOff();
mAreNotesOn = false;
mCountdown = 0; // trigger notes on
mPreviousWorkload = workload;
}
if (mCountdown <= 0) {
if (mAreNotesOn) {
mSynth.allNotesOff();
mAreNotesOn = false;
mCountdown = mOffFrames;
} else {
mSynth.notesOn((int)mPreviousWorkload);
mAreNotesOn = true;
mCountdown = mOnFrames;
}
}
}

/**
* Render the notes into a stereo buffer.
* Passing a nullptr will cause the calculated results to be discarded.
* The workload should be the same.
* @param buffer a real stereo buffer or nullptr
* @param numFrames
*/
void renderStereo(float *buffer, int numFrames) {
if (buffer == nullptr) {
int framesLeft = numFrames;
while (framesLeft > 0) {
int framesThisTime = std::min(kDummyBufferSizeInFrames, framesLeft);
// Do the work then throw it away.
mSynth.renderStereo(&mDummyStereoBuffer[0], framesThisTime);
framesLeft -= framesThisTime;
}
} else {
mSynth.renderStereo(buffer, numFrames);
}
mCountdown -= numFrames;
}

private:
marksynth::Synthesizer mSynth;
static constexpr int kDummyBufferSizeInFrames = 32;
float mDummyStereoBuffer[kDummyBufferSizeInFrames * 2];
double mPreviousWorkload = 1.0;
bool mAreNotesOn = false;
int mCountdown = 0;
int mOnFrames = (int) (0.2 * 48000);
int mOffFrames = (int) (0.3 * 48000);
};

class OboeStreamCallbackProxy : public oboe::AudioStreamCallback {
public:

void setCallback(oboe::AudioStreamCallback *callback) {
mCallback = callback;
setCallbackCount(0);
mStatistics.clear();
mPreviousMask = 0;
}

static void setCallbackReturnStop(bool b) {
Expand Down Expand Up @@ -100,39 +170,91 @@ class OboeStreamCallbackProxy : public oboe::AudioStreamCallback {
/**
* Specify the amount of artificial workload that will waste CPU cycles
* and increase the CPU load.
* @param workload typically ranges from 0.0 to 100.0
* @param workload typically ranges from 0 to 400
*/
void setWorkload(double workload) {
mWorkload = std::max(0.0, workload);
void setWorkload(int32_t workload) {
mNumWorkloadVoices = std::max(0, workload);
}

double getWorkload() const {
return mWorkload;
int32_t getWorkload() const {
return mNumWorkloadVoices;
}

double getCpuLoad() const {
void setHearWorkload(bool enabled) {
mHearWorkload = enabled;
}

/**
* This is the callback duration relative to the real-time equivalent.
* So it may be higher than 1.0.
* @return low pass filtered value for the fractional CPU load
*/
float getCpuLoad() const {
return mCpuLoad;
}

/**
* Calling this will atomically reset the max to zero so only call
* this from one client.
*
* @return last value of the maximum unfiltered CPU load.
*/
float getAndResetMaxCpuLoad() {
return mMaxCpuLoad.exchange(0.0f);
}

std::string getCallbackTimeString() const {
return mStatistics.dump();
}

static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC);

/**
* @param cpuIndex
* @return 0 on success or a negative errno
*/
int setCpuAffinity(int cpuIndex) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(cpuIndex, &cpu_set);
int err = sched_setaffinity((pid_t) 0, sizeof(cpu_set_t), &cpu_set);
return err == 0 ? 0 : -errno;
}

int applyCpuAffinityMask(uint32_t mask) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
int cpuCount = get_nprocs();
for (int cpuIndex = 0; cpuIndex < cpuCount; cpuIndex++) {
if (mask & (1 << cpuIndex)) {
CPU_SET(cpuIndex, &cpu_set);
}
}
int err = sched_setaffinity((pid_t) 0, sizeof(cpu_set_t), &cpu_set);
return err == 0 ? 0 : -errno;
}

void setCpuAffinityMask(uint32_t mask) {
mCpuAffinityMask = mask;
}

private:
static constexpr int32_t kWorkloadScaler = 500;
static constexpr double kNsToMsScaler = 0.000001;
double mWorkload = 0.0;
std::atomic<double> mCpuLoad{0};
std::atomic<float> mCpuLoad{0.0f};
std::atomic<float > mMaxCpuLoad{0.0f};
int64_t mPreviousCallbackTimeNs = 0;
DoubleStatistics mStatistics;
int32_t mNumWorkloadVoices = 0;
SynthWorkload mSynthWorkload;
bool mHearWorkload = false;

oboe::AudioStreamCallback *mCallback = nullptr;
static bool mCallbackReturnStop;
int64_t mCallbackCount = 0;
std::atomic<int32_t> mFramesPerCallback{0};
};

std::atomic<uint32_t> mCpuAffinityMask{0};
std::atomic<uint32_t> mPreviousMask{0};
};

#endif //NATIVEOBOE_OBOESTREAMCALLBACKPROXY_H
Loading

0 comments on commit ad6cca7

Please sign in to comment.