audio_processing_performance_unittest.cc (19625B)
1 /* 2 * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 #include <atomic> 12 #include <cstddef> 13 #include <cstdint> 14 #include <cstdio> 15 #include <memory> 16 #include <string> 17 #include <vector> 18 19 #include "absl/strings/string_view.h" 20 #include "api/audio/audio_processing.h" 21 #include "api/audio/builtin_audio_processing_builder.h" 22 #include "api/environment/environment_factory.h" 23 #include "api/numerics/samples_stats_counter.h" 24 #include "api/scoped_refptr.h" 25 #include "api/test/metrics/global_metrics_logger_and_exporter.h" 26 #include "api/test/metrics/metric.h" 27 #include "api/units/time_delta.h" 28 #include "rtc_base/checks.h" 29 #include "rtc_base/event.h" 30 #include "rtc_base/platform_thread.h" 31 #include "rtc_base/random.h" 32 #include "system_wrappers/include/clock.h" 33 #include "test/gtest.h" 34 35 namespace webrtc { 36 namespace { 37 38 using test::GetGlobalMetricsLogger; 39 using test::ImprovementDirection; 40 using test::Metric; 41 using test::Unit; 42 43 class CallSimulator; 44 45 // Type of the render thread APM API call to use in the test. 46 enum class ProcessorType { kRender, kCapture }; 47 48 // Variant of APM processing settings to use in the test. 49 enum class SettingsType { 50 kDefaultApmDesktop, 51 kDefaultApmMobile, 52 kAllSubmodulesTurnedOff, 53 kDefaultApmDesktopWithoutDelayAgnostic, 54 kDefaultApmDesktopWithoutExtendedFilter 55 }; 56 57 // Variables related to the audio data and formats. 58 struct AudioFrameData { 59 explicit AudioFrameData(size_t max_frame_size) { 60 // Set up the two-dimensional arrays needed for the APM API calls. 61 input_framechannels.resize(2 * max_frame_size); 62 input_frame.resize(2); 63 input_frame[0] = &input_framechannels[0]; 64 input_frame[1] = &input_framechannels[max_frame_size]; 65 66 output_frame_channels.resize(2 * max_frame_size); 67 output_frame.resize(2); 68 output_frame[0] = &output_frame_channels[0]; 69 output_frame[1] = &output_frame_channels[max_frame_size]; 70 } 71 72 std::vector<float> output_frame_channels; 73 std::vector<float*> output_frame; 74 std::vector<float> input_framechannels; 75 std::vector<float*> input_frame; 76 StreamConfig input_stream_config; 77 StreamConfig output_stream_config; 78 }; 79 80 // The configuration for the test. 81 struct SimulationConfig { 82 SimulationConfig(int sample_rate_hz, SettingsType simulation_settings) 83 : sample_rate_hz(sample_rate_hz), 84 simulation_settings(simulation_settings) {} 85 86 static std::vector<SimulationConfig> GenerateSimulationConfigs() { 87 std::vector<SimulationConfig> simulation_configs; 88 #ifndef WEBRTC_ANDROID 89 const SettingsType desktop_settings[] = { 90 SettingsType::kDefaultApmDesktop, SettingsType::kAllSubmodulesTurnedOff, 91 SettingsType::kDefaultApmDesktopWithoutDelayAgnostic, 92 SettingsType::kDefaultApmDesktopWithoutExtendedFilter}; 93 94 const int desktop_sample_rates[] = {8000, 16000, 32000, 48000}; 95 96 for (auto sample_rate : desktop_sample_rates) { 97 for (auto settings : desktop_settings) { 98 simulation_configs.push_back(SimulationConfig(sample_rate, settings)); 99 } 100 } 101 #endif 102 103 const SettingsType mobile_settings[] = {SettingsType::kDefaultApmMobile}; 104 105 const int mobile_sample_rates[] = {8000, 16000}; 106 107 for (auto sample_rate : mobile_sample_rates) { 108 for (auto settings : mobile_settings) { 109 simulation_configs.push_back(SimulationConfig(sample_rate, settings)); 110 } 111 } 112 113 return simulation_configs; 114 } 115 116 std::string SettingsDescription() const { 117 std::string description; 118 switch (simulation_settings) { 119 case SettingsType::kDefaultApmMobile: 120 description = "DefaultApmMobile"; 121 break; 122 case SettingsType::kDefaultApmDesktop: 123 description = "DefaultApmDesktop"; 124 break; 125 case SettingsType::kAllSubmodulesTurnedOff: 126 description = "AllSubmodulesOff"; 127 break; 128 case SettingsType::kDefaultApmDesktopWithoutDelayAgnostic: 129 description = "DefaultApmDesktopWithoutDelayAgnostic"; 130 break; 131 case SettingsType::kDefaultApmDesktopWithoutExtendedFilter: 132 description = "DefaultApmDesktopWithoutExtendedFilter"; 133 break; 134 } 135 return description; 136 } 137 138 int sample_rate_hz = 16000; 139 SettingsType simulation_settings = SettingsType::kDefaultApmDesktop; 140 }; 141 142 // Handler for the frame counters. 143 class FrameCounters { 144 public: 145 void IncreaseRenderCounter() { render_count_.fetch_add(1); } 146 147 void IncreaseCaptureCounter() { capture_count_.fetch_add(1); } 148 149 int CaptureMinusRenderCounters() const { 150 // The return value will be approximate, but that's good enough since 151 // by the time we return the value, it's not guaranteed to be correct 152 // anyway. 153 return capture_count_.load(std::memory_order_acquire) - 154 render_count_.load(std::memory_order_acquire); 155 } 156 157 int RenderMinusCaptureCounters() const { 158 return -CaptureMinusRenderCounters(); 159 } 160 161 bool BothCountersExceedeThreshold(int threshold) const { 162 // TODO(tommi): We could use an event to signal this so that we don't need 163 // to be polling from the main thread and possibly steal cycles. 164 const int capture_count = capture_count_.load(std::memory_order_acquire); 165 const int render_count = render_count_.load(std::memory_order_acquire); 166 return (render_count > threshold && capture_count > threshold); 167 } 168 169 private: 170 std::atomic<int> render_count_{0}; 171 std::atomic<int> capture_count_{0}; 172 }; 173 174 // Class that represents a flag that can only be raised. 175 class LockedFlag { 176 public: 177 bool get_flag() const { return flag_.load(std::memory_order_acquire); } 178 179 void set_flag() { 180 if (!get_flag()) { 181 // read-only operation to avoid affecting the cache-line. 182 int zero = 0; 183 flag_.compare_exchange_strong(zero, 1); 184 } 185 } 186 187 private: 188 std::atomic<int> flag_{0}; 189 }; 190 191 // Parent class for the thread processors. 192 class TimedThreadApiProcessor { 193 public: 194 TimedThreadApiProcessor(ProcessorType processor_type, 195 Random* rand_gen, 196 FrameCounters* shared_counters_state, 197 LockedFlag* capture_call_checker, 198 CallSimulator* test_framework, 199 const SimulationConfig* simulation_config, 200 AudioProcessing* apm, 201 int num_durations_to_store, 202 float input_level, 203 int num_channels) 204 : rand_gen_(rand_gen), 205 frame_counters_(shared_counters_state), 206 capture_call_checker_(capture_call_checker), 207 test_(test_framework), 208 simulation_config_(simulation_config), 209 apm_(apm), 210 frame_data_(kMaxFrameSize), 211 clock_(Clock::GetRealTimeClock()), 212 num_durations_to_store_(num_durations_to_store), 213 api_call_durations_(num_durations_to_store_ - kNumInitializationFrames), 214 samples_count_(0), 215 input_level_(input_level), 216 processor_type_(processor_type), 217 num_channels_(num_channels) {} 218 219 // Implements the callback functionality for the threads. 220 bool Process(); 221 222 // Method for printing out the simulation statistics. 223 void print_processor_statistics(absl::string_view processor_name) const { 224 const std::string modifier = "_api_call_duration"; 225 226 const std::string sample_rate_name = 227 "_" + std::to_string(simulation_config_->sample_rate_hz) + "Hz"; 228 229 GetGlobalMetricsLogger()->LogMetric( 230 "apm_timing" + sample_rate_name, processor_name, api_call_durations_, 231 Unit::kMilliseconds, ImprovementDirection::kNeitherIsBetter); 232 } 233 234 void AddDuration(int64_t duration) { 235 if (samples_count_ >= kNumInitializationFrames && 236 samples_count_ < num_durations_to_store_) { 237 api_call_durations_.AddSample({.value = static_cast<double>(duration), 238 .time = clock_->CurrentTime()}); 239 } 240 samples_count_++; 241 } 242 243 private: 244 static const int kMaxCallDifference = 10; 245 static const int kMaxFrameSize = 480; 246 static const int kNumInitializationFrames = 5; 247 248 int ProcessCapture() { 249 // Set the stream delay. 250 apm_->set_stream_delay_ms(30); 251 252 // Call and time the specified capture side API processing method. 253 const int64_t start_time = clock_->TimeInMicroseconds(); 254 const int result = apm_->ProcessStream( 255 &frame_data_.input_frame[0], frame_data_.input_stream_config, 256 frame_data_.output_stream_config, &frame_data_.output_frame[0]); 257 const int64_t end_time = clock_->TimeInMicroseconds(); 258 259 frame_counters_->IncreaseCaptureCounter(); 260 261 AddDuration(end_time - start_time); 262 263 if (first_process_call_) { 264 // Flag that the capture side has been called at least once 265 // (needed to ensure that a capture call has been done 266 // before the first render call is performed (implicitly 267 // required by the APM API). 268 capture_call_checker_->set_flag(); 269 first_process_call_ = false; 270 } 271 return result; 272 } 273 274 bool ReadyToProcessCapture() { 275 return (frame_counters_->CaptureMinusRenderCounters() <= 276 kMaxCallDifference); 277 } 278 279 int ProcessRender() { 280 // Call and time the specified render side API processing method. 281 const int64_t start_time = clock_->TimeInMicroseconds(); 282 const int result = apm_->ProcessReverseStream( 283 &frame_data_.input_frame[0], frame_data_.input_stream_config, 284 frame_data_.output_stream_config, &frame_data_.output_frame[0]); 285 const int64_t end_time = clock_->TimeInMicroseconds(); 286 frame_counters_->IncreaseRenderCounter(); 287 288 AddDuration(end_time - start_time); 289 290 return result; 291 } 292 293 bool ReadyToProcessRender() { 294 // Do not process until at least one capture call has been done. 295 // (implicitly required by the APM API). 296 if (first_process_call_ && !capture_call_checker_->get_flag()) { 297 return false; 298 } 299 300 // Ensure that the number of render and capture calls do not differ too 301 // much. 302 if (frame_counters_->RenderMinusCaptureCounters() > kMaxCallDifference) { 303 return false; 304 } 305 306 first_process_call_ = false; 307 return true; 308 } 309 310 void PrepareFrame() { 311 // Lambda function for populating a float multichannel audio frame 312 // with random data. 313 auto populate_audio_frame = [](float amplitude, size_t num_channels, 314 size_t samples_per_channel, Random* rand_gen, 315 float** frame) { 316 for (size_t ch = 0; ch < num_channels; ch++) { 317 for (size_t k = 0; k < samples_per_channel; k++) { 318 // Store random float number with a value between +-amplitude. 319 frame[ch][k] = amplitude * (2 * rand_gen->Rand<float>() - 1); 320 } 321 } 322 }; 323 324 // Prepare the audio input data and metadata. 325 frame_data_.input_stream_config.set_sample_rate_hz( 326 simulation_config_->sample_rate_hz); 327 frame_data_.input_stream_config.set_num_channels(num_channels_); 328 populate_audio_frame(input_level_, num_channels_, 329 (simulation_config_->sample_rate_hz * 330 AudioProcessing::kChunkSizeMs / 1000), 331 rand_gen_, &frame_data_.input_frame[0]); 332 333 // Prepare the float audio output data and metadata. 334 frame_data_.output_stream_config.set_sample_rate_hz( 335 simulation_config_->sample_rate_hz); 336 frame_data_.output_stream_config.set_num_channels(1); 337 } 338 339 bool ReadyToProcess() { 340 switch (processor_type_) { 341 case ProcessorType::kRender: 342 return ReadyToProcessRender(); 343 344 case ProcessorType::kCapture: 345 return ReadyToProcessCapture(); 346 } 347 348 // Should not be reached, but the return statement is needed for the code to 349 // build successfully on Android. 350 RTC_DCHECK_NOTREACHED(); 351 return false; 352 } 353 354 Random* rand_gen_ = nullptr; 355 FrameCounters* frame_counters_ = nullptr; 356 LockedFlag* capture_call_checker_ = nullptr; 357 CallSimulator* test_ = nullptr; 358 const SimulationConfig* const simulation_config_ = nullptr; 359 AudioProcessing* apm_ = nullptr; 360 AudioFrameData frame_data_; 361 Clock* clock_; 362 const size_t num_durations_to_store_; 363 SamplesStatsCounter api_call_durations_; 364 size_t samples_count_ = 0; 365 const float input_level_; 366 bool first_process_call_ = true; 367 const ProcessorType processor_type_; 368 const int num_channels_ = 1; 369 }; 370 371 // Class for managing the test simulation. 372 class CallSimulator : public ::testing::TestWithParam<SimulationConfig> { 373 public: 374 CallSimulator() 375 : rand_gen_(42U), 376 simulation_config_(static_cast<SimulationConfig>(GetParam())) {} 377 378 // Run the call simulation with a timeout. 379 bool Run() { 380 StartThreads(); 381 382 bool result = test_complete_.Wait(kTestTimeout); 383 384 StopThreads(); 385 386 render_thread_state_->print_processor_statistics( 387 simulation_config_.SettingsDescription() + "_render"); 388 capture_thread_state_->print_processor_statistics( 389 simulation_config_.SettingsDescription() + "_capture"); 390 391 return result; 392 } 393 394 // Tests whether all the required render and capture side calls have been 395 // done. 396 bool MaybeEndTest() { 397 if (frame_counters_.BothCountersExceedeThreshold(kMinNumFramesToProcess)) { 398 test_complete_.Set(); 399 return true; 400 } 401 return false; 402 } 403 404 private: 405 static const float kCaptureInputFloatLevel; 406 static const float kRenderInputFloatLevel; 407 static const int kMinNumFramesToProcess = 150; 408 static constexpr TimeDelta kTestTimeout = 409 TimeDelta::Millis(3 * 10 * kMinNumFramesToProcess); 410 411 // Stop all running threads. 412 void StopThreads() { 413 render_thread_.Finalize(); 414 capture_thread_.Finalize(); 415 } 416 417 // Simulator and APM setup. 418 void SetUp() override { 419 // Lambda function for setting the default APM runtime settings for desktop. 420 auto set_default_desktop_apm_runtime_settings = [](AudioProcessing* apm) { 421 AudioProcessing::Config apm_config = apm->GetConfig(); 422 apm_config.echo_canceller.enabled = true; 423 apm_config.echo_canceller.mobile_mode = false; 424 apm_config.noise_suppression.enabled = true; 425 apm_config.gain_controller1.enabled = true; 426 apm_config.gain_controller1.mode = 427 AudioProcessing::Config::GainController1::kAdaptiveDigital; 428 apm->ApplyConfig(apm_config); 429 }; 430 431 // Lambda function for setting the default APM runtime settings for mobile. 432 auto set_default_mobile_apm_runtime_settings = [](AudioProcessing* apm) { 433 AudioProcessing::Config apm_config = apm->GetConfig(); 434 apm_config.echo_canceller.enabled = true; 435 apm_config.echo_canceller.mobile_mode = true; 436 apm_config.noise_suppression.enabled = true; 437 apm_config.gain_controller1.mode = 438 AudioProcessing::Config::GainController1::kAdaptiveDigital; 439 apm->ApplyConfig(apm_config); 440 }; 441 442 // Lambda function for turning off all of the APM runtime settings 443 // submodules. 444 auto turn_off_default_apm_runtime_settings = [](AudioProcessing* apm) { 445 AudioProcessing::Config apm_config = apm->GetConfig(); 446 apm_config.echo_canceller.enabled = false; 447 apm_config.gain_controller1.enabled = false; 448 apm_config.noise_suppression.enabled = false; 449 apm->ApplyConfig(apm_config); 450 }; 451 452 int num_capture_channels = 1; 453 switch (simulation_config_.simulation_settings) { 454 case SettingsType::kDefaultApmMobile: { 455 apm_ = BuiltinAudioProcessingBuilder().Build(CreateEnvironment()); 456 ASSERT_TRUE(!!apm_); 457 set_default_mobile_apm_runtime_settings(apm_.get()); 458 break; 459 } 460 case SettingsType::kDefaultApmDesktop: { 461 apm_ = BuiltinAudioProcessingBuilder().Build(CreateEnvironment()); 462 ASSERT_TRUE(!!apm_); 463 set_default_desktop_apm_runtime_settings(apm_.get()); 464 break; 465 } 466 case SettingsType::kAllSubmodulesTurnedOff: { 467 apm_ = BuiltinAudioProcessingBuilder().Build(CreateEnvironment()); 468 ASSERT_TRUE(!!apm_); 469 turn_off_default_apm_runtime_settings(apm_.get()); 470 break; 471 } 472 case SettingsType::kDefaultApmDesktopWithoutDelayAgnostic: { 473 apm_ = BuiltinAudioProcessingBuilder().Build(CreateEnvironment()); 474 ASSERT_TRUE(!!apm_); 475 set_default_desktop_apm_runtime_settings(apm_.get()); 476 break; 477 } 478 case SettingsType::kDefaultApmDesktopWithoutExtendedFilter: { 479 apm_ = BuiltinAudioProcessingBuilder().Build(CreateEnvironment()); 480 ASSERT_TRUE(!!apm_); 481 set_default_desktop_apm_runtime_settings(apm_.get()); 482 break; 483 } 484 } 485 486 render_thread_state_.reset(new TimedThreadApiProcessor( 487 ProcessorType::kRender, &rand_gen_, &frame_counters_, 488 &capture_call_checker_, this, &simulation_config_, apm_.get(), 489 kMinNumFramesToProcess, kRenderInputFloatLevel, 1)); 490 capture_thread_state_.reset(new TimedThreadApiProcessor( 491 ProcessorType::kCapture, &rand_gen_, &frame_counters_, 492 &capture_call_checker_, this, &simulation_config_, apm_.get(), 493 kMinNumFramesToProcess, kCaptureInputFloatLevel, num_capture_channels)); 494 } 495 496 // Start the threads used in the test. 497 void StartThreads() { 498 const auto attributes = 499 ThreadAttributes().SetPriority(ThreadPriority::kRealtime); 500 render_thread_ = PlatformThread::SpawnJoinable( 501 [this] { 502 while (render_thread_state_->Process()) { 503 } 504 }, 505 "render", attributes); 506 capture_thread_ = PlatformThread::SpawnJoinable( 507 [this] { 508 while (capture_thread_state_->Process()) { 509 } 510 }, 511 "capture", attributes); 512 } 513 514 // Event handler for the test. 515 Event test_complete_; 516 517 // Thread related variables. 518 Random rand_gen_; 519 520 scoped_refptr<AudioProcessing> apm_; 521 const SimulationConfig simulation_config_; 522 FrameCounters frame_counters_; 523 LockedFlag capture_call_checker_; 524 std::unique_ptr<TimedThreadApiProcessor> render_thread_state_; 525 std::unique_ptr<TimedThreadApiProcessor> capture_thread_state_; 526 PlatformThread render_thread_; 527 PlatformThread capture_thread_; 528 }; 529 530 // Implements the callback functionality for the threads. 531 bool TimedThreadApiProcessor::Process() { 532 PrepareFrame(); 533 534 // Wait in a spinlock manner until it is ok to start processing. 535 // Note that SleepMs is not applicable since it only allows sleeping 536 // on a millisecond basis which is too long. 537 // TODO(tommi): This loop may affect the performance of the test that it's 538 // meant to measure. See if we could use events instead to signal readiness. 539 while (!ReadyToProcess()) { 540 } 541 542 int result = AudioProcessing::kNoError; 543 switch (processor_type_) { 544 case ProcessorType::kRender: 545 result = ProcessRender(); 546 break; 547 case ProcessorType::kCapture: 548 result = ProcessCapture(); 549 break; 550 } 551 552 EXPECT_EQ(result, AudioProcessing::kNoError); 553 554 return !test_->MaybeEndTest(); 555 } 556 557 const float CallSimulator::kRenderInputFloatLevel = 0.5f; 558 const float CallSimulator::kCaptureInputFloatLevel = 0.03125f; 559 } // anonymous namespace 560 561 TEST_P(CallSimulator, ApiCallDurationTest) { 562 // Run test and verify that it did not time out. 563 EXPECT_TRUE(Run()); 564 } 565 566 INSTANTIATE_TEST_SUITE_P( 567 AudioProcessingPerformanceTest, 568 CallSimulator, 569 ::testing::ValuesIn(SimulationConfig::GenerateSimulationConfigs())); 570 571 } // namespace webrtc