| /* | |
| * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. | |
| * | |
| * Use of this source code is governed by a BSD-style license | |
| * that can be found in the LICENSE file in the root of the source | |
| * tree. An additional intellectual property rights grant can be found | |
| * in the file PATENTS. All contributing project authors may | |
| * be found in the AUTHORS file in the root of the source tree. | |
| */ | |
| #include <cstdio> | |
| #include <fstream> | |
| #include <iostream> | |
| #include <map> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/algorithm/container.h" | |
| #include "absl/flags/flag.h" | |
| #include "absl/flags/parse.h" | |
| #include "absl/flags/usage.h" | |
| #include "absl/flags/usage_config.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/string_view.h" | |
| #include "api/environment/environment.h" | |
| #include "api/environment/environment_factory.h" | |
| #include "api/field_trials.h" | |
| #include "api/neteq/neteq.h" | |
| #include "api/units/time_delta.h" | |
| #include "logging/rtc_event_log/rtc_event_log_parser.h" | |
| #include "rtc_base/checks.h" | |
| #include "rtc_base/logging.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/alerts.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/analyze_audio.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/analyzer.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/analyzer_common.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/conversational_speech_en.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/plot_base.h" | |
| #include "rtc_tools/rtc_event_log_visualizer/proto/chart.pb.h" | |
| ABSL_FLAG(std::string, | |
| plot, | |
| "default", | |
| "A comma separated list of plot names. See --list_plots for valid " | |
| "options."); | |
| ABSL_FLAG( | |
| std::string, | |
| force_fieldtrials, | |
| "", | |
| "Field trials control experimental feature code which can be forced. " | |
| "E.g. running with --force_fieldtrials=WebRTC-FooFeature/Enabled/" | |
| " will assign the group Enabled to field trial WebRTC-FooFeature. Multiple " | |
| "trials are separated by \"/\""); | |
| ABSL_FLAG(std::string, | |
| wav_filename, | |
| "", | |
| "Path to wav file used for simulation of jitter buffer."); | |
| ABSL_FLAG(bool, | |
| show_detector_state, | |
| false, | |
| "Show the state of the delay based BWE detector on the total " | |
| "bitrate graph."); | |
| ABSL_FLAG(bool, | |
| show_alr_state, | |
| false, | |
| "Show the state ALR state on the total bitrate graph."); | |
| ABSL_FLAG(bool, | |
| show_link_capacity, | |
| true, | |
| "Show the lower and upper link capacity on the outgoing bitrate " | |
| "graph."); | |
| ABSL_FLAG(bool, | |
| parse_unconfigured_header_extensions, | |
| true, | |
| "Attempt to parse unconfigured header extensions using the default " | |
| "WebRTC mapping. This can give very misleading results if the " | |
| "application negotiates a different mapping."); | |
| ABSL_FLAG(bool, | |
| print_triage_alerts, | |
| true, | |
| "Print triage alerts, i.e. a list of potential problems."); | |
| ABSL_FLAG(bool, | |
| normalize_time, | |
| true, | |
| "Normalize the log timestamps so that the call starts at time 0."); | |
| ABSL_FLAG(bool, | |
| shared_xaxis, | |
| false, | |
| "Share x-axis between all plots so that zooming in one plot " | |
| "updates all the others too. A downside is that certain " | |
| "operations like panning become much slower."); | |
| ABSL_FLAG(bool, show_grid, false, "Show a grid in all plots."); | |
| ABSL_FLAG(bool, | |
| protobuf_output, | |
| false, | |
| "Output charts as protobuf instead of python code."); | |
| ABSL_FLAG(std::string, | |
| figure_output_path, | |
| "", | |
| "A path to output the python plots into."); | |
| ABSL_FLAG(bool, | |
| list_plots, | |
| false, | |
| "List of registered plots (for use with the --plot flag)."); | |
| ABSL_FLAG(int, | |
| averaging_window, | |
| 250, | |
| "Time window (in ms) used for calculating moving average bitrates."); | |
| ABSL_FLAG(int, | |
| averaging_step, | |
| 10, | |
| "How often (in ms) a data point is generated in bitrate plots."); | |
| namespace { | |
| std::vector<std::string> StrSplit(const std::string& s, | |
| const std::string& delimiter) { | |
| std::vector<std::string> v; | |
| size_t pos = 0; | |
| while (pos < s.length()) { | |
| const std::string token = s.substr(pos, s.find(delimiter, pos) - pos); | |
| pos += token.length() + delimiter.length(); | |
| v.push_back(token); | |
| } | |
| return v; | |
| } | |
| bool KnownPlotName(absl::string_view name, | |
| const std::vector<std::string>& known_plots) { | |
| return absl::c_find(known_plots, name) != known_plots.end(); | |
| } | |
| bool ContainsHelppackageFlags(absl::string_view filename) { | |
| return absl::EndsWith(filename, "main.cc"); | |
| } | |
| } // namespace | |
| int main(int argc, char* argv[]) { | |
| absl::SetProgramUsageMessage( | |
| "A tool for visualizing WebRTC event logs.\n" | |
| "Example usage:\n" | |
| "./event_log_visualizer <logfile> | python\n"); | |
| absl::FlagsUsageConfig flag_config; | |
| flag_config.contains_help_flags = &ContainsHelppackageFlags; | |
| absl::SetFlagsUsageConfig(flag_config); | |
| std::vector<char*> args = absl::ParseCommandLine(argc, argv); | |
| // Print RTC_LOG warnings and errors even in release builds. | |
| if (webrtc::LogMessage::GetLogToDebug() > webrtc::LS_WARNING) { | |
| webrtc::LogMessage::LogToDebug(webrtc::LS_WARNING); | |
| } | |
| webrtc::LogMessage::SetLogToStderr(true); | |
| const std::string field_trials = absl::GetFlag(FLAGS_force_fieldtrials); | |
| webrtc::Environment env = webrtc::CreateEnvironment( | |
| std::make_unique<webrtc::FieldTrials>(field_trials)); | |
| webrtc::ParsedRtcEventLog::UnconfiguredHeaderExtensions header_extensions = | |
| webrtc::ParsedRtcEventLog::UnconfiguredHeaderExtensions::kDontParse; | |
| if (absl::GetFlag(FLAGS_parse_unconfigured_header_extensions)) { | |
| header_extensions = webrtc::ParsedRtcEventLog:: | |
| UnconfiguredHeaderExtensions::kAttemptWebrtcDefaultConfig; | |
| } | |
| webrtc::ParsedRtcEventLog parsed_log(header_extensions, | |
| /*allow_incomplete_logs*/ true); | |
| if (args.size() == 2) { | |
| std::string filename = args[1]; | |
| auto status = parsed_log.ParseFile(filename); | |
| if (!status.ok()) { | |
| std::cerr << "Failed to parse " << filename << ": " << status.message() | |
| << std::endl; | |
| return -1; | |
| } | |
| } | |
| webrtc::AnalyzerConfig config; | |
| config.window_duration_ = | |
| webrtc::TimeDelta::Millis(absl::GetFlag(FLAGS_averaging_window)); | |
| config.step_ = webrtc::TimeDelta::Millis(absl::GetFlag(FLAGS_averaging_step)); | |
| if (!parsed_log.start_log_events().empty()) { | |
| config.rtc_to_utc_offset_ = parsed_log.start_log_events()[0].utc_time() - | |
| parsed_log.start_log_events()[0].log_time(); | |
| } | |
| config.normalize_time_ = absl::GetFlag(FLAGS_normalize_time); | |
| config.begin_time_ = parsed_log.first_timestamp(); | |
| config.end_time_ = parsed_log.last_timestamp(); | |
| if (config.end_time_ < config.begin_time_) { | |
| RTC_LOG(LS_WARNING) << "Log end time " << config.end_time_ | |
| << " not after begin time " << config.begin_time_ | |
| << ". Nothing to analyze. Is the log broken?"; | |
| return -1; | |
| } | |
| std::string wav_path; | |
| bool has_generated_wav_file = false; | |
| if (!absl::GetFlag(FLAGS_wav_filename).empty()) { | |
| wav_path = absl::GetFlag(FLAGS_wav_filename); | |
| } else { | |
| // TODO(bugs.webrtc.org/14248): Remove the need to generate a file | |
| // and read the file directly from memory. | |
| wav_path = std::tmpnam(nullptr); | |
| std::ofstream out_wav_file(wav_path); | |
| out_wav_file.write( | |
| reinterpret_cast<char*>(&webrtc::conversational_speech_en_wav[0]), | |
| webrtc::conversational_speech_en_wav_len); | |
| has_generated_wav_file = true; | |
| } | |
| webrtc::EventLogAnalyzer analyzer(env, parsed_log, config); | |
| analyzer.InitializeMapOfNamedGraphs(absl::GetFlag(FLAGS_show_detector_state), | |
| absl::GetFlag(FLAGS_show_alr_state), | |
| absl::GetFlag(FLAGS_show_link_capacity)); | |
| // Flag replacements | |
| std::map<std::string, std::vector<std::string>> flag_aliases = { | |
| {"default", | |
| {"incoming_delay", "incoming_loss_rate", "incoming_bitrate", | |
| "outgoing_bitrate", "incoming_stream_bitrate", | |
| "outgoing_stream_bitrate", "network_delay_feedback", | |
| "fraction_loss_feedback"}}, | |
| {"sendside_bwe", | |
| {"outgoing_packet_sizes", "outgoing_bitrate", "outgoing_stream_bitrate", | |
| "simulated_sendside_bwe", "network_delay_feedback", | |
| "fraction_loss_feedback", "outgoing_twcc_loss"}}, | |
| {"receiveside_bwe", | |
| {"incoming_packet_sizes", "incoming_delay", "incoming_loss_rate", | |
| "incoming_bitrate", "incoming_stream_bitrate", | |
| "simulated_receiveside_bwe"}}, | |
| {"rtcp_details", | |
| {"incoming_rtcp_fraction_lost", "outgoing_rtcp_fraction_lost", | |
| "incoming_rtcp_cumulative_lost", "outgoing_rtcp_cumulative_lost", | |
| "incoming_rtcp_highest_seq_number", "outgoing_rtcp_highest_seq_number", | |
| "incoming_rtcp_delay_since_last_sr", | |
| "outgoing_rtcp_delay_since_last_sr"}}, | |
| {"simulated_neteq_stats", | |
| {"simulated_neteq_jitter_buffer_delay", | |
| "simulated_neteq_preferred_buffer_size", | |
| "simulated_neteq_concealment_events", "simulated_neteq_preemptive_rate", | |
| "simulated_neteq_accelerate_rate", "simulated_neteq_speech_expand_rate", | |
| "simulated_neteq_expand_rate"}}, | |
| {"l4s", | |
| {"incoming_bitrate", "outgoing_bitrate", "incoming_ecn_feedback", | |
| "outgoing_ecn_feedback"}}, | |
| {"scream", | |
| {"scream_delay_estimates", "scream_ref_window", | |
| "simulated_scream_bitrates", "simulated_scream_ref_window", | |
| "simulated_scream_ratios", "network_delay_feedback", "pacer_delay"}}}; | |
| if (absl::GetFlag(FLAGS_list_plots)) { | |
| std::cerr << "List of registered plots (for use with the --plot flag):" | |
| << std::endl; | |
| for (const auto& plot_name : analyzer.GetGraphNames()) { | |
| // TODO(terelius): Also print a help text. | |
| std::cerr << " " << plot_name; | |
| } | |
| // The following flags don't fit the model used for the other plots. | |
| for (const auto& plot_name : flag_aliases["simulated_neteq_stats"]) { | |
| std::cerr << " " << plot_name; | |
| } | |
| std::cerr << std::endl; | |
| std::cerr << "List of plot aliases (for use with the --plot flag):" | |
| << std::endl; | |
| std::cerr << " all = every registered plot" << std::endl; | |
| for (const auto& alias : flag_aliases) { | |
| std::cerr << " " << alias.first << " = "; | |
| for (const auto& replacement : alias.second) { | |
| std::cerr << replacement << ","; | |
| } | |
| std::cerr << std::endl; | |
| } | |
| return 0; | |
| } | |
| if (args.size() != 2) { | |
| // Print usage information. | |
| std::cerr << absl::ProgramUsageMessage(); | |
| return 1; | |
| } | |
| // Select which plots to output | |
| std::vector<std::string> plot_flags = | |
| StrSplit(absl::GetFlag(FLAGS_plot), ","); | |
| std::vector<std::string> plot_names; | |
| const std::vector<std::string> known_analyzer_plots = | |
| analyzer.GetGraphNames(); | |
| const std::vector<std::string> known_neteq_plots = | |
| flag_aliases["simulated_neteq_stats"]; | |
| std::vector<std::string> all_known_plots = known_analyzer_plots; | |
| all_known_plots.insert(all_known_plots.end(), known_neteq_plots.begin(), | |
| known_neteq_plots.end()); | |
| for (const std::string& flag : plot_flags) { | |
| if (flag == "all") { | |
| plot_names = all_known_plots; | |
| break; | |
| } | |
| auto alias_it = flag_aliases.find(flag); | |
| if (alias_it != flag_aliases.end()) { | |
| for (std::string& replacement : alias_it->second) { | |
| if (!KnownPlotName(replacement, all_known_plots)) { | |
| std::cerr << "Unknown plot name \"" << replacement << "\"" | |
| << std::endl; | |
| return 1; | |
| } | |
| plot_names.push_back(replacement); | |
| } | |
| } else { | |
| plot_names.push_back(flag); | |
| if (!KnownPlotName(flag, all_known_plots)) { | |
| std::cerr << "Unknown plot name \"" << flag << "\"" << std::endl; | |
| return 1; | |
| } | |
| } | |
| } | |
| webrtc::PlotCollection collection; | |
| analyzer.CreateGraphsByName(plot_names, &collection); | |
| // The simulated neteq charts are treated separately because they have a | |
| // different behavior compared to all other plots. In particular, the neteq | |
| // plots | |
| // * cache the simulation results between different plots | |
| // * open and read files | |
| // * dont have a 1-to-1 mapping between IDs and charts. | |
| std::optional<webrtc::NetEqStatsGetterMap> neteq_stats; | |
| if (absl::c_find(plot_names, "simulated_neteq_expand_rate") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| webrtc::CreateNetEqNetworkStatsGraph( | |
| parsed_log, config, *neteq_stats, | |
| [](const webrtc::NetEqNetworkStatistics& stats) { | |
| return stats.expand_rate / 16384.f; | |
| }, | |
| "Expand rate", collection.AppendNewPlot("simulated_neteq_expand_rate")); | |
| } | |
| if (absl::c_find(plot_names, "simulated_neteq_speech_expand_rate") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| webrtc::CreateNetEqNetworkStatsGraph( | |
| parsed_log, config, *neteq_stats, | |
| [](const webrtc::NetEqNetworkStatistics& stats) { | |
| return stats.speech_expand_rate / 16384.f; | |
| }, | |
| "Speech expand rate", | |
| collection.AppendNewPlot("simulated_neteq_speech_expand_rate")); | |
| } | |
| if (absl::c_find(plot_names, "simulated_neteq_accelerate_rate") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| webrtc::CreateNetEqNetworkStatsGraph( | |
| parsed_log, config, *neteq_stats, | |
| [](const webrtc::NetEqNetworkStatistics& stats) { | |
| return stats.accelerate_rate / 16384.f; | |
| }, | |
| "Accelerate rate", | |
| collection.AppendNewPlot("simulated_neteq_accelerate_rate")); | |
| } | |
| if (absl::c_find(plot_names, "simulated_neteq_preemptive_rate") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| webrtc::CreateNetEqNetworkStatsGraph( | |
| parsed_log, config, *neteq_stats, | |
| [](const webrtc::NetEqNetworkStatistics& stats) { | |
| return stats.preemptive_rate / 16384.f; | |
| }, | |
| "Preemptive rate", | |
| collection.AppendNewPlot("simulated_neteq_preemptive_rate")); | |
| } | |
| if (absl::c_find(plot_names, "simulated_neteq_concealment_events") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| webrtc::CreateNetEqLifetimeStatsGraph( | |
| parsed_log, config, *neteq_stats, | |
| [](const webrtc::NetEqLifetimeStatistics& stats) { | |
| return static_cast<float>(stats.concealment_events); | |
| }, | |
| "Concealment events", | |
| collection.AppendNewPlot("simulated_neteq_concealment_events")); | |
| } | |
| if (absl::c_find(plot_names, "simulated_neteq_preferred_buffer_size") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| webrtc::CreateNetEqNetworkStatsGraph( | |
| parsed_log, config, *neteq_stats, | |
| [](const webrtc::NetEqNetworkStatistics& stats) { | |
| return stats.preferred_buffer_size_ms; | |
| }, | |
| "Preferred buffer size (ms)", | |
| collection.AppendNewPlot("simulated_neteq_preferred_buffer_size")); | |
| } | |
| // The model we use for registering plots assumes that the each plot label | |
| // can be mapped to a lambda that will produce exactly one plot. The | |
| // simulated_neteq_jitter_buffer_delay plot doesn't fit this model since it | |
| // creates multiple plots, and would need some state kept between the lambda | |
| // calls. | |
| if (absl::c_find(plot_names, "simulated_neteq_jitter_buffer_delay") != | |
| plot_names.end()) { | |
| if (!neteq_stats) { | |
| neteq_stats = webrtc::SimulateNetEq(parsed_log, config, wav_path, 48000, | |
| field_trials); | |
| } | |
| for (auto it = neteq_stats->cbegin(); it != neteq_stats->cend(); ++it) { | |
| webrtc::CreateAudioJitterBufferGraph( | |
| parsed_log, config, it->first, it->second.get(), | |
| collection.AppendNewPlot("simulated_neteq_jitter_buffer_delay")); | |
| } | |
| } | |
| collection.SetCallTimeToUtcOffsetMs(config.CallTimeToUtcOffsetMs()); | |
| if (absl::GetFlag(FLAGS_protobuf_output)) { | |
| webrtc::analytics::ChartCollection proto_charts; | |
| collection.ExportProtobuf(&proto_charts); | |
| std::cout << proto_charts.SerializeAsString(); | |
| } else { | |
| collection.PrintPythonCode(absl::GetFlag(FLAGS_shared_xaxis), | |
| absl::GetFlag(FLAGS_show_grid), | |
| absl::GetFlag(FLAGS_figure_output_path)); | |
| } | |
| if (absl::GetFlag(FLAGS_print_triage_alerts)) { | |
| webrtc::TriageHelper triage_alerts(config); | |
| triage_alerts.AnalyzeLog(parsed_log); | |
| triage_alerts.Print(stderr); | |
| } | |
| // TODO(bugs.webrtc.org/14248): Remove the need to generate a file | |
| // and read the file directly from memory. | |
| if (has_generated_wav_file) { | |
| RTC_CHECK_EQ(std::remove(wav_path.c_str()), 0) | |
| << "Failed to remove " << wav_path; | |
| } | |
| return 0; | |
| } |