// License: Apache 2.0. See LICENSE file in root directory. // Copyright(c) 2021 Intel Corporation. All Rights Reserved. //#cmake:add-file ../../unit-tests-common.cpp //#cmake: static! //#test:donotrun //#test:device D435 //#test:timeout 480 #include #include #include #include "../../catch.h" #include "../../unit-tests-common.h" #include #include "./../src/environment.h" using namespace librealsense; constexpr int DELAY_INCREMENT_THRESHOLD = 4; //[%] constexpr int DELAY_INCREMENT_THRESHOLD_IMU = 8; //[%] constexpr int SPIKE_THRESHOLD = 2; //[stdev] constexpr int ITERATIONS_PER_CONFIG = 60; constexpr int INNER_ITERATIONS_PER_CONFIG = 10; // Input: vector that represent samples of delay to first frame of one stream // Output: - slope of the fitted line // reference: https://en.wikipedia.org/wiki/Least_squares double line_fitting(const std::vector& y_vec, std::vector y_fit = std::vector()) { double ysum = std::accumulate(y_vec.begin(), y_vec.end(), 0.0); //calculate sigma(yi) double xsum = 0; double x2sum = 0; double xysum = 0; double a = 1; auto y_vec_it = y_vec.begin(); size_t n = y_vec.size(); for (auto i = 0; i < n; i++) { xsum = xsum + i; //sigma(xi) x2sum = x2sum + pow(i, 2); //sigma(x^2i) xysum = xysum + i * *(y_vec_it + i); } a = (n * xysum - xsum * ysum) / (n * x2sum - xsum * xsum); //slope double b = (x2sum * ysum - xsum * xysum) / (x2sum * n - xsum * xsum); //intercept if (y_fit.size() > 0) { auto it = y_fit.begin(); for (auto i = 0; i < n; i++) { *(it + i) = a * i + b; } } return a; // slope is used when checking delay increment } // Input: vector that represent samples of time delay to first frame of one stream // Output: vector of filtered-out spikes void data_filter(const std::vector& stream_vec, std::vector& filtered_stream_vec) { std::vector y_fit(stream_vec.size()); double slope = line_fitting(stream_vec, y_fit); auto y_fit_it = y_fit.begin(); auto stream_vec_it = stream_vec.begin(); std::vector diff_y_fit; for (auto i = 0; i < stream_vec.size(); i++) { double diff = std::abs(*(y_fit_it + i) - *(stream_vec_it + i)); diff_y_fit.push_back(diff); } // calc stdev from fitted linear line double sq_sum = std::inner_product(diff_y_fit.begin(), diff_y_fit.end(), diff_y_fit.begin(), 0.0); double stdev = std::sqrt(sq_sum / diff_y_fit.size()); // calc % of each sample from stdev std::vector samples_stdev(diff_y_fit.size()); auto v_size = diff_y_fit.size(); std::transform(diff_y_fit.begin(), diff_y_fit.end(), samples_stdev.begin(), [stdev](double d) { auto val = d / stdev; return val; } ); // filter spikes auto samples_stdev_it = samples_stdev.begin(); stream_vec_it = stream_vec.begin(); for (auto i = 0; i < samples_stdev.size(); i++) { if (*(samples_stdev_it + i) > SPIKE_THRESHOLD) continue; filtered_stream_vec.push_back(*(stream_vec_it + i)); } } TEST_CASE("Extrinsic memory leak detection", "[live]") { // Require at least one device to be plugged in rs2::context ctx( "{\"dds\":false}" ); rs2::log_to_file(RS2_LOG_SEVERITY_DEBUG, "lrs_log.txt"); std::cout << "Extrinsic memory leak detection started" << std::endl; #ifdef LIGHT_TEST bool is_pipe_test[1] = { true }; #else bool is_pipe_test[2] = { true, false }; #endif for (auto is_pipe : is_pipe_test) { auto list = ctx.query_devices(); REQUIRE(list.size()); auto dev = list.front(); auto sensors = dev.query_sensors(); REQUIRE(dev.supports(RS2_CAMERA_INFO_PRODUCT_LINE)); std::string device_type = dev.get_info(RS2_CAMERA_INFO_PRODUCT_LINE); if (dev.supports(RS2_CAMERA_INFO_PRODUCT_LINE) && std::string(dev.get_info(RS2_CAMERA_INFO_PRODUCT_LINE)) == "D400") device_type = "D400"; bool usb3_device = is_usb3(dev); int fps = usb3_device ? 30 : 15; // In USB2 Mode the devices will switch to lower FPS rates int req_fps = usb3_device ? 60 : 30; // USB2 Mode has only a single resolution for 60 fps which is not sufficient to run the test int width = 848; int height = 480; auto res = configure_all_supported_streams(dev, width, height, fps); for (auto& s : res.first) { s.close(); } // collect a log that contains info about 20 iterations for each stream // the info should include: // 1. extrinsics table size // 2. delay to first frame // 3. delay threshold for each stream (set fps=6 delay as worst case) // the test will succeed only if all 3 conditions are met: // 1. extrinsics table size is preserved over iterations for each stream // 2. no delay increment over iterations // 3. "most" iterations have time to first frame delay below a defined threshold std::vector extrinsics_table_size; std::map> streams_delay; // map to vector to collect all data std::map>> unique_streams_delay; std::map new_frame; std::map extrinsic_graph_at_sensor; auto& b = environment::get_instance().get_extrinsics_graph(); if (is_pipe) { std::cout << "==============================================" << std::endl; std::cout << "Pipe Test is running .." << std::endl; } else { std::cout << "==============================================" << std::endl; std::cout << "Sensor Test is running .." << std::endl; } for (auto i = 0; i < ITERATIONS_PER_CONFIG; i++) { rs2::config cfg; rs2::pipeline pipe; size_t cfg_size = 0; std::vector valid_stream_profiles; std::map> sensor_stream_profiles; std::map> ds5_sensor_stream_map; for (auto& profile : res.second) { auto fps = profile.fps; if (device_type == "D400" && profile.stream == RS2_STREAM_ACCEL) fps = 250; cfg.enable_stream(profile.stream, profile.index, profile.width, profile.height, profile.format, fps); // all streams in cfg cfg_size += 1; // create stream profiles data structure to open streams per sensor when testing in sensor mode for (auto& s : res.first) { auto stream_profiles = s.get_stream_profiles(); for (auto& sp : stream_profiles) { if (!(sp.stream_type() == profile.stream && sp.fps() == fps && sp.stream_index() == profile.index && sp.format() == profile.format)) continue; if (sp.stream_type() == RS2_STREAM_ACCEL || sp.stream_type() == RS2_STREAM_GYRO) sensor_stream_profiles[2].push_back(sp); auto vid = sp.as(); auto h = vid.height(); auto w = vid.width(); if (!(w == profile.width && h == profile.height)) continue; if (sp.stream_type() == RS2_STREAM_DEPTH || sp.stream_type() == RS2_STREAM_INFRARED || sp.stream_type() == RS2_STREAM_CONFIDENCE) sensor_stream_profiles[0].push_back(sp); if (sp.stream_type() == RS2_STREAM_COLOR) sensor_stream_profiles[1].push_back(sp); } } } for (auto it = new_frame.begin(); it != new_frame.end(); it++) { it->second = 0; } auto start_time = std::chrono::system_clock::now().time_since_epoch(); auto start_time_milli = std::chrono::duration_cast(start_time).count(); bool condition = false; bool all_arrived = false; std::mutex mutex; std::condition_variable cv; auto process_frame = [&](const rs2::frame& f) { auto stream_type = f.get_profile().stream_name(); auto frame_num = f.get_frame_number(); auto time_of_arrival = f.get_frame_metadata(RS2_FRAME_METADATA_TIME_OF_ARRIVAL); if (!new_frame[stream_type]) { streams_delay[stream_type].push_back((double(time_of_arrival - start_time_milli))); new_frame[stream_type] = true; } new_frame[stream_type] += 1; if (new_frame.size() == cfg_size) { int completed = 0; for (auto it = new_frame.begin(); it != new_frame.end(); it++) { if (it->second >= INNER_ITERATIONS_PER_CONFIG) completed++; } // if all streams received more than 20 frames, stop waiting if (completed == cfg_size) { all_arrived = true; cv.notify_all(); } } }; auto frame_callback = [&](const rs2::frame& f) { std::lock_guard lock(mutex); if (rs2::frameset fs = f.as()) { // With callbacks, all synchronized stream will arrive in a single frameset for (const rs2::frame& ff : fs) { process_frame(ff); } } else { // Stream that bypass synchronization (such as IMU) will produce single frames process_frame(f); } }; if (is_pipe) { rs2::pipeline_profile profiles = pipe.start(cfg, frame_callback); } else { int i = 0; for (auto& s : res.first) { if (sensor_stream_profiles.find(i) == sensor_stream_profiles.end()) continue; s.open(sensor_stream_profiles[i]); i += 1; } i = 0; for (auto& s : res.first) { if (sensor_stream_profiles.find(i) == sensor_stream_profiles.end()) continue; s.start(frame_callback); i += 1; } } // to prevent FW issue, at least 20 frames per stream should arrive std::unique_lock lock(mutex); auto pred = [&](){ return all_arrived; }; REQUIRE(cv.wait_for(lock, std::chrono::seconds(5), pred)); if (new_frame.size() > cfg_size) std::cout << "The number of active streams :" << new_frame.size() << " doesn't match the number of total streams:" << cfg_size << std::endl; if (is_pipe) { pipe.stop(); } else { // Stop & flush all active sensors. The separation is intended to semi-confirm the FPS for (auto& s : res.first) { s.stop(); } for (auto& s : res.first) { s.close(); } } extrinsics_table_size.push_back(b._extrinsics.size()); } std::cout << "Analyzing info .. " << std::endl; // the test will succeed only if all 3 conditions are met: // 1. extrinsics table size is preserved over iterations for each stream // 2. no delay increment over iterations // 3. "most" iterations have time to first frame delay below a defined threshold if (extrinsics_table_size.size()) { CAPTURE(extrinsics_table_size); // 1. extrinsics table preserve its size over iterations CHECK(std::adjacent_find(extrinsics_table_size.begin(), extrinsics_table_size.end(), std::not_equal_to()) == extrinsics_table_size.end()); } // 2. no delay increment over iterations // filter spikes : calc stdev for each half and filter out samples that are not close for (auto& stream : streams_delay) { // make sure we have enough data for each stream REQUIRE(stream.second.size() > 10); // remove first 5 iterations from each stream stream.second.erase(stream.second.begin(), stream.second.begin() + 5); double slope = line_fitting(stream.second); // check slope value against threshold auto threshold = DELAY_INCREMENT_THRESHOLD; // IMU streams have different threshold if (stream.first == "Accel" || stream.first == "Gyro") threshold = DELAY_INCREMENT_THRESHOLD_IMU; CAPTURE(stream.first, slope, threshold); CAPTURE(stream.second); CHECK(slope < threshold); } // 3. "most" iterations have time to first frame delay below a defined threshold // REMOVED : this part is a duplication of : test-t2ff-pipeline.py and test-t2ff-sensor.py /*std::map delay_thresholds; // D400 delay_thresholds["Accel"] = 1200; // ms delay_thresholds["Color"] = 1200; // ms delay_thresholds["Depth"] = 1200; // ms delay_thresholds["Gyro"] = 1200; // ms delay_thresholds["Infrared 1"] = 1200; // ms delay_thresholds["Infrared 2"] = 1200; // ms for (const auto& stream_ : streams_delay) { std::vector filtered_stream_vec; data_filter(stream_.second, filtered_stream_vec); auto stream = stream_.first; double sum = std::accumulate(filtered_stream_vec.begin(), filtered_stream_vec.end(), 0.0); double avg = sum / filtered_stream_vec.size(); std::cout << "Delay of " << stream << " = " << avg * 1.5 << std::endl; CAPTURE(stream); for (auto it = stream_.second.begin(); it != stream_.second.end(); ++it) { CHECK(*it <= delay_thresholds[stream]); } }*/ } }