1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Standalone benchmarking application based on FFmpeg. This tool is used to 6 // measure decoding performance between different FFmpeg compile and run-time 7 // options. We also use this tool to measure performance regressions when 8 // testing newer builds of FFmpeg from trunk. 9 10 #include <iomanip> 11 #include <iostream> 12 #include <string> 13 14 #include "base/at_exit.h" 15 #include "base/basictypes.h" 16 #include "base/command_line.h" 17 #include "base/file_util.h" 18 #include "base/files/file_path.h" 19 #include "base/files/memory_mapped_file.h" 20 #include "base/logging.h" 21 #include "base/md5.h" 22 #include "base/path_service.h" 23 #include "base/strings/string_number_conversions.h" 24 #include "base/strings/string_util.h" 25 #include "base/strings/utf_string_conversions.h" 26 #include "base/time/time.h" 27 #include "build/build_config.h" 28 #include "media/base/djb2.h" 29 #include "media/base/media.h" 30 #include "media/ffmpeg/ffmpeg_common.h" 31 #include "media/filters/ffmpeg_glue.h" 32 #include "media/filters/ffmpeg_video_decoder.h" 33 #include "media/filters/in_memory_url_protocol.h" 34 35 // For pipe _setmode to binary 36 #if defined(OS_WIN) 37 #include <fcntl.h> 38 #include <io.h> 39 #endif 40 41 namespace switches { 42 const char kStream[] = "stream"; 43 const char kVideoThreads[] = "video-threads"; 44 const char kFast2[] = "fast2"; 45 const char kErrorCorrection[] = "error-correction"; 46 const char kSkip[] = "skip"; 47 const char kFlush[] = "flush"; 48 const char kDjb2[] = "djb2"; 49 const char kMd5[] = "md5"; 50 const char kFrames[] = "frames"; 51 const char kLoop[] = "loop"; 52 53 } // namespace switches 54 55 #if defined(OS_WIN) 56 57 // Enable to build with exception handler 58 // #define ENABLE_WINDOWS_EXCEPTIONS 1 59 60 #ifdef ENABLE_WINDOWS_EXCEPTIONS 61 // warning: disable warning about exception handler. 62 #pragma warning(disable:4509) 63 #endif 64 65 // Thread priorities to make benchmark more stable. 66 67 void EnterTimingSection() { 68 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); 69 } 70 71 void LeaveTimingSection() { 72 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); 73 } 74 #else 75 void EnterTimingSection() { 76 pthread_attr_t pta; 77 struct sched_param param; 78 79 pthread_attr_init(&pta); 80 memset(¶m, 0, sizeof(param)); 81 param.sched_priority = 78; 82 pthread_attr_setschedparam(&pta, ¶m); 83 pthread_attr_destroy(&pta); 84 } 85 86 void LeaveTimingSection() { 87 } 88 #endif 89 90 int main(int argc, const char** argv) { 91 base::AtExitManager exit_manager; 92 93 CommandLine::Init(argc, argv); 94 95 logging::LoggingSettings settings; 96 settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG; 97 logging::InitLogging(settings); 98 99 const CommandLine* cmd_line = CommandLine::ForCurrentProcess(); 100 const CommandLine::StringVector& filenames = cmd_line->GetArgs(); 101 if (filenames.empty()) { 102 std::cerr << "Usage: " << argv[0] << " [OPTIONS] FILE [DUMPFILE]\n" 103 << " --stream=[audio|video] " 104 << "Benchmark either the audio or video stream\n" 105 << " --video-threads=N " 106 << "Decode video using N threads\n" 107 << " --frames=N " 108 << "Decode N frames\n" 109 << " --loop=N " 110 << "Loop N times\n" 111 << " --fast2 " 112 << "Enable fast2 flag\n" 113 << " --error-correction " 114 << "Enable ffmpeg error correction\n" 115 << " --flush " 116 << "Flush last frame\n" 117 << " --djb2 (aka --hash) " 118 << "Hash decoded buffers (DJB2)\n" 119 << " --md5 " 120 << "Hash decoded buffers (MD5)\n" 121 << " --skip=[1|2|3] " 122 << "1=loop nonref, 2=loop, 3= frame nonref\n" << std::endl; 123 return 1; 124 } 125 126 // Initialize our media library (try loading DLLs, etc.) before continuing. 127 base::FilePath media_path; 128 PathService::Get(base::DIR_MODULE, &media_path); 129 if (!media::InitializeMediaLibrary(media_path)) { 130 std::cerr << "Unable to initialize the media library." << std::endl; 131 return 1; 132 } 133 134 // Retrieve command line options. 135 base::FilePath in_path(filenames[0]); 136 base::FilePath out_path; 137 if (filenames.size() > 1) 138 out_path = base::FilePath(filenames[1]); 139 AVMediaType target_codec = AVMEDIA_TYPE_UNKNOWN; 140 141 // Determine whether to benchmark audio or video decoding. 142 std::string stream(cmd_line->GetSwitchValueASCII(switches::kStream)); 143 if (!stream.empty()) { 144 if (stream.compare("audio") == 0) { 145 target_codec = AVMEDIA_TYPE_AUDIO; 146 } else if (stream.compare("video") == 0) { 147 target_codec = AVMEDIA_TYPE_VIDEO; 148 } else { 149 std::cerr << "Unknown --stream option " << stream << std::endl; 150 return 1; 151 } 152 } 153 154 // Determine number of threads to use for video decoding (optional). 155 int video_threads = 0; 156 std::string threads(cmd_line->GetSwitchValueASCII(switches::kVideoThreads)); 157 if (!threads.empty() && 158 !base::StringToInt(threads, &video_threads)) { 159 video_threads = 0; 160 } 161 162 // Determine number of frames to decode (optional). 163 int max_frames = 0; 164 std::string frames_opt(cmd_line->GetSwitchValueASCII(switches::kFrames)); 165 if (!frames_opt.empty() && 166 !base::StringToInt(frames_opt, &max_frames)) { 167 max_frames = 0; 168 } 169 170 // Determine number of times to loop (optional). 171 int max_loops = 0; 172 std::string loop_opt(cmd_line->GetSwitchValueASCII(switches::kLoop)); 173 if (!loop_opt.empty() && 174 !base::StringToInt(loop_opt, &max_loops)) { 175 max_loops = 0; 176 } 177 178 bool fast2 = false; 179 if (cmd_line->HasSwitch(switches::kFast2)) { 180 fast2 = true; 181 } 182 183 bool error_correction = false; 184 if (cmd_line->HasSwitch(switches::kErrorCorrection)) { 185 error_correction = true; 186 } 187 188 bool flush = false; 189 if (cmd_line->HasSwitch(switches::kFlush)) { 190 flush = true; 191 } 192 193 unsigned int hash_value = 5381u; // Seed for DJB2. 194 bool hash_djb2 = false; 195 if (cmd_line->HasSwitch(switches::kDjb2)) { 196 hash_djb2 = true; 197 } 198 199 base::MD5Context ctx; // Intermediate MD5 data: do not use 200 base::MD5Init(&ctx); 201 bool hash_md5 = false; 202 if (cmd_line->HasSwitch(switches::kMd5)) 203 hash_md5 = true; 204 205 int skip = 0; 206 if (cmd_line->HasSwitch(switches::kSkip)) { 207 std::string skip_opt(cmd_line->GetSwitchValueASCII(switches::kSkip)); 208 if (!base::StringToInt(skip_opt, &skip)) { 209 skip = 0; 210 } 211 } 212 213 std::ostream* log_out = &std::cout; 214 #if defined(ENABLE_WINDOWS_EXCEPTIONS) 215 // Catch exceptions so this tool can be used in automated testing. 216 __try { 217 #endif 218 219 base::MemoryMappedFile file_data; 220 file_data.Initialize(in_path); 221 media::InMemoryUrlProtocol protocol( 222 file_data.data(), file_data.length(), false); 223 224 // Register FFmpeg and attempt to open file. 225 media::FFmpegGlue glue(&protocol); 226 if (!glue.OpenContext()) { 227 std::cerr << "Error: Could not open input for " 228 << in_path.value() << std::endl; 229 return 1; 230 } 231 232 AVFormatContext* format_context = glue.format_context(); 233 234 // Open output file. 235 FILE *output = NULL; 236 if (!out_path.empty()) { 237 // TODO(fbarchard): Add pipe:1 for piping to stderr. 238 if (out_path.value().substr(0, 5) == FILE_PATH_LITERAL("pipe:") || 239 out_path.value() == FILE_PATH_LITERAL("-")) { 240 output = stdout; 241 log_out = &std::cerr; 242 #if defined(OS_WIN) 243 _setmode(_fileno(stdout), _O_BINARY); 244 #endif 245 } else { 246 output = file_util::OpenFile(out_path, "wb"); 247 } 248 if (!output) { 249 std::cerr << "Error: Could not open output " 250 << out_path.value() << std::endl; 251 return 1; 252 } 253 } 254 255 // Parse a little bit of the stream to fill out the format context. 256 if (avformat_find_stream_info(format_context, NULL) < 0) { 257 std::cerr << "Error: Could not find stream info for " 258 << in_path.value() << std::endl; 259 return 1; 260 } 261 262 // Find our target stream. 263 int target_stream = -1; 264 for (size_t i = 0; i < format_context->nb_streams; ++i) { 265 AVCodecContext* codec_context = format_context->streams[i]->codec; 266 AVCodec* codec = avcodec_find_decoder(codec_context->codec_id); 267 268 // See if we found our target codec. 269 if (codec_context->codec_type == target_codec && target_stream < 0) { 270 *log_out << "* "; 271 target_stream = i; 272 } else { 273 *log_out << " "; 274 } 275 276 if (!codec || (codec_context->codec_type == AVMEDIA_TYPE_UNKNOWN)) { 277 *log_out << "Stream #" << i << ": Unknown" << std::endl; 278 } else { 279 // Print out stream information 280 *log_out << "Stream #" << i << ": " << codec->name << " (" 281 << codec->long_name << ")" << std::endl; 282 } 283 } 284 285 // Only continue if we found our target stream. 286 if (target_stream < 0) { 287 std::cerr << "Error: Could not find target stream " 288 << target_stream << " for " << in_path.value() << std::endl; 289 return 1; 290 } 291 292 // Prepare FFmpeg structures. 293 AVPacket packet; 294 AVCodecContext* codec_context = format_context->streams[target_stream]->codec; 295 AVCodec* codec = avcodec_find_decoder(codec_context->codec_id); 296 297 // Only continue if we found our codec. 298 if (!codec) { 299 std::cerr << "Error: Could not find codec for " 300 << in_path.value() << std::endl; 301 return 1; 302 } 303 304 if (skip == 1) { 305 codec_context->skip_loop_filter = AVDISCARD_NONREF; 306 } else if (skip == 2) { 307 codec_context->skip_loop_filter = AVDISCARD_ALL; 308 } else if (skip == 3) { 309 codec_context->skip_loop_filter = AVDISCARD_ALL; 310 codec_context->skip_frame = AVDISCARD_NONREF; 311 } 312 if (fast2) { 313 // Note this flag is no longer necessary for H264 multithreading. 314 codec_context->flags2 |= CODEC_FLAG2_FAST; 315 } 316 if (error_correction) { 317 codec_context->error_concealment = FF_EC_GUESS_MVS | FF_EC_DEBLOCK; 318 } 319 320 // Initialize threaded decode. 321 if (target_codec == AVMEDIA_TYPE_VIDEO && video_threads > 0) { 322 codec_context->thread_count = video_threads; 323 } 324 325 // Initialize our codec. 326 if (avcodec_open2(codec_context, codec, NULL) < 0) { 327 std::cerr << "Error: Could not open codec " 328 << (codec_context->codec ? codec_context->codec->name : "(NULL)") 329 << " for " << in_path.value() << std::endl; 330 return 1; 331 } 332 333 // Buffer used for audio decoding. 334 scoped_ptr_malloc<AVFrame, media::ScopedPtrAVFree> audio_frame( 335 avcodec_alloc_frame()); 336 if (!audio_frame) { 337 std::cerr << "Error: avcodec_alloc_frame for " 338 << in_path.value() << std::endl; 339 return 1; 340 } 341 342 // Buffer used for video decoding. 343 scoped_ptr_malloc<AVFrame, media::ScopedPtrAVFree> video_frame( 344 avcodec_alloc_frame()); 345 if (!video_frame) { 346 std::cerr << "Error: avcodec_alloc_frame for " 347 << in_path.value() << std::endl; 348 return 1; 349 } 350 351 // Remember size of video. 352 int video_width = codec_context->width; 353 int video_height = codec_context->height; 354 355 // Stats collector. 356 EnterTimingSection(); 357 std::vector<double> decode_times; 358 decode_times.reserve(4096); 359 // Parse through the entire stream until we hit EOF. 360 base::TimeTicks start = base::TimeTicks::HighResNow(); 361 int frames = 0; 362 int read_result = 0; 363 do { 364 read_result = av_read_frame(format_context, &packet); 365 366 if (read_result < 0) { 367 if (max_loops) { 368 --max_loops; 369 } 370 if (max_loops > 0) { 371 av_seek_frame(format_context, -1, 0, AVSEEK_FLAG_BACKWARD); 372 read_result = 0; 373 continue; 374 } 375 if (flush) { 376 packet.stream_index = target_stream; 377 packet.size = 0; 378 } else { 379 break; 380 } 381 } 382 383 // Only decode packets from our target stream. 384 if (packet.stream_index == target_stream) { 385 int result = -1; 386 if (target_codec == AVMEDIA_TYPE_AUDIO) { 387 int size_out = 0; 388 int got_audio = 0; 389 390 avcodec_get_frame_defaults(audio_frame.get()); 391 392 base::TimeTicks decode_start = base::TimeTicks::HighResNow(); 393 result = avcodec_decode_audio4(codec_context, audio_frame.get(), 394 &got_audio, &packet); 395 base::TimeDelta delta = base::TimeTicks::HighResNow() - decode_start; 396 397 if (got_audio) { 398 size_out = av_samples_get_buffer_size( 399 NULL, codec_context->channels, audio_frame->nb_samples, 400 codec_context->sample_fmt, 1); 401 } 402 403 if (got_audio && size_out) { 404 decode_times.push_back(delta.InMillisecondsF()); 405 ++frames; 406 read_result = 0; // Force continuation. 407 408 if (output) { 409 if (fwrite(audio_frame->data[0], 1, size_out, output) != 410 static_cast<size_t>(size_out)) { 411 std::cerr << "Error: Could not write " 412 << size_out << " bytes for " << in_path.value() 413 << std::endl; 414 return 1; 415 } 416 } 417 418 const uint8* u8_samples = 419 reinterpret_cast<const uint8*>(audio_frame->data[0]); 420 if (hash_djb2) { 421 hash_value = DJB2Hash(u8_samples, size_out, hash_value); 422 } 423 if (hash_md5) { 424 base::MD5Update( 425 &ctx, 426 base::StringPiece(reinterpret_cast<const char*>(u8_samples), 427 size_out)); 428 } 429 } 430 } else if (target_codec == AVMEDIA_TYPE_VIDEO) { 431 int got_picture = 0; 432 433 avcodec_get_frame_defaults(video_frame.get()); 434 435 base::TimeTicks decode_start = base::TimeTicks::HighResNow(); 436 result = avcodec_decode_video2(codec_context, video_frame.get(), 437 &got_picture, &packet); 438 base::TimeDelta delta = base::TimeTicks::HighResNow() - decode_start; 439 440 if (got_picture) { 441 decode_times.push_back(delta.InMillisecondsF()); 442 ++frames; 443 read_result = 0; // Force continuation. 444 445 for (int plane = 0; plane < 3; ++plane) { 446 const uint8* source = video_frame->data[plane]; 447 const size_t source_stride = video_frame->linesize[plane]; 448 size_t bytes_per_line = codec_context->width; 449 size_t copy_lines = codec_context->height; 450 if (plane != 0) { 451 switch (codec_context->pix_fmt) { 452 case PIX_FMT_YUV420P: 453 case PIX_FMT_YUVJ420P: 454 bytes_per_line /= 2; 455 copy_lines = (copy_lines + 1) / 2; 456 break; 457 case PIX_FMT_YUV422P: 458 case PIX_FMT_YUVJ422P: 459 bytes_per_line /= 2; 460 break; 461 case PIX_FMT_YUV444P: 462 case PIX_FMT_YUVJ444P: 463 break; 464 default: 465 std::cerr << "Error: Unknown video format " 466 << codec_context->pix_fmt; 467 return 1; 468 } 469 } 470 if (output) { 471 for (size_t i = 0; i < copy_lines; ++i) { 472 if (fwrite(source, 1, bytes_per_line, output) != 473 bytes_per_line) { 474 std::cerr << "Error: Could not write data after " 475 << copy_lines << " lines for " 476 << in_path.value() << std::endl; 477 return 1; 478 } 479 source += source_stride; 480 } 481 } 482 if (hash_djb2) { 483 for (size_t i = 0; i < copy_lines; ++i) { 484 hash_value = DJB2Hash(source, bytes_per_line, hash_value); 485 source += source_stride; 486 } 487 } 488 if (hash_md5) { 489 for (size_t i = 0; i < copy_lines; ++i) { 490 base::MD5Update( 491 &ctx, 492 base::StringPiece(reinterpret_cast<const char*>(source), 493 bytes_per_line)); 494 source += source_stride; 495 } 496 } 497 } 498 } 499 } else { 500 NOTREACHED(); 501 } 502 503 // Make sure our decoding went OK. 504 if (result < 0) { 505 std::cerr << "Error: avcodec_decode returned " 506 << result << " for " << in_path.value() << std::endl; 507 return 1; 508 } 509 } 510 // Free our packet. 511 av_free_packet(&packet); 512 513 if (max_frames && (frames >= max_frames)) 514 break; 515 } while (read_result >= 0); 516 base::TimeDelta total = base::TimeTicks::HighResNow() - start; 517 LeaveTimingSection(); 518 519 // Clean up. 520 if (output) 521 file_util::CloseFile(output); 522 523 // Calculate the sum of times. Note that some of these may be zero. 524 double sum = 0; 525 for (size_t i = 0; i < decode_times.size(); ++i) { 526 sum += decode_times[i]; 527 } 528 529 double average = 0; 530 double stddev = 0; 531 double fps = 0; 532 if (frames > 0) { 533 // Calculate the average time per frame. 534 average = sum / frames; 535 536 // Calculate the sum of the squared differences. 537 // Standard deviation will only be accurate if no threads are used. 538 // TODO(fbarchard): Rethink standard deviation calculation. 539 double squared_sum = 0; 540 for (int i = 0; i < frames; ++i) { 541 double difference = decode_times[i] - average; 542 squared_sum += difference * difference; 543 } 544 545 // Calculate the standard deviation (jitter). 546 stddev = sqrt(squared_sum / frames); 547 548 // Calculate frames per second. 549 fps = frames * 1000.0 / sum; 550 } 551 552 // Print our results. 553 log_out->setf(std::ios::fixed); 554 log_out->precision(2); 555 *log_out << std::endl; 556 *log_out << " Frames:" << std::setw(11) << frames << std::endl; 557 *log_out << " Width:" << std::setw(11) << video_width << std::endl; 558 *log_out << " Height:" << std::setw(11) << video_height << std::endl; 559 *log_out << " Total:" << std::setw(11) << total.InMillisecondsF() 560 << " ms" << std::endl; 561 *log_out << " Summation:" << std::setw(11) << sum 562 << " ms" << std::endl; 563 *log_out << " Average:" << std::setw(11) << average 564 << " ms" << std::endl; 565 *log_out << " StdDev:" << std::setw(11) << stddev 566 << " ms" << std::endl; 567 *log_out << " FPS:" << std::setw(11) << fps 568 << std::endl; 569 if (hash_djb2) { 570 *log_out << " DJB2 Hash:" << std::setw(11) << hash_value 571 << " " << in_path.value() << std::endl; 572 } 573 if (hash_md5) { 574 base::MD5Digest digest; // The result of the computation. 575 base::MD5Final(&digest, &ctx); 576 *log_out << " MD5 Hash: " << base::MD5DigestToBase16(digest) 577 << " " << in_path.value() << std::endl; 578 } 579 #if defined(ENABLE_WINDOWS_EXCEPTIONS) 580 } __except(EXCEPTION_EXECUTE_HANDLER) { 581 *log_out << " Exception:" << std::setw(11) << GetExceptionCode() 582 << " " << in_path.value() << std::endl; 583 return 1; 584 } 585 #endif 586 CommandLine::Reset(); 587 return 0; 588 } 589