blob: 2d77204099e9c2037a758df083e0184927f73a27 [file] [log] [blame]
// Copyright 2021 The Fuchsia 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 <lib/syslog/cpp/log_settings.h>
#include <map>
#include <set>
#include "gperftools/profiler.h"
#include "src/lib/fxl/command_line.h"
#include "src/lib/fxl/strings/split_string.h"
#include "src/media/audio/audio_core/mixer/tools/audio_performance.h"
using Resampler = ::media::audio::Mixer::Resampler;
using ASF = fuchsia::media::AudioSampleFormat;
using AudioPerformance = media::audio::tools::AudioPerformance;
using GainType = AudioPerformance::GainType;
using OutputRange = AudioPerformance::OutputSourceRange;
using MixerConfig = AudioPerformance::MixerConfig;
using OutputProducerConfig = AudioPerformance::OutputProducerConfig;
namespace {
enum class Benchmark { Create, Mix, Output };
constexpr char kBenchmarkDurationSwitch[] = "bench-time";
constexpr char kBenchmarkRunsSwitch[] = "bench-runs";
constexpr char kProfileMixerCreationSwitch[] = "enable-create";
constexpr char kProfileMixingSwitch[] = "enable-mix";
constexpr char kProfileOutputSwitch[] = "enable-output";
constexpr char kEnablePprofSwitch[] = "enable-pprof";
constexpr char kSamplerSwitch[] = "samplers";
constexpr char kSamplerPointOption[] = "point";
constexpr char kSamplerSincOption[] = "sinc";
constexpr char kChannelsSwitch[] = "channels";
constexpr char kFrameRatesSwitch[] = "frame-rates";
constexpr char kSampleFormatsSwitch[] = "sample-formats";
constexpr char kSampleFormatUint8Option[] = "uint8";
constexpr char kSampleFormatInt16Option[] = "int16";
constexpr char kSampleFormatInt24In32Option[] = "int24";
constexpr char kSampleFormatFloat32Option[] = "float";
constexpr char kMixGainsSwitch[] = "mix-gains";
constexpr char kMixGainMuteOption[] = "mute";
constexpr char kMixGainUnityOption[] = "unity";
constexpr char kMixGainScaledOption[] = "scaled";
constexpr char kMixGainRampedOption[] = "ramped";
constexpr char kMixAccumulateSwitch[] = "accumulate";
constexpr char kOutputProducerSourceRangesSwitch[] = "output-ranges";
constexpr char kOutputProducerSourceRangeSilenceOption[] = "silence";
constexpr char kOutputProducerSourceRangeOutOfRangeOption[] = "out-of-range";
constexpr char kOutputProducerSourceRangeNormalOption[] = "normal";
constexpr char kPerftestJsonFilepathSwitch[] = "perftest-json";
constexpr char kUsageSwitch[] = "help";
constexpr zx::duration kBenchmarkDurationDefault = zx::msec(250);
constexpr size_t kBenchmarkRunsDefault = 1000;
constexpr size_t kBenchmarkMinRuns = 5;
constexpr int32_t kPreferredInputChans = 1;
constexpr int32_t kPreferredOutputChans = 1;
constexpr int32_t kPreferredSourceRate = 48000;
constexpr int32_t kPreferredDestRate = 48000;
constexpr ASF kPreferredSampleFormat = ASF::FLOAT;
constexpr GainType kPreferredGainType = GainType::Unity;
constexpr bool kPreferredAccumSetting = false;
constexpr int32_t kAltPreferredInputChans = 2;
constexpr int32_t kAltPreferredOutputChans = 2;
constexpr int32_t kAltPreferredSourceRate = 44100;
constexpr int32_t kAltPreferredDestRate = 48000;
constexpr ASF kAltPreferredSampleFormat = ASF::SIGNED_16;
constexpr GainType kAltPreferredGainType = GainType::Scaled;
constexpr bool kAltPreferredAccumSetting = true;
constexpr OutputRange kPreferredOutputRange = OutputRange::Normal;
struct Options {
// Duration and iteration limits per config.
AudioPerformance::Limits limits;
std::set<Benchmark> enabled;
bool enable_pprof;
// MixerConfig + OutputProducerConfig.
std::set<ASF> sample_formats;
std::set<std::pair<int32_t, int32_t>> num_input_output_chans;
// MixerConfig.
std::set<Resampler> samplers;
std::set<std::pair<int32_t, int32_t>> source_dest_rates;
std::set<GainType> gain_types;
std::set<bool> accumulates;
// OutputProducerConfig.
std::set<OutputRange> output_ranges;
// JSON filepath to export perftest results.
std::optional<std::string> perftest_json;
// Provide matching source and dest rates if available; else return a default.
const std::pair<int32_t, int32_t> matching_rates() const {
for (auto [src, dest] : source_dest_rates) {
if (src == dest) {
return {src, dest};
}
}
return {kPreferredSourceRate, kPreferredDestRate};
}
};
std::vector<MixerConfig> ConfigsForMixerCreation(const Options& opt) {
if (opt.enabled.count(Benchmark::Create) == 0) {
return {};
}
if (opt.samplers.count(Resampler::WindowedSinc) == 0) {
return {};
}
std::vector<MixerConfig> out;
for (auto [source_rate, dest_rate] : opt.source_dest_rates) {
out.push_back({
.sampler_type = Resampler::WindowedSinc,
.num_input_chans = kPreferredInputChans, // this has no effect on mixer creation time
.num_output_chans = kPreferredOutputChans, // this has no effect on mixer creation time
.source_rate = source_rate,
.dest_rate = dest_rate,
.sample_format = kPreferredSampleFormat, // this has no effect on mixer creation time
});
}
return out;
}
// Create mixer configs that cover every combination of provided Options.
std::vector<MixerConfig> ConfigsForMixer(const Options& opt) {
if (opt.enabled.count(Benchmark::Mix) == 0) {
return {};
}
std::vector<MixerConfig> out;
for (auto sampler : opt.samplers) {
for (auto [source_rate, dest_rate] : opt.source_dest_rates) {
if (sampler == Resampler::SampleAndHold && source_rate != dest_rate) {
continue;
}
for (auto [num_input_chans, num_output_chans] : opt.num_input_output_chans) {
for (auto sample_format : opt.sample_formats) {
for (auto gain_type : opt.gain_types) {
for (auto accumulate : opt.accumulates) {
out.push_back({
.sampler_type = sampler,
.num_input_chans = num_input_chans,
.num_output_chans = num_output_chans,
.source_rate = source_rate,
.dest_rate = dest_rate,
.sample_format = sample_format,
.gain_type = gain_type,
.accumulate = accumulate,
});
}
}
}
}
}
}
return out;
}
// Create mixer configs such that one of each provided Option is included in a config.
std::vector<MixerConfig> ConfigsForMixerReduced(const Options& opt) {
if (opt.enabled.count(Benchmark::Mix) == 0) {
return {};
}
std::vector<MixerConfig> out;
for (auto sampler : opt.samplers) {
// Create base config from which to deviate. Note: point sampler can only accept matching source
// and dest rates, so we accommodate that here.
MixerConfig base_config = {
.sampler_type = sampler,
.num_input_chans = kPreferredInputChans,
.num_output_chans = kPreferredOutputChans,
.source_rate =
sampler == Resampler::SampleAndHold ? opt.matching_rates().first : kPreferredSourceRate,
.dest_rate =
sampler == Resampler::SampleAndHold ? opt.matching_rates().second : kPreferredDestRate,
.sample_format = kPreferredSampleFormat,
.gain_type = kPreferredGainType,
.accumulate = kPreferredAccumSetting,
};
out.push_back(base_config);
MixerConfig alt_base_config = {
.sampler_type = sampler,
.num_input_chans = kAltPreferredInputChans,
.num_output_chans = kAltPreferredOutputChans,
.source_rate = sampler == Resampler::SampleAndHold ? opt.matching_rates().first
: kAltPreferredSourceRate,
.dest_rate = sampler == Resampler::SampleAndHold ? opt.matching_rates().second
: kAltPreferredDestRate,
.sample_format = kAltPreferredSampleFormat,
.gain_type = kAltPreferredGainType,
.accumulate = kAltPreferredAccumSetting,
};
out.push_back(alt_base_config);
for (auto [source_rate, dest_rate] : opt.source_dest_rates) {
if (sampler == Resampler::SampleAndHold && source_rate != dest_rate) {
continue;
}
MixerConfig config = base_config;
config.source_rate = source_rate;
config.dest_rate = dest_rate;
if (config != base_config) {
out.push_back(config);
}
config = alt_base_config;
config.source_rate = source_rate;
config.dest_rate = dest_rate;
if (config != alt_base_config) {
out.push_back(config);
}
}
for (auto [num_input_chans, num_output_chans] : opt.num_input_output_chans) {
MixerConfig config = base_config;
config.num_input_chans = num_input_chans;
config.num_output_chans = num_output_chans;
if (config != base_config) {
out.push_back(config);
}
config = alt_base_config;
config.num_input_chans = num_input_chans;
config.num_output_chans = num_output_chans;
if (config != alt_base_config) {
out.push_back(config);
}
}
for (auto sample_format : opt.sample_formats) {
MixerConfig config = base_config;
config.sample_format = sample_format;
if (config != base_config) {
out.push_back(config);
}
config = alt_base_config;
config.sample_format = sample_format;
if (config != alt_base_config) {
out.push_back(config);
}
}
for (auto gain_type : opt.gain_types) {
MixerConfig config = base_config;
config.gain_type = gain_type;
if (config != base_config) {
out.push_back(config);
}
config = alt_base_config;
config.gain_type = gain_type;
if (config != alt_base_config) {
out.push_back(config);
}
}
for (auto accumulate : opt.accumulates) {
MixerConfig config = base_config;
config.accumulate = accumulate;
if (config != base_config) {
out.push_back(config);
}
config = alt_base_config;
config.accumulate = accumulate;
if (config != alt_base_config) {
out.push_back(config);
}
}
}
return out;
}
std::vector<OutputProducerConfig> ConfigsForOutputProducer(const Options& opt) {
if (opt.enabled.count(Benchmark::Output) == 0) {
return {};
}
std::vector<OutputProducerConfig> out;
for (auto [num_input_chans, num_output_chans] : opt.num_input_output_chans) {
for (auto sample_format : opt.sample_formats) {
for (auto output_range : opt.output_ranges) {
out.push_back({
.sample_format = sample_format,
.output_range = output_range,
.num_chans = num_output_chans,
});
}
}
}
return out;
}
// Create output producer configs such that one of each provided Option is included in a config.
std::vector<OutputProducerConfig> ConfigsForOutputProducerReduced(const Options& opt) {
if (opt.enabled.count(Benchmark::Output) == 0) {
return {};
}
std::vector<OutputProducerConfig> out;
OutputProducerConfig base_config = {
.sample_format = kPreferredSampleFormat,
.output_range = kPreferredOutputRange,
.num_chans = kAltPreferredOutputChans,
};
for (auto [num_input_chans, num_output_chans] : opt.num_input_output_chans) {
OutputProducerConfig config = base_config;
config.num_chans = num_output_chans;
if (config != base_config) {
out.push_back(config);
}
}
for (auto sample_format : opt.sample_formats) {
OutputProducerConfig config = base_config;
config.sample_format = sample_format;
if (config != base_config) {
out.push_back(config);
}
}
for (auto output_range : opt.output_ranges) {
OutputProducerConfig config = base_config;
config.output_range = output_range;
if (config != base_config) {
out.push_back(config);
}
}
return out;
}
const Options kDefaultOpts = {
// Expected execution time for kDefaultOpts on an Astro device:
// ~2min40sec for a full run; ~6sec for a reduced (perftest) run.
.limits =
{
.duration_per_config = kBenchmarkDurationDefault,
.runs_per_config = kBenchmarkRunsDefault,
.min_runs_per_config = kBenchmarkMinRuns,
},
.enabled =
{
Benchmark::Create,
Benchmark::Mix,
Benchmark::Output,
},
.enable_pprof = false,
.sample_formats =
{
ASF::UNSIGNED_8,
ASF::SIGNED_16, // kAltPreferredSampleFormat
ASF::SIGNED_24_IN_32,
ASF::FLOAT, // kPreferredSampleFormat
},
.num_input_output_chans =
{
{1, 1}, // kPreferredInputChans, kPreferredOutputChans
{1, 2},
{2, 1},
{2, 2}, // kAltPreferredInputChans, kAltPreferredOutputChans
{4, 4},
},
.samplers =
{
Resampler::SampleAndHold,
Resampler::WindowedSinc,
},
.source_dest_rates =
{
// Typical capture-path rate pairs
{96000, 16000},
{96000, 48000},
// Typical render-path rate pairs
{48000, 48000}, // kPreferredSourceRate, kPreferredDestRate
{44100, 48000}, // kAltPreferredSourceRate, kAltPreferredDestRate
{48000, 96000},
// Extreme cases
{1000, 192000},
{192000, 1000},
{192000, 192000},
},
.gain_types =
{
GainType::Mute,
GainType::Unity, // kPreferredGainType
GainType::Scaled, // kAltPreferredGainType
GainType::Ramped,
},
.accumulates =
{
false, // kPreferredAccumSetting
true, // kAltPreferredAccumSetting
},
.output_ranges =
{
OutputRange::Silence,
OutputRange::Normal, // kPreferredOutputRange
OutputRange::OutOfRange,
},
};
void Usage(const char* prog_name) {
printf("\nUsage: %s [--option] [...]\n", prog_name);
printf("Measure the performance of the audio mixer in microbenchmark operations.\n");
printf("\n");
printf("By default, all benchmark types are enabled for a large set of configurations.\n");
printf("Valid options are:\n");
printf("\n");
printf(" --%s=<seconds>\n", kBenchmarkDurationSwitch);
printf(" Run each benchmark for this duration at most (default: %.2f sec).\n",
static_cast<double>(kBenchmarkDurationDefault.to_msecs()) / 1000.0);
printf("\n");
printf(" --%s=<runs>\n", kBenchmarkRunsSwitch);
printf(" Run each benchmark for this many iterations at most (default: %zu, minimum: %zu).\n",
kBenchmarkRunsDefault, kBenchmarkMinRuns);
printf("\n");
printf(" --%s=<bool>\n", kProfileMixerCreationSwitch);
printf(" Run Mixer creation benchmarks (default: true).\n");
printf(" --%s=<bool>\n", kProfileMixingSwitch);
printf(" Run Mixer::Mix() benchmarks (default: true).\n");
printf(" --%s=<bool>\n", kProfileOutputSwitch);
printf(" Run OutputProducer benchmarks (default: true).\n");
printf("\n");
printf(" --%s=<bool>\n", kEnablePprofSwitch);
printf(" Save a pprof-compatible log to /tmp/audio_mixer_profiler.pprof (default: false).\n");
printf("\n");
printf(" --%s=[%s|%s]*\n", kSamplerSwitch, kSamplerPointOption, kSamplerSincOption);
printf(" Profile these samplers. Multiple samplers can be separated by commas.\n");
printf(" For example: --%s=%s,%s\n", kSamplerSwitch, kSamplerPointOption, kSamplerSincOption);
printf("\n");
printf(" --%s=[input_chans:output_chans]*\n", kChannelsSwitch);
printf(" Profile these channel pairs. Multiple pairs can be separated by commas.\n");
printf(" For example: --%s=1:2,1:4\n", kChannelsSwitch);
printf("\n");
printf(" --%s=[source_rate:dest_rate]*\n", kFrameRatesSwitch);
printf(" Profile these frame rate pairs. Multiple pairs can be separated by commas.\n");
printf(" For example: --%s=48000:48000,16000:48000\n", kFrameRatesSwitch);
printf("\n");
printf(" --%s=[%s|%s|%s|%s]*\n", kSampleFormatsSwitch, kSampleFormatUint8Option,
kSampleFormatInt16Option, kSampleFormatInt24In32Option, kSampleFormatFloat32Option);
printf(" Profile these sample formats. Multiple sample formats can be separated by commas.\n");
printf("\n");
printf(" --%s=[%s|%s|%s|%s]*\n", kMixGainsSwitch, kMixGainMuteOption, kMixGainUnityOption,
kMixGainScaledOption, kMixGainRampedOption);
printf(" Profile these mixer gain options. Multiple options can be separated by commas.\n");
printf("\n");
printf(" --%s=[false|true]*\n", kMixAccumulateSwitch);
printf(" Profile these overwrite/accumulate options (default is both).\n");
printf(" Both can be listed, separated by comma.\n");
printf("\n");
printf(" --%s=[%s|%s|%s]*\n", kOutputProducerSourceRangesSwitch,
kOutputProducerSourceRangeSilenceOption, kOutputProducerSourceRangeOutOfRangeOption,
kOutputProducerSourceRangeNormalOption);
printf(" Profile these outbound data ranges for OutputProducer benchmarks.\n");
printf(" Multiple types can be separated by commas.\n");
printf("\n");
printf(" --%s=<filepath.json>\n", kPerftestJsonFilepathSwitch);
printf(" Record perftest results to the specified json filepath. This reduces the profile\n");
printf(" set to as few as two unique configurations for each option value.\n");
printf("\n");
printf(" --%s\n", kUsageSwitch);
printf(" Display this message.\n");
printf("\n");
}
Options ParseCommandLine(int argc, char** argv) {
auto opt = kDefaultOpts;
auto command_line = fxl::CommandLineFromArgcArgv(argc, argv);
auto bool_flag = [&command_line](const std::string& flag_name, bool& out) {
if (!command_line.HasOption(flag_name)) {
return;
}
std::string str;
command_line.GetOptionValue(flag_name, &str);
if (str == "" || str == "true") {
out = true;
} else {
out = false;
}
};
auto duration_seconds_flag = [&command_line](const std::string& flag_name, zx::duration& out) {
if (!command_line.HasOption(flag_name)) {
return;
}
std::string str;
command_line.GetOptionValue(flag_name, &str);
double d = std::stod(str);
out = zx::nsec(static_cast<int64_t>(d * 1e9));
};
auto enum_flagset = [&command_line](const std::string& flag_name, auto& out, auto value_mapping) {
if (!command_line.HasOption(flag_name)) {
return;
}
out.clear();
std::string str;
command_line.GetOptionValue(flag_name, &str);
for (auto s : fxl::SplitStringCopy(str, ",", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty)) {
if (value_mapping.count(s) > 0) {
out.insert(value_mapping[s]);
}
}
};
auto int32_pair_flagset = [&command_line](const std::string& flag_name,
std::set<std::pair<int32_t, int32_t>>& out) {
if (!command_line.HasOption(flag_name)) {
return;
}
out.clear();
std::string str;
command_line.GetOptionValue(flag_name, &str);
for (auto s : fxl::SplitStringCopy(str, ",", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty)) {
auto pair = fxl::SplitStringCopy(s, ":", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
if (pair.size() == 2) {
out.insert({std::stoi(pair[0]), std::stoi(pair[1])});
}
}
};
if (command_line.HasOption(kUsageSwitch)) {
Usage(argv[0]);
exit(0);
}
if (command_line.HasOption(kPerftestJsonFilepathSwitch)) {
std::string json;
command_line.GetOptionValue(kPerftestJsonFilepathSwitch, &json);
opt.perftest_json = json;
}
if (command_line.HasOption(kBenchmarkRunsSwitch)) {
std::string runs;
command_line.GetOptionValue(kBenchmarkRunsSwitch, &runs);
opt.limits.runs_per_config = std::stoi(runs);
}
duration_seconds_flag(kBenchmarkDurationSwitch, opt.limits.duration_per_config);
bool profile_creation = true;
bool profile_mixing = true;
bool profile_output_producer = true;
bool_flag(kProfileMixerCreationSwitch, profile_creation);
bool_flag(kProfileMixingSwitch, profile_mixing);
bool_flag(kProfileOutputSwitch, profile_output_producer);
if (!profile_creation) {
opt.enabled.erase(Benchmark::Create);
}
if (!profile_mixing) {
opt.enabled.erase(Benchmark::Mix);
}
if (!profile_output_producer) {
opt.enabled.erase(Benchmark::Output);
}
bool_flag(kEnablePprofSwitch, opt.enable_pprof);
enum_flagset(kSamplerSwitch, opt.samplers,
std::map<std::string, Resampler>{
{kSamplerPointOption, Resampler::SampleAndHold},
{kSamplerSincOption, Resampler::WindowedSinc},
});
int32_pair_flagset(kChannelsSwitch, opt.num_input_output_chans);
int32_pair_flagset(kFrameRatesSwitch, opt.source_dest_rates);
enum_flagset(kSampleFormatsSwitch, opt.sample_formats,
std::map<std::string, ASF>{
{kSampleFormatUint8Option, ASF::UNSIGNED_8},
{kSampleFormatInt16Option, ASF::SIGNED_16},
{kSampleFormatInt24In32Option, ASF::SIGNED_24_IN_32},
{kSampleFormatFloat32Option, ASF::FLOAT},
});
enum_flagset(kMixGainsSwitch, opt.gain_types,
std::map<std::string, GainType>{
{kMixGainMuteOption, GainType::Mute},
{kMixGainUnityOption, GainType::Unity},
{kMixGainScaledOption, GainType::Scaled},
{kMixGainRampedOption, GainType::Ramped},
});
enum_flagset(kMixAccumulateSwitch, opt.accumulates,
std::map<std::string, bool>{
{"false", false},
{"true", true},
});
enum_flagset(kOutputProducerSourceRangesSwitch, opt.output_ranges,
std::map<std::string, OutputRange>{
{kOutputProducerSourceRangeSilenceOption, OutputRange::Silence},
{kOutputProducerSourceRangeOutOfRangeOption, OutputRange::OutOfRange},
{kOutputProducerSourceRangeNormalOption, OutputRange::Normal},
});
return opt;
}
} // namespace
int main(int argc, char** argv) {
syslog::SetTags({"audio_mixer_profiler"});
auto opt = ParseCommandLine(argc, argv);
printf("\n\n Performance Profiling\n\n");
if (opt.enable_pprof) {
ProfilerStart("/tmp/audio_mixer_profiler.pprof");
}
perftest::ResultsSet* results = nullptr;
if (opt.perftest_json) {
results = new perftest::ResultsSet();
}
if (opt.enabled.count(Benchmark::Create) > 0) {
AudioPerformance::ProfileMixerCreation(ConfigsForMixerCreation(opt), opt.limits, results);
}
if (opt.enabled.count(Benchmark::Mix) > 0) {
AudioPerformance::ProfileMixer(results ? ConfigsForMixerReduced(opt) : ConfigsForMixer(opt),
opt.limits, results);
}
if (opt.enabled.count(Benchmark::Output) > 0) {
AudioPerformance::ProfileOutputProducer(
results ? ConfigsForOutputProducerReduced(opt) : ConfigsForOutputProducer(opt), opt.limits,
results);
}
if (opt.enable_pprof) {
ProfilerStop();
}
if (results) {
return results->WriteJSONFile(opt.perftest_json.value().c_str()) ? 0 : 1;
}
return 0;
}