Skip to content

Commit

Permalink
OboeTester: add workload fader, remove benchmark (#1938)
Browse files Browse the repository at this point in the history
This will make it easier to dial in a specific workload
and also is more robust.
  • Loading branch information
philburk authored Nov 22, 2023
1 parent f6aa353 commit 0f94598
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,23 @@
/**
* Demonstrate the behavior of a changing CPU load on underruns.
* Display the workload and the callback duration in a chart.
* Enable PerformanceHints (ADPF).
* Enable or disable PerformanceHints (ADPF) using a checkbox.
* This might boost the CPU frequency when Oboe is taking too long to compute the next buffer.
* ADPF docs at: https://developer.android.com/reference/android/os/PerformanceHintManager
*/
public class DynamicWorkloadActivity extends TestOutputActivityBase {
private static final double WORKLOAD_MAX = 500.0;
private static final int WORKLOAD_HIGH_MIN = 30;
private static final int WORKLOAD_HIGH_MAX = 150;
// When the CPU is completely saturated then the load will be above 1.0.
public static final double LOAD_RECOVERY_HIGH = 1.0;
// Use a slightly lower value for going low so that the comparator has hysteresis.
public static final double LOAD_RECOVERY_LOW = 0.95;

private static final float MARGIN_ABOVE_WORKLOAD_FOR_CPU = 1.2f;

// By default, set high workload to 70 voices, which is reasonable for most devices.
public static final double WORKLOAD_PROGRESS_FOR_70_VOICES = 0.53;

private Button mStopButton;
private Button mStartButton;
private TextView mResultView;
Expand All @@ -57,34 +67,25 @@ public class DynamicWorkloadActivity extends TestOutputActivityBase {
private CheckBox mDrawAlwaysBox;
private int mCpuCount;

private static final int WORKLOAD_LOW = 1;
private int mWorkloadHigh; // this will get set later
private WorkloadView mDynamicWorkloadView;

// Periodically query the status of the streams.
protected class WorkloadUpdateThread {
public static final int SNIFFER_UPDATE_PERIOD_MSEC = 40;
public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
public static final int SNIFFER_TOGGLE_PERIOD_MSEC = 3000;
public static final int REQUIRED_STABLE_MEASUREMENTS = 20;

private static final double WORKLOAD_FILTER_COEFFICIENT = 0.9;
private static final int STATE_IDLE = 0;
private static final int STATE_BENCHMARK_TARGET = 1;
private static final int STATE_RUN_LOW = 2;
private static final int STATE_RUN_HIGH = 3;
private static final int STATE_RUN_LOW = 1;
private static final int STATE_RUN_HIGH = 2;

private Handler mHandler;
private int mCount;

private double mCpuLoadBenchmark = 0.90; // Determine workload that will hit this CPU load.
private double mCpuLoadHigh = 0.80; // Target CPU load during HIGH cycle.

private double mWorkloadLow = 0.0;
private double mWorkloadHigh = 0.0;
private double mWorkloadCurrent = 1.0;
private double mWorkloadBenchmark = 0.0;
private int mWorkloadCurrent = 1;

private int mState = STATE_IDLE;
private long mLastToggleTime = 0;
private int mStableCount = 0;
private boolean mArmLoadMonitor = false;
private long mRecoveryTimeBegin;
private long mRecoveryTimeEnd;
private long mStartTimeNanos;
Expand All @@ -93,8 +94,6 @@ String stateToString(int state) {
switch(state) {
case STATE_IDLE:
return "Idle";
case STATE_BENCHMARK_TARGET:
return "Benchmarking";
case STATE_RUN_LOW:
return "low";
case STATE_RUN_HIGH:
Expand All @@ -108,7 +107,7 @@ String stateToString(int state) {
private Runnable runnableCode = new Runnable() {
@Override
public void run() {
double nextWorkload = 0.0;
int nextWorkload = mWorkloadCurrent;
AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
float cpuLoad = stream.getCpuLoad();
float maxCpuLoad = stream.getAndResetMaxCpuLoad();
Expand All @@ -119,29 +118,11 @@ public void run() {
switch (mState) {
case STATE_IDLE:
drawChartOnce = true; // clear old chart
mState = STATE_BENCHMARK_TARGET;
break;
case STATE_BENCHMARK_TARGET:
// prevent divide by zero
double targetWorkload = (mWorkloadCurrent / Math.max(cpuLoad, 0.01)) * mCpuLoadBenchmark;
targetWorkload = Math.min(WORKLOAD_MAX, targetWorkload);
// low pass filter to find matching workload
nextWorkload = (WORKLOAD_FILTER_COEFFICIENT * mWorkloadCurrent)
+ ((1.0 - WORKLOAD_FILTER_COEFFICIENT) * targetWorkload);
if (Math.abs(cpuLoad - mCpuLoadBenchmark) < 0.04) {
if (++mStableCount > REQUIRED_STABLE_MEASUREMENTS) {
// Found the right workload.
mWorkloadBenchmark = nextWorkload;
mLastToggleTime = now;
mState = STATE_RUN_LOW;
mWorkloadLow = Math.max(1, (int)(nextWorkload * 0.02));
mWorkloadHigh = (int)(nextWorkload * (mCpuLoadHigh / mCpuLoadBenchmark));
mWorkloadTrace.setMax((float)(2.0 * nextWorkload));
}
}
mState = STATE_RUN_LOW;
mLastToggleTime = now;
break;
case STATE_RUN_LOW:
nextWorkload = mWorkloadLow;
nextWorkload = WORKLOAD_LOW;
if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) {
mLastToggleTime = now;
mState = STATE_RUN_HIGH;
Expand Down Expand Up @@ -171,6 +152,8 @@ public void run() {
}
break;
}
stream.setWorkload((int) nextWorkload);
mWorkloadCurrent = nextWorkload;
// Update chart
float nowMicros = (System.nanoTime() - mStartTimeNanos) * 0.001f;
mMultiLineChart.addX(nowMicros);
Expand All @@ -184,15 +167,12 @@ public void run() {
String recoveryTimeString = (mRecoveryTimeEnd <= mRecoveryTimeBegin) ?
"---" : ((mRecoveryTimeEnd - mRecoveryTimeBegin) + " msec");
String message =
"#Voices: max = " + String.format(Locale.getDefault(), "%d", (int) mWorkloadBenchmark)
+ ", now = " + String.format(Locale.getDefault(), "%d", (int) nextWorkload)
"#Voices = " + (int) nextWorkload
+ "\nWorkState = " + stateToString(mState)
+ "\nCPU = " + String.format(Locale.getDefault(), "%6.3f%c", cpuLoad * 100, '%')
+ "\ncores = " + cpuMaskToString(cpuMask, mCpuCount)
+ "\nRecovery = " + recoveryTimeString;
postResult(message);
stream.setWorkload((int)(nextWorkload));
mWorkloadCurrent = nextWorkload;

mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
}
Expand All @@ -202,9 +182,7 @@ private void start() {
stop();
mStartTimeNanos = System.nanoTime();
mMultiLineChart.reset();
mCount = 0;
mStableCount = 0;
mState = STATE_BENCHMARK_TARGET;
mState = STATE_IDLE;
mHandler = new Handler(Looper.getMainLooper());
// Start the initial runnable task by posting through the handler
mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
Expand All @@ -218,6 +196,10 @@ private void stop() {

}

private void setWorkloadHigh(int workloadHigh) {
mWorkloadHigh = workloadHigh;
}


/**
* This text will look best in a monospace font.
Expand Down Expand Up @@ -259,9 +241,12 @@ protected void onCreate(Bundle savedInstanceState) {
mStartButton = (Button) findViewById(R.id.button_start);
mStopButton = (Button) findViewById(R.id.button_stop);

mDynamicWorkloadView = (WorkloadView) findViewById(R.id.dynamic_workload_view);
mWorkloadView.setVisibility(View.GONE);

// Add a row of checkboxes for setting CPU affinity.
mCpuCount = NativeEngine.getCpuCount();
final int defaultCpuAffinity = 2;
final int defaultCpuAffinityMask = 0;
View.OnClickListener checkBoxListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
Expand All @@ -283,25 +268,26 @@ public void onClick(View view) {
mAffinityBoxes.add(checkBox);
checkBox.setText(cpuIndex + "");
checkBox.setOnClickListener(checkBoxListener);
if (cpuIndex == defaultCpuAffinity) {
if (((1 << cpuIndex) & defaultCpuAffinityMask) != 0) {
checkBox.setChecked(true);
}
}
NativeEngine.setCpuAffinityMask(1 << defaultCpuAffinity);
NativeEngine.setCpuAffinityMask(defaultCpuAffinityMask);

mMultiLineChart = (MultiLineChart) findViewById(R.id.multiline_chart);
mMaxCpuLoadTrace = mMultiLineChart.createTrace("CPU", Color.RED,
0.0f, 2.0f);
mWorkloadTrace = mMultiLineChart.createTrace("Work", Color.BLUE,
0.0f, (float)WORKLOAD_MAX);
0.0f, (MARGIN_ABOVE_WORKLOAD_FOR_CPU * WORKLOAD_HIGH_MAX));

// TODO remove when finished with ADPF experiments.
mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf);
mPerfHintBox = (CheckBox) findViewById(R.id.enable_perf_hint);

// TODO remove when finished with ADPF experiments.
mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf);
mUseAltAdpfBox.setOnClickListener(buttonView -> {
CheckBox checkBox = (CheckBox) buttonView;
setUseAlternativeAdpf(checkBox.isChecked());
mPerfHintBox.setEnabled(!checkBox.isChecked());
});
mUseAltAdpfBox.setVisibility(View.GONE);

Expand All @@ -323,6 +309,16 @@ public void onClick(View view) {
mDrawChartAlways = checkBox.isChecked();
});

if (mDynamicWorkloadView != null) {
mDynamicWorkloadView.setWorkloadReceiver((w) -> {
setWorkloadHigh(w);
});

mDynamicWorkloadView.setLabel("High Workload");
mDynamicWorkloadView.setRange(WORKLOAD_HIGH_MIN, WORKLOAD_HIGH_MAX);
mDynamicWorkloadView.setFaderNormalizedProgress(WORKLOAD_PROGRESS_FOR_70_VOICES);
}

updateButtons(false);
updateEnabledWidgets();
hideSettingsViews(); // make more room
Expand Down Expand Up @@ -356,9 +352,6 @@ int getActivityType() {
}

public void startTest(View view) {
// Do not draw until the benchmark stage has finished.
mDrawAlwaysBox.setChecked(false);
mDrawChartAlways = false;
try {
openAudio();
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ protected void onCreate(Bundle savedInstanceState) {

mWorkloadView = (WorkloadView) findViewById(R.id.workload_view);
if (mWorkloadView != null) {
mWorkloadView.setAudioStreamTester(mAudioInputTester);
mWorkloadView.setWorkloadReceiver((w) -> mAudioInputTester.setWorkload(w));
}

mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ abstract class TestOutputActivityBase extends TestAudioActivity {
AudioOutputTester mAudioOutTester;

private BufferSizeView mBufferSizeView;
private WorkloadView mWorkloadView;
protected WorkloadView mWorkloadView;

@Override boolean isOutput() { return true; }

Expand All @@ -40,13 +40,14 @@ protected void findAudioCommon() {
super.findAudioCommon();
mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view);
mWorkloadView = (WorkloadView) findViewById(R.id.workload_view);
if (mWorkloadView != null) {
mWorkloadView.setWorkloadReceiver((w) -> mAudioOutTester.setWorkload(w));
}
}

@Override
public AudioOutputTester addAudioOutputTester() {
AudioOutputTester audioOutTester = super.addAudioOutputTester();
mWorkloadView.setAudioStreamTester(audioOutTester);
return audioOutTester;
return super.addAudioOutputTester();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.SeekBar;
Expand All @@ -27,13 +28,19 @@

public class WorkloadView extends LinearLayout {

private AudioStreamTester mAudioStreamTester;

protected static final int FADER_PROGRESS_MAX = 1000; // must match layout
protected TextView mTextView;
protected SeekBar mSeekBar;

private String mLabel = "Workload";
protected ExponentialTaper mExponentialTaper;

public interface WorkloadReceiver {
void setWorkload(int workload);
}

WorkloadReceiver mWorkloadReceiver;

private SeekBar.OnSeekBarChangeListener mChangeListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
Expand Down Expand Up @@ -66,12 +73,8 @@ public WorkloadView(Context context,
initializeViews(context);
}

public AudioStreamTester getAudioStreamTester() {
return mAudioStreamTester;
}

public void setAudioStreamTester(AudioStreamTester audioStreamTester) {
mAudioStreamTester = audioStreamTester;
public void setWorkloadReceiver(WorkloadReceiver workloadReceiver) {
mWorkloadReceiver = workloadReceiver;
}

void setFaderNormalizedProgress(double fraction) {
Expand All @@ -92,20 +95,34 @@ private void initializeViews(Context context) {
mTextView = (TextView) findViewById(R.id.textWorkload);
mSeekBar = (SeekBar) findViewById(R.id.faderWorkload);
mSeekBar.setOnSeekBarChangeListener(mChangeListener);
mExponentialTaper = new ExponentialTaper(0.0, 100.0, 10.0);
setRange(0.0, 100.0);
//mSeekBar.setProgress(0);
}

void setRange(double dMin, double dMax) {
mExponentialTaper = new ExponentialTaper(dMin, dMax, 10.0);
}

private void setValueByPosition(int progress) {
int workload = (int) mExponentialTaper.linearToExponential(
((double)progress) / FADER_PROGRESS_MAX);
mAudioStreamTester.setWorkload(workload);
mTextView.setText("Workload = " + String.format(Locale.getDefault(), "%3d", workload));
if (mWorkloadReceiver != null) {
mWorkloadReceiver.setWorkload(workload);
}
mTextView.setText(getLabel() + " = " + String.format(Locale.getDefault(), "%3d", workload));
}

@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mSeekBar.setEnabled(enabled);
}

public String getLabel() {
return mLabel;
}

public void setLabel(String label) {
this.mLabel = label;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8sp"
android:text="Perf Hint" />
android:text="ADPF" />

<CheckBox
android:id="@+id/use_alternative_adpf"
Expand Down Expand Up @@ -90,6 +90,13 @@

</HorizontalScrollView>

<com.mobileer.oboetester.WorkloadView
android:id="@+id/dynamic_workload_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" />

<TextView
android:id="@+id/resultView"
android:layout_width="match_parent"
Expand Down

0 comments on commit 0f94598

Please sign in to comment.