diff --git a/.gitignore b/.gitignore index da14eed..8078693 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.txt *.json -*.dot \ No newline at end of file +*.dot +*.sqlite diff --git a/Cargo.lock b/Cargo.lock index 8d25a0f..1294e09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,8 +16,11 @@ dependencies = [ "derive_more", "env_logger", "log", + "postcard", + "rusqlite", "serde", "serde_json", + "serde_qs", "thiserror", "walkdir", ] @@ -111,6 +114,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -184,12 +196,19 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" -version = "1.2.17" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -283,6 +302,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -341,6 +369,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "csscolorparser" version = "0.8.2" @@ -381,6 +415,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "env_filter" version = "0.1.3" @@ -414,12 +460,36 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "gimli" version = "0.28.1" @@ -432,6 +502,47 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -515,9 +626,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" dependencies = [ "once_cell", "wasm-bindgen", @@ -551,6 +662,25 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libsqlite3-sys" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -619,6 +749,12 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "phf" version = "0.13.1" @@ -670,6 +806,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "portable-atomic" version = "1.11.0" @@ -685,6 +827,19 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "prettyplease" version = "0.2.31" @@ -742,6 +897,31 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -784,6 +964,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -822,6 +1008,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037" +dependencies = [ + "itoa", + "percent-encoding", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -843,6 +1041,39 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -970,6 +1201,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -988,35 +1225,22 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1024,22 +1248,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index c74fc8f..2c4c342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,9 @@ env_logger = "0.11.5" clap-verbosity-flag = "3.0.4" colorgrad = "0.8.0" serde_json = "1.0.133" - +postcard = { version = "1.1.3", features = [ "alloc" ] } +rusqlite = "0.38.0" +serde_qs = "1.0.0" [workspace] members = ["bt2-derive", "bt2-sys"] diff --git a/README.md b/README.md index b3ac056..086bd92 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,15 @@ Record traces of your ROS application: Then you can use `Ros2TraceAnalyzer` subcommands to obtain various information from the trace. - - + ``` Usage: Ros2TraceAnalyzer [OPTIONS] Commands: - analyze Analyze a ROS 2 trace and generate graphs, JSON or bundle outputs + analyze Analyze a ROS 2 trace and store the result either as a binary bundle or separate files. See the extract subcommand for how to work with the binary bundle chart Render a chart of a specific property of a ROS 2 interface viewer Start a .dot viewer capable of generating charts on demand + extract Retrieve data from binary bundle produced by the analysis help Print this message or the help of the given subcommand(s) Options: @@ -74,8 +74,9 @@ Options: ## Analyze This command analyzes the traces and saves relevant information for later use into JSON, TXT and DOT files. + ``` -Analyze a ROS 2 trace and generate graphs, JSON or bundle outputs +Analyze a ROS 2 trace and store the result either as a binary bundle or separate files. See the extract subcommand for how to work with the binary bundle Usage: Ros2TraceAnalyzer analyze [OPTIONS] ... @@ -126,6 +127,11 @@ Options: --spin-duration[=] Analyze the duration of executor spins + --binary-bundle [] + File path of the binary bundle output + + [default: r2ta_results.sqlite] + -o, --out-dir Directory to write output files @@ -133,6 +139,9 @@ Options: When analysis output filename is specified and it is not an absolute path, it is resolved relative to `OUT_DIR`. + --legacy-output + Store the results into multiple files rather than to the binary bundle + --quantiles Quantiles to compute for the latency and duration analysis. @@ -169,7 +178,6 @@ Options: -h, --help Print help (see a summary with '-h') - ``` To gain **overview of timing in your application**, generate a @@ -268,10 +276,11 @@ Thread 1737158 on steelpick has utilization 2.10334 % ## Chart This command is reserved for later use. It is intended for generating charts from analyzed traces. + ``` Render a chart of a specific property of a ROS 2 interface -Usage: Ros2TraceAnalyzer chart [OPTIONS] --node --value +Usage: Ros2TraceAnalyzer chart [OPTIONS] --element-id --value Commands: histogram @@ -279,22 +288,22 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -n, --node - Full name of the node to draw the chart for - - The name should include the namespace and node's name + -e, --element-id + Identifies the element in the dependency graph for which to generate the chart -v, --verbose... Increase logging verbosity - -i, --input-path - The input path, either a file of the data or a folder containing the default named file with the necessary data + -i, --input + Path to the r2ta_results.sqlite file from which to retreive the data + + [default: r2ta_results.sqlite] -q, --quiet... Decrease logging verbosity - -o, --output-path - The output path, either a folder to which the file will be generated or a file to write into + -o, --output + Store the chart data to the given file -c, --clean Indicates whether the chart should be rendered from scratch. @@ -312,7 +321,9 @@ Options: - messages-latency: Latency of a communication channel --size - The size of the rendered image in pixels + The rectangular size of the rendered image in pixels + + - For PNG this directly translates to pixels - For SVG this is the size in pixels with scale 1.0 [default: 800] @@ -328,6 +339,8 @@ Options: ## Viewer This command is reserved for later use. Builtin .dot graphs viewer. + + ``` Start a .dot viewer capable of generating charts on demand @@ -351,6 +364,27 @@ Options: Print help ``` +## Extract +This command retrieves various data from the "binary bundle" produced by the analysis subcommand. + + +``` +Retrieve data from binary bundle produced by the analysis + +Usage: Ros2TraceAnalyzer extract [OPTIONS] + +Commands: + graph Extract dependency graph + property Extract property values for a node + help Print this message or the help of the given subcommand(s) + +Options: + -i, --input Path to the results file or directory containing r2ta_results.sqlite [default: r2ta_results.sqlite] + -v, --verbose... Increase logging verbosity + -o, --output File to extract the data to, if not present the data is written to stdout + -q, --quiet... Decrease logging verbosity + -h, --help Print help +``` [`ros2trace`]: https://index.ros.org/p/ros2trace/ [xdot.py]: https://github.com/jrfonseca/xdot.py diff --git a/src/analyses/analysis/dependency_graph.rs b/src/analyses/analysis/dependency_graph.rs index 6169bb8..1cb03cb 100644 --- a/src/analyses/analysis/dependency_graph.rs +++ b/src/analyses/analysis/dependency_graph.rs @@ -4,13 +4,15 @@ use std::sync::{Arc, Mutex}; use crate::analysis::utils::DisplayDurationStats; use crate::events_common::Context; +use crate::extract::{RosChannelCompleteName, RosInterfaceCompleteName}; +use crate::model::display::get_node_name_from_weak; use crate::model::{ self, Callback, CallbackCaller, CallbackInstance, CallbackTrigger, Publisher, Service, Subscriber, Time, Timer, }; use crate::processed_events::{Event, FullEvent, r2r, ros2}; use crate::statistics::Sorted; -use crate::utils::{DisplayDuration, Known}; +use crate::utils::{DisplayDuration, Known, WeakKnown}; use crate::visualization::COLOR_GRADIENT; use crate::visualization::graphviz_export::{self, NodeShape}; @@ -169,13 +171,8 @@ impl DependencyGraph { Self::default() } - pub fn display_as_dot( - &self, - color: bool, - thickness: bool, - min_multiplier: f64, - ) -> DisplayAsDot { - DisplayAsDot::new(self, color, thickness, min_multiplier) + pub fn to_dot_graph(&self, color: bool, thickness: bool, min_multiplier: f64) -> DotGraph { + DotGraph::new(self, color, thickness, min_multiplier) } } @@ -488,6 +485,233 @@ impl DependencyGraph { } } +impl DependencyGraph { + pub fn activation_delays(&self, node_ids: &HashMap) -> Vec { + let timers = self.timer_nodes.iter().map(|(k, v)| { + let id = node_ids[&Node::Timer(k.clone())]; + let n = k.0.lock().unwrap(); + ActivationDelayExport { + id, + name: RosInterfaceCompleteName { + interface: format!("Timer({})", n.get_period().unwrap_or(0)), + node: n + .get_node() + .map_or(WeakKnown::Unknown, |node_weak| { + get_node_name_from_weak(&node_weak.get_weak()) + }) + .unwrap_or(String::new()), + }, + activation_delays: v.activation_delay.clone(), + } + }); + + let callbacks = self.callback_nodes.iter().map(|(k, v)| { + let id = node_ids[&Node::Callback(k.clone())]; + let n = k.0.lock().unwrap(); + ActivationDelayExport { + id, + name: RosInterfaceCompleteName { + interface: format!( + "Callback({})", + match n.get_caller() { + Some(c) => match c { + CallbackCaller::Subscription(arc_weak) => format!( + "Subscriber(\"{}\")", + arc_weak.get_arc().unwrap().lock().unwrap().get_topic() + ), + v => v.to_string(), + }, + None => String::new(), + } + ), + node: n + .get_node() + .map_or(WeakKnown::Unknown, |node_weak| { + get_node_name_from_weak(&node_weak.get_weak()) + }) + .unwrap_or(String::new()), + }, + activation_delays: v.activation_delay.clone(), + } + }); + + timers.chain(callbacks).collect() + } + + pub fn publication_delays( + &self, + node_ids: &HashMap, + ) -> Vec { + self.publisher_nodes + .iter() + .map(|(k, v)| { + let id = node_ids[&Node::Publisher(k.clone())]; + let n = k.0.lock().unwrap(); + + PublicationDelayExport { + id, + name: RosInterfaceCompleteName { + interface: format!("Publisher({})", n.get_topic()), + node: n + .get_node() + .map_or(WeakKnown::Unknown, |node_weak| { + get_node_name_from_weak(&node_weak.get_weak()) + }) + .unwrap_or(String::new()), + }, + publication_delays: v.publication_delay.clone(), + } + }) + .collect() + } + + pub fn message_delays(&self, node_ids: &HashMap) -> Vec { + self.subscriber_nodes + .iter() + .map(|(k, v)| { + let id = node_ids[&Node::Subscriber(k.clone())]; + let n = k.0.lock().unwrap(); + + MessagesDelayExport { + id, + name: RosInterfaceCompleteName { + interface: format!("Subscriber({})", n.get_topic()), + node: n + .get_node() + .map_or(WeakKnown::Unknown, |node_weak| { + get_node_name_from_weak(&node_weak.get_weak()) + }) + .unwrap_or(String::new()), + }, + messages_delays: v.take_delay.clone(), + } + }) + .collect() + } + + pub fn callback_durations( + &self, + node_ids: &HashMap, + ) -> Vec { + self.callback_nodes + .iter() + .map(|(k, v)| { + let id = node_ids[&Node::Callback(k.clone())]; + let c = k.0.lock().unwrap(); + + CallbackDurationExport { + id, + name: RosInterfaceCompleteName { + interface: format!( + "Callback({})", + c.get_caller().map(ToString::to_string).unwrap_or_default() + ), + node: c + .get_node() + .map_or(WeakKnown::Unknown, |node_weak| { + get_node_name_from_weak(&node_weak.get_weak()) + }) + .unwrap_or(String::new()), + }, + callback_durations: v.durations.clone(), + } + }) + .collect() + } + + pub fn message_latencies( + &self, + node_ids: &HashMap, + edge_ids: &HashMap<(usize, usize), usize>, + ) -> Vec { + self.edges + .iter() + .map(|(k, v)| { + let source = get_node_name_from_graph_node(&k.source()); + let dest = get_node_name_from_graph_node(&k.target()); + + let topic = match k.source() { + Node::Publisher(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_topic().to_string() + } + Node::Subscriber(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_topic().to_string() + } + Node::Service(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_name().to_string() + } + Node::Timer(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_period().to_string() + } + _ => match k.target() { + Node::Publisher(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_topic().to_string() + } + Node::Subscriber(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_topic().to_string() + } + Node::Service(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_name().to_string() + } + Node::Timer(arc_mut_wrapper) => { + arc_mut_wrapper.0.lock().unwrap().get_period().to_string() + } + _ => String::new(), + }, + }; + + let from = node_ids[&k.source()]; + let to = node_ids[&k.target()]; + + MessageLatencyExport { + id: edge_ids[&(from, to)], + name: RosChannelCompleteName { + source_node: source, + destination_node: dest, + topic, + }, + messages_latencies: v.latencies.clone(), + } + }) + .collect() + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct ActivationDelayExport { + pub id: usize, + pub name: RosInterfaceCompleteName, + pub activation_delays: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct PublicationDelayExport { + pub id: usize, + pub name: RosInterfaceCompleteName, + pub publication_delays: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct MessagesDelayExport { + pub id: usize, + pub name: RosInterfaceCompleteName, + pub messages_delays: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct CallbackDurationExport { + pub id: usize, + pub name: RosInterfaceCompleteName, + pub callback_durations: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct MessageLatencyExport { + pub id: usize, + pub name: RosChannelCompleteName, + pub messages_latencies: Vec, +} + impl EventAnalysis for DependencyGraph { fn initialize(&mut self) { *self = Self::default(); @@ -598,31 +822,26 @@ struct DisplayAsDotEdge { edge_type: EdgeType, } -pub struct DisplayAsDot<'a> { +pub struct DotGraph { graph_node_to_ros_node: HashMap>, node_to_id: HashMap, ros_nodes: Vec>, ros_node_to_id: HashMap, usize>, ros_nodes_min_max_latency_stats: HashMap, EdgeWeightStats>, + edge_ids: HashMap<(usize, usize), usize>, + edges: Vec, pub_sub_latency_range: Option<(i64, i64)>, - analysis: &'a DependencyGraph, - // Cli Arguments color: bool, thickness: bool, min_multiplier: f64, } -impl<'a> DisplayAsDot<'a> { - pub fn new( - graph: &'a DependencyGraph, - color: bool, - thickness: bool, - min_multiplier: f64, - ) -> Self { +impl DotGraph { + pub fn new(graph: &DependencyGraph, color: bool, thickness: bool, min_multiplier: f64) -> Self { let mut graph_node_to_ros_node: HashMap> = HashMap::new(); let mut node_to_id = HashMap::new(); @@ -685,12 +904,13 @@ impl<'a> DisplayAsDot<'a> { .map(|(id, ros_node)| (ros_node.clone(), id)) .collect::>(); - let (edges, ros_nodes_min_max_latency_stats, pub_sub_latency_range) = process_edges( - &graph.edges, - &graph_node_to_ros_node, - &ros_node_to_id, - &node_to_id, - ); + let (edges, ros_nodes_min_max_latency_stats, pub_sub_latency_range, edge_ids) = + process_edges( + &graph.edges, + &graph_node_to_ros_node, + &ros_node_to_id, + &node_to_id, + ); Self { graph_node_to_ros_node, @@ -699,13 +919,21 @@ impl<'a> DisplayAsDot<'a> { node_to_id, ros_nodes_min_max_latency_stats, edges, - analysis: graph, + edge_ids, pub_sub_latency_range, color, thickness, min_multiplier, } } + + pub fn node_ids(&self) -> &HashMap { + &self.node_to_id + } + + pub fn edge_ids(&self) -> &HashMap<(usize, usize), usize> { + &self.edge_ids + } } fn process_edges( @@ -717,11 +945,15 @@ fn process_edges( Vec, HashMap, EdgeWeightStats>, Option<(i64, i64)>, + HashMap<(usize, usize), usize>, ) { let (mut pub_sub_min_latency, mut pub_sub_max_latency) = (i64::MAX, i64::MIN); let mut edges = Vec::new(); let mut ros_nodes_min_max_latency_stats: HashMap, EdgeWeightStats> = HashMap::new(); + let mut edge_ids = HashMap::new(); + + let mut edge_id = 1; for (edge, edge_data) in graph_edges { let latencies = Sorted::from_unsorted(&edge_data.latencies); @@ -769,6 +1001,9 @@ fn process_edges( let source_id = node_to_id[&source]; let target_id = node_to_id[&target]; + edge_ids.insert((source_id, target_id), edge_id); + edge_id += 1; + edges.push(DisplayAsDotEdge { source: source_id, target: target_id, @@ -788,49 +1023,34 @@ fn process_edges( edges, ros_nodes_min_max_latency_stats, pub_sub_latency_range, + edge_ids, ) } -fn get_node_name_and_tooltip( - node: &Node, - analysis: &DependencyGraph, - ros_node_name: Known<&str>, -) -> (String, String) { +fn get_node_name_and_tooltip(node: &Node, ros_node_name: Known<&str>) -> (String, String) { match node { Node::Publisher(publisher_arc) => { let publisher = publisher_arc.0.lock().unwrap(); let topic = publisher.get_topic().to_string(); let name = format!("Publisher\n{topic}"); - let tooltip = format!( - "Node: {ros_node_name}\nDelay between publications:\n{}", - DisplayDurationStats::with_newline( - &analysis.publisher_nodes[publisher_arc].publication_delay - ) - ); + let tooltip = String::new(); + (name, tooltip) } Node::Subscriber(subscriber_arc) => { let subscriber = subscriber_arc.0.lock().unwrap(); let topic = subscriber.get_topic().to_string(); let name = format!("Subscriber\n{topic}"); - let tooltip = format!( - "Node: {ros_node_name}\nDelay between messages:\n{}", - DisplayDurationStats::with_newline( - &analysis.subscriber_nodes[subscriber_arc].take_delay - ) - ); + let tooltip = String::new(); + (name, tooltip) } Node::Timer(timer_arc) => { let timer = timer_arc.0.lock().unwrap(); let period = timer.get_period().unwrap(); let name = format!("Timer\n{}", DisplayDuration(period)); - let tooltip = format!( - "Node: {ros_node_name}\nDelay between activations:\n{}", - DisplayDurationStats::with_newline( - &analysis.timer_nodes[timer_arc].activation_delay - ) - ); + let tooltip = String::new(); + (name, tooltip) } Node::Callback(callback_arc) => { @@ -839,15 +1059,8 @@ fn get_node_name_and_tooltip( "Callback\n{}", Known::<&CallbackCaller>::from(callback.get_caller()) ); - let tooltip = format!( - "Node: {ros_node_name}\nDelay between activations:\n{}\nExecution duration:\n{}", - DisplayDurationStats::with_newline( - &analysis.callback_nodes[callback_arc].activation_delay - ), - DisplayDurationStats::with_newline( - &analysis.callback_nodes[callback_arc].durations - ) - ); + let tooltip = String::new(); + (name, tooltip) } Node::Service(service_arc) => { @@ -859,7 +1072,23 @@ fn get_node_name_and_tooltip( } } -impl std::fmt::Display for DisplayAsDot<'_> { +fn get_node_name_from_graph_node(node: &Node) -> String { + let x = match node { + Node::Publisher(arc_mut_wrapper) => arc_mut_wrapper.0.lock().unwrap().get_node(), + Node::Subscriber(arc_mut_wrapper) => arc_mut_wrapper.0.lock().unwrap().get_node(), + Node::Service(arc_mut_wrapper) => arc_mut_wrapper.0.lock().unwrap().get_node(), + Node::Timer(arc_mut_wrapper) => arc_mut_wrapper.0.lock().unwrap().get_node(), + Node::Callback(arc_mut_wrapper) => arc_mut_wrapper.0.lock().unwrap().get_node().into(), + }; + + match x { + Known::Known(c) => get_node_name_from_weak(&c.get_weak()), + Known::Unknown => WeakKnown::Unknown, + } + .to_string() +} + +impl std::fmt::Display for DotGraph { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let cluster_names = self .ros_nodes @@ -888,8 +1117,7 @@ impl std::fmt::Display for DisplayAsDot<'_> { .get_full_name() .map(ToString::to_string) }); - let (node_name, tooltip) = - get_node_name_and_tooltip(node, self.analysis, ros_node_name.as_deref()); + let (node_name, tooltip) = get_node_name_and_tooltip(node, ros_node_name.as_deref()); let graph_node = graph.add_node(&node_name, *id); graph_node.set_shape(NodeShape::Ellipse); diff --git a/src/analyses/mod.rs b/src/analyses/mod.rs index 687867a..235b85e 100644 --- a/src/analyses/mod.rs +++ b/src/analyses/mod.rs @@ -6,6 +6,7 @@ use color_eyre::eyre::Context; use crate::analyses::analysis::AnalysisOutputExt; use crate::analyses::event_iterator::get_buf_writer_for_path; use crate::argsv2::analysis_args::AnalysisArgs; +use crate::utils::binary_sql_store::BinarySqlStore; pub mod analysis; pub mod event_iterator; @@ -94,86 +95,108 @@ impl Analyses { } pub fn save_output(&self, args: &AnalysisArgs) -> color_eyre::eyre::Result<()> { - if let Some(path) = args.message_latency_path() { - let analysis = self.message_latency_analysis.as_ref().unwrap(); - analysis.write_json_to_output_dir(&path)?; - } - - if let Some(path) = args.callback_duration_path() { - let analysis = self.callback_analysis.as_ref().unwrap(); - analysis.write_json_to_output_dir(&path)?; - } - - if let Some(path) = args.callback_publications_path() { - let analysis = self.callback_dependency_analysis.as_ref().unwrap(); - let analysis = analysis.get_publication_in_callback_analysis(); - let mut writer = get_buf_writer_for_path(&path)?; - analysis - .write_stats(&mut writer) - .wrap_err("Failed to write publication in callback stats")?; - - // TODO: Implement JSON output - // analysis.write_json_to_output_dir(&path, None)?; - } - - if let Some(path) = args.callback_dependency_path() { - let analysis = self.callback_dependency_analysis.as_ref().unwrap(); - - let graph = analysis.get_graph().unwrap(); - let mut writer = get_buf_writer_for_path(&path)?; - writer - .write_fmt(format_args!("{}", graph.as_dot())) - .wrap_err("Failed to write dependency graph")?; - } - - if let Some(path) = args.message_take_to_callback_latency_path() { - let analysis = self.message_take_to_callback_analysis.as_ref().unwrap(); - analysis - .write_json_to_output_dir(&path) - .wrap_err("Failed to write message take to callback latency stats")?; - } - - if let Some(path) = args.utilization_path() { - let analysis = self.callback_analysis.as_ref().unwrap(); - let utilization = analysis::Utilization::new(analysis); - - let mut writer = get_buf_writer_for_path(&path)?; - utilization - .write_stats(&mut writer, args.utilization_quantile()) - .wrap_err("Failed to write utilization stats")?; - - // TODO: Implement JSON output - // utilization.write_json_to_output_dir(&path, None)?; - } - - if let Some(path) = args.real_utilization_path() { - let analysis = self.callback_analysis.as_ref().unwrap(); - let utilization = analysis::Utilization::new(analysis); - - let mut writer = get_buf_writer_for_path(&path)?; - utilization - .write_stats_real(&mut writer) - .wrap_err("Failed to write real utilization stats")?; - - // TODO: Implement JSON output - // utilization.write_json_to_output_dir(&path)?; - } - - if let Some(path) = args.spin_duration_path() { - let analysis = self.spin_duration_analysis.as_ref().unwrap(); - analysis - .write_json_to_output_dir(&path) - .wrap_err("Failed to write spin duration stats")?; - } - - if let Some(path) = args.dependency_graph_path() { - let analysis = self.dependency_graph.as_ref().unwrap(); - let dot_output = - analysis.display_as_dot(args.color(), args.thickness(), args.min_multiplier()); - let mut writer = get_buf_writer_for_path(&path)?; - writer - .write_fmt(format_args!("{dot_output}")) - .wrap_err("Failed to write dependency graph")?; + if args.bundle_output() + && let Some(path) = args.binary_bundle_path() + { + if let Some(graph_analysis) = &self.dependency_graph { + let mut store = BinarySqlStore::new(&path)?; + + let dot_graph = graph_analysis.to_dot_graph(false, false, 1.0); + + store.insert(&[crate::utils::binary_sql_store::DependencyGraph { + graph: dot_graph.to_string(), + }])?; + + store.insert( + &graph_analysis.message_latencies(dot_graph.node_ids(), dot_graph.edge_ids()), + )?; + store.insert(&graph_analysis.activation_delays(dot_graph.node_ids()))?; + store.insert(&graph_analysis.publication_delays(dot_graph.node_ids()))?; + store.insert(&graph_analysis.callback_durations(dot_graph.node_ids()))?; + store.insert(&graph_analysis.message_delays(dot_graph.node_ids()))?; + } + } else { + if let Some(path) = args.dependency_graph_path() { + let analysis = self.dependency_graph.as_ref().unwrap(); + let dot_output = + analysis.to_dot_graph(args.color(), args.thickness(), args.min_multiplier()); + let mut writer = get_buf_writer_for_path(&path)?; + writer + .write_fmt(format_args!("{dot_output}")) + .wrap_err("Failed to write dependency graph")?; + } + + if let Some(path) = args.callback_dependency_path() { + let analysis = self.callback_dependency_analysis.as_ref().unwrap(); + + let graph = analysis.get_graph().unwrap(); + let mut writer = get_buf_writer_for_path(&path)?; + writer + .write_fmt(format_args!("{}", graph.as_dot())) + .wrap_err("Failed to write callback graph")?; + } + + if let Some(path) = args.message_latency_path() { + let analysis = self.message_latency_analysis.as_ref().unwrap(); + analysis.write_json_to_output_dir(&path)?; + } + + if let Some(path) = args.callback_duration_path() { + let analysis = self.callback_analysis.as_ref().unwrap(); + analysis.write_json_to_output_dir(&path)?; + } + + if let Some(path) = args.message_take_to_callback_latency_path() { + let analysis = self.message_take_to_callback_analysis.as_ref().unwrap(); + analysis + .write_json_to_output_dir(&path) + .wrap_err("Failed to write message take to callback latency stats")?; + } + + if let Some(path) = args.spin_duration_path() { + let analysis = self.spin_duration_analysis.as_ref().unwrap(); + analysis + .write_json_to_output_dir(&path) + .wrap_err("Failed to write spin duration stats")?; + } + + if let Some(path) = args.callback_publications_path() { + let analysis = self.callback_dependency_analysis.as_ref().unwrap(); + let analysis = analysis.get_publication_in_callback_analysis(); + let mut writer = get_buf_writer_for_path(&path)?; + analysis + .write_stats(&mut writer) + .wrap_err("Failed to write publication in callback stats")?; + + // TODO: Implement JSON output + // analysis.write_json_to_output_dir(&path, None)?; + } + + if let Some(path) = args.utilization_path() { + let analysis = self.callback_analysis.as_ref().unwrap(); + let utilization = analysis::Utilization::new(analysis); + + let mut writer = get_buf_writer_for_path(&path)?; + utilization + .write_stats(&mut writer, args.utilization_quantile()) + .wrap_err("Failed to write utilization stats")?; + + // TODO: Implement JSON output + // utilization.write_json_to_output_dir(&path, None)?; + } + + if let Some(path) = args.real_utilization_path() { + let analysis = self.callback_analysis.as_ref().unwrap(); + let utilization = analysis::Utilization::new(analysis); + + let mut writer = get_buf_writer_for_path(&path)?; + utilization + .write_stats_real(&mut writer) + .wrap_err("Failed to write real utilization stats")?; + + // TODO: Implement JSON output + // utilization.write_json_to_output_dir(&path)?; + } } Ok(()) diff --git a/src/argsv2/analysis_args.rs b/src/argsv2/analysis_args.rs index 2d66d44..5a64de8 100644 --- a/src/argsv2/analysis_args.rs +++ b/src/argsv2/analysis_args.rs @@ -7,7 +7,7 @@ use clap::{Parser, ValueHint}; use crate::statistics::Quantile; -mod filenames { +pub(super) mod filenames { pub const DEPENDENCY_GRAPH: &str = "dependency_graph.dot"; pub const MESSAGE_LATENCY: &str = "message_latency.json"; pub const CALLBACK_DURATION: &str = "callback_duration.json"; @@ -17,6 +17,8 @@ mod filenames { pub const UTILIZATION: &str = "utilization.txt"; pub const REAL_UTILIZATION: &str = "real_utilization.txt"; pub const SPIN_DURATION: &str = "spin_duration.json"; + + pub const BINARY_BUNDLE: &str = "r2ta_results.sqlite"; } #[derive(Debug, Clone, Parser)] @@ -78,6 +80,10 @@ pub struct AnalysisArgs { #[arg(long, value_name = "FILENAME", default_missing_value = filenames::SPIN_DURATION, num_args = 0..=1, require_equals = true, default_value_if("all", "true", filenames::SPIN_DURATION))] spin_duration: Option, + /// File path of the binary bundle output + #[arg(long, value_name = "FILENAME", default_value = filenames::BINARY_BUNDLE, num_args = 0..=1)] + binary_bundle: Option, + /// Directory to write output files /// /// If not provided, the current working directory is used. @@ -86,6 +92,10 @@ pub struct AnalysisArgs { #[arg(long, short = 'o', value_hint = ValueHint::DirPath)] out_dir: Option, + /// Store the results into multiple files rather than to the binary bundle + #[arg(long)] + legacy_output: bool, + /// Quantiles to compute for the latency and duration analysis. /// /// The quantiles must be in the range [0, 1]. @@ -196,60 +206,70 @@ impl AnalysisArgs { self.spin_duration.is_some() } - pub fn dependency_graph_path(&self) -> Option> { + pub fn dependency_graph_path(&self) -> Option> { self.dependency_graph .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn message_latency_path(&self) -> Option> { + pub fn message_latency_path(&self) -> Option> { self.message_latency .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn callback_duration_path(&self) -> Option> { + pub fn callback_duration_path(&self) -> Option> { self.callback_duration .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn callback_publications_path(&self) -> Option> { + pub fn callback_publications_path(&self) -> Option> { self.callback_publications .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn callback_dependency_path(&self) -> Option> { + pub fn callback_dependency_path(&self) -> Option> { self.callback_dependency .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn message_take_to_callback_latency_path(&self) -> Option> { + pub fn message_take_to_callback_latency_path(&self) -> Option> { self.message_take_to_callback_latency .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn utilization_path(&self) -> Option> { + pub fn utilization_path(&self) -> Option> { self.utilization .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn real_utilization_path(&self) -> Option> { + pub fn real_utilization_path(&self) -> Option> { self.real_utilization .as_ref() .map(|p| self.concatenate_with_out_path(p)) } - pub fn spin_duration_path(&self) -> Option> { + pub fn spin_duration_path(&self) -> Option> { self.spin_duration .as_ref() .map(|p| self.concatenate_with_out_path(p)) } + pub fn binary_bundle_path(&self) -> Option> { + self.binary_bundle + .as_ref() + .map(|p| self.concatenate_with_out_path(p)) + } + + pub fn bundle_output(&self) -> bool { + !self.legacy_output + } + pub fn quantiles(&self) -> &[Quantile] { &self.quantiles } diff --git a/src/argsv2/chart_args.rs b/src/argsv2/chart_args.rs index fa7407e..3b1bf52 100644 --- a/src/argsv2/chart_args.rs +++ b/src/argsv2/chart_args.rs @@ -2,21 +2,22 @@ use clap::{Args, Subcommand, ValueEnum, ValueHint}; use derive_more::Display; use std::path::{Path, PathBuf}; +use crate::argsv2::analysis_args; + #[derive(Debug, Clone, Args)] pub struct ChartArgs { - /// Full name of the node to draw the chart for - /// - /// The name should include the namespace and node's name - #[clap(long, short = 'n')] - node: String, + /// Identifies the element in the dependency graph for + /// which to generate the chart + #[clap(long, short = 'e')] + element_id: i64, - /// The input path, either a file of the data or a folder containing the default named file with the necessary data - #[clap(long, short = 'i', value_name = "INPUT", value_hint = ValueHint::AnyPath)] - input_path: Option, + /// Path to the r2ta_results.sqlite file from which to retreive the data + #[clap(long, short = 'i', value_name = "FILENAME", value_hint = ValueHint::FilePath, default_value = analysis_args::filenames::BINARY_BUNDLE)] + input: Option, - /// The output path, either a folder to which the file will be generated or a file to write into - #[clap(long, short = 'o', value_name = "OUTPUT", value_hint = ValueHint::AnyPath)] - output_path: Option, + /// Store the chart data to the given file + #[clap(long, short = 'o', value_name = "FILENAME", value_hint = ValueHint::AnyPath)] + output: Option, /// Indicates whether the chart should be rendered from scratch. /// @@ -29,16 +30,16 @@ pub struct ChartArgs { } impl ChartArgs { - pub fn node(&self) -> &str { - &self.node + pub fn element_id(&self) -> i64 { + self.element_id } pub fn input_path(&self) -> Option<&Path> { - self.input_path.as_deref() + self.input.as_deref() } pub fn output_path(&self) -> Option<&Path> { - self.output_path.as_deref() + self.output.as_deref() } pub fn clean(&self) -> bool { @@ -77,9 +78,9 @@ pub struct ChartRequest { pub enum ChartOutputFormat { #[default] #[display("svg")] - SVG, + Svg, #[display("png")] - PNG, + Png, } #[derive(Debug, Display, ValueEnum, Clone, Copy)] diff --git a/src/argsv2/extract_args.rs b/src/argsv2/extract_args.rs new file mode 100644 index 0000000..a691f83 --- /dev/null +++ b/src/argsv2/extract_args.rs @@ -0,0 +1,111 @@ +use std::path::{Path, PathBuf}; + +use clap::{Args, Subcommand, ValueEnum, ValueHint}; +use derive_more::Display; + +use crate::argsv2::analysis_args::filenames; + +#[derive(Debug, Clone, Args)] +pub struct ExtractArgs { + /// Path to the results file or directory containing r2ta_results.sqlite + #[clap(long, short = 'i', value_name = "FILENAME", value_hint = ValueHint::FilePath, default_value = super::analysis_args::filenames::BINARY_BUNDLE)] + input: Option, + + /// File to extract the data to, if not present the data is written to stdout + #[clap(long, short = 'o', value_name = "FILENAME", value_hint = ValueHint::FilePath)] + output: Option, + + #[clap(subcommand)] + extract_content: ExtractContentArgs, +} + +impl ExtractArgs { + pub fn input_path(&self) -> PathBuf { + match &self.input { + Some(p) => { + if p.is_dir() { + p.join(filenames::BINARY_BUNDLE) + } else { + p.clone() + } + } + None => std::env::current_dir() + .unwrap() + .join(filenames::BINARY_BUNDLE), + } + } + + pub fn output_path(&self) -> Option<&Path> { + self.output.as_deref() + } + + pub fn content(&self) -> &ExtractContentArgs { + &self.extract_content + } +} + +#[derive(Debug, Clone, Subcommand)] +pub enum ExtractContentArgs { + /// Extract dependency graph + Graph, + /// Extract property values for a node + Property(ExtractPropertyArgs), +} + +#[derive(Debug, Clone, Args)] +pub struct ExtractPropertyArgs { + /// Identifies the element in the dependency graph for + /// which to extract the data + #[clap(long)] + element_id: i64, + + /// The property to extract from the node. + #[clap(long)] + property: AnalysisProperty, +} + +impl ExtractPropertyArgs { + pub fn element_id(&self) -> i64 { + self.element_id + } + + pub fn property(&self) -> &AnalysisProperty { + &self.property + } +} + +#[derive(Debug, Display, ValueEnum, Clone, PartialEq, Eq, Hash)] +pub enum AnalysisProperty { + /// Callback execution durations + #[display("Callback execution time")] + CallbackDurations, + + /// Delays between callback or timer activations + #[display("Delays between activations")] + ActivationDelays, + + /// Delays between publisher publications + #[display("Delay between publication")] + PublicationDelays, + + /// Delays between subscriber messages + #[display("Delay between")] + MessageDelays, + + /// Latency of a communication channel + #[display("Message latency")] + MessageLatencies, +} + +impl AnalysisProperty { + /// Table name in the binary data blob for this property + pub const fn table_name(&self) -> &'static str { + match self { + AnalysisProperty::CallbackDurations => "callback_duration", + AnalysisProperty::ActivationDelays => "activations_delay", + AnalysisProperty::PublicationDelays => "publications_delay", + AnalysisProperty::MessageDelays => "messages_delay", + AnalysisProperty::MessageLatencies => "messages_latency", + } + } +} diff --git a/src/argsv2/mod.rs b/src/argsv2/mod.rs index 2303d8c..547b149 100644 --- a/src/argsv2/mod.rs +++ b/src/argsv2/mod.rs @@ -5,6 +5,7 @@ use clap_verbosity_flag::{Verbosity, WarnLevel}; pub mod analysis_args; pub mod chart_args; +pub mod extract_args; pub mod helpers; pub mod viewer_args; @@ -38,7 +39,7 @@ impl Args { pub fn into_analysis_args(self) -> analysis_args::AnalysisArgs { match self.command { - TracerCommand::Analyze(analysis_args) => analysis_args, + TracerCommand::Analyze(analysis_args) => *analysis_args, _ => { panic!( "Tried to extract Analysis arguments subcommand but {} subcommand was used", @@ -51,9 +52,11 @@ impl Args { #[derive(Debug, Subcommand, Clone, derive_more::Display)] pub enum TracerCommand { - /// Analyze a ROS 2 trace and generate graphs, JSON or bundle outputs + /// Analyze a ROS 2 trace and store the result either as a binary bundle + /// or separate files. See the extract subcommand for how to work with the + /// binary bundle. #[display("analyze")] - Analyze(analysis_args::AnalysisArgs), + Analyze(Box), /// Render a chart of a specific property of a ROS 2 interface #[display("chart")] @@ -62,6 +65,10 @@ pub enum TracerCommand { /// Start a .dot viewer capable of generating charts on demand #[display("viewer")] Viewer(viewer_args::ViewerArgs), + + /// Retrieve data from binary bundle produced by the analysis + #[display("extract")] + Extract(#[clap(subcommand)] extract_args::ExtractArgs), } #[cfg(test)] diff --git a/src/extract/mod.rs b/src/extract/mod.rs new file mode 100644 index 0000000..60383d2 --- /dev/null +++ b/src/extract/mod.rs @@ -0,0 +1,99 @@ +use std::io::Write; +use std::path::Path; + +use derive_more::Display; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::analyses::analysis::dependency_graph::{ + ActivationDelayExport, CallbackDurationExport, MessageLatencyExport, MessagesDelayExport, + PublicationDelayExport, +}; +use crate::argsv2::extract_args::AnalysisProperty; +use crate::utils::binary_sql_store::{BinarySQLStoreError, BinarySqlStore}; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Display, Debug)] +#[display("{node}::{interface}")] +pub struct RosInterfaceCompleteName { + pub interface: String, + pub node: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Display, Debug)] +#[display("{source_node}-({topic})>{destination_node}")] +pub struct RosChannelCompleteName { + pub source_node: String, + pub destination_node: String, + pub topic: String, +} + +pub enum ChartableData { + I64(Vec), +} + +#[derive(Error, Debug)] +pub enum DataExtractionError { + #[error("An error occurred during data parsing\n{0}")] + SourceDataParseError(BinarySQLStoreError), +} + +pub fn extract_graph(input: &Path) -> color_eyre::eyre::Result { + let store = BinarySqlStore::open(input)?; + + Ok(store.get_dependency_graph()?.graph) +} + +pub fn extract_property( + input: &Path, + element_id: i64, + property: &AnalysisProperty, +) -> color_eyre::eyre::Result { + let store = BinarySqlStore::open(input)?; + + let element_id = element_id as usize; + + Ok(match property { + AnalysisProperty::CallbackDurations => ChartableData::I64( + store + .get_by_id::(element_id) + .map_err(DataExtractionError::SourceDataParseError)? + .callback_durations, + ), + AnalysisProperty::ActivationDelays => ChartableData::I64( + store + .get_by_id::(element_id) + .map_err(DataExtractionError::SourceDataParseError)? + .activation_delays, + ), + AnalysisProperty::PublicationDelays => ChartableData::I64( + store + .get_by_id::(element_id) + .map_err(DataExtractionError::SourceDataParseError)? + .publication_delays, + ), + AnalysisProperty::MessageDelays => ChartableData::I64( + store + .get_by_id::(element_id) + .map_err(DataExtractionError::SourceDataParseError)? + .messages_delays, + ), + AnalysisProperty::MessageLatencies => ChartableData::I64( + store + .get_by_id::(element_id) + .map_err(DataExtractionError::SourceDataParseError)? + .messages_latencies, + ), + }) +} + +impl ChartableData { + pub fn export(&self, output: &mut impl Write) -> color_eyre::eyre::Result<()> { + let data = match self { + ChartableData::I64(items) => serde_json::to_string(&items)?, + }; + + writeln!(output, "{data}")?; + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 53050db..16c7e3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod analyses; mod argsv2; mod events_common; +mod extract; mod model; mod processed_events; mod processor; @@ -12,12 +13,14 @@ mod utils; mod visualization; use std::ffi::CString; +use std::io::Write; use argsv2::Args; use argsv2::helpers::prepare_trace_paths; use crate::argsv2::analysis_args::AnalysisArgs; use crate::argsv2::chart_args::ChartArgs; +use crate::argsv2::extract_args::ExtractArgs; use crate::argsv2::viewer_args::ViewerArgs; use analyses::analysis; @@ -40,11 +43,34 @@ fn run_analysis( Ok(()) } -fn run_charting(args: &ChartArgs) -> color_eyre::eyre::Result<()> { +fn run_charting(_args: &ChartArgs) -> color_eyre::eyre::Result<()> { Ok(()) } -fn run_viewer(args: &ViewerArgs) -> color_eyre::eyre::Result<()> { +fn run_viewer(_args: &ViewerArgs) -> color_eyre::eyre::Result<()> { + Ok(()) +} + +fn run_extract(args: &ExtractArgs) -> color_eyre::eyre::Result<()> { + let source_file = args.input_path(); + let mut output: Box = args + .output_path() + .map(|p| std::fs::File::create(p).map(|f| Box::new(f) as Box)) + .unwrap_or_else(|| Ok(Box::new(std::io::stdout()) as Box))?; + + match args.content() { + argsv2::extract_args::ExtractContentArgs::Graph => { + let graph = extract::extract_graph(&source_file)?; + + writeln!(output, "{graph}")?; + } + argsv2::extract_args::ExtractContentArgs::Property(args) => { + let data = extract::extract_property(&source_file, args.element_id(), args.property())?; + + data.export(&mut output)?; + } + } + Ok(()) } @@ -58,10 +84,9 @@ fn main() -> color_eyre::eyre::Result<()> { let args = Args::get(); match &args.command { - argsv2::TracerCommand::Analyze(analysis_args) => { - run_analysis(&analysis_args, &args.verbose) - } - argsv2::TracerCommand::Chart(chart_args) => run_charting(&chart_args), - argsv2::TracerCommand::Viewer(viewer_args) => run_viewer(&viewer_args), + argsv2::TracerCommand::Analyze(analysis_args) => run_analysis(analysis_args, &args.verbose), + argsv2::TracerCommand::Chart(chart_args) => run_charting(chart_args), + argsv2::TracerCommand::Viewer(viewer_args) => run_viewer(viewer_args), + argsv2::TracerCommand::Extract(extract_args) => run_extract(extract_args), } } diff --git a/src/utils/binary_sql_store.rs b/src/utils/binary_sql_store.rs new file mode 100644 index 0000000..c92bf33 --- /dev/null +++ b/src/utils/binary_sql_store.rs @@ -0,0 +1,390 @@ +use crate::analyses::analysis::dependency_graph::{ + ActivationDelayExport, CallbackDurationExport, MessageLatencyExport, MessagesDelayExport, + PublicationDelayExport, +}; +use crate::extract::{RosChannelCompleteName, RosInterfaceCompleteName}; + +#[derive(thiserror::Error, std::fmt::Debug)] +pub enum BinarySQLStoreError { + #[error("An error occured in rusqlite {0}")] + SQLiteError(rusqlite::Error), + #[error("The provided file path does not point to an existing file {0}")] + NoStore(std::path::PathBuf), +} + +pub struct BinarySqlStore { + connection: rusqlite::Connection, +} + +impl BinarySqlStore { + pub fn open(sqlite_path: &std::path::Path) -> Result { + if !sqlite_path.exists() { + return Err(BinarySQLStoreError::NoStore(sqlite_path.to_path_buf())); + } + + let sqlite_connection = + rusqlite::Connection::open(sqlite_path).map_err(BinarySQLStoreError::SQLiteError)?; + + let store: Self = Self { + connection: sqlite_connection, + }; + + let meta = store.get_by_id::(0)?; + + if meta.version != Self::VERSION { + log::warn!( + "Mismatched file version, expected: {}, got: {}", + Self::VERSION, + meta.version + ); + } + + Ok(store) + } + + pub fn new(sqlite_path: &std::path::Path) -> Result { + if sqlite_path.exists() { + std::fs::remove_file(sqlite_path); + } + + let sqlite_connection = + rusqlite::Connection::open(sqlite_path).map_err(BinarySQLStoreError::SQLiteError)?; + + let mut store = BinarySqlStore { + connection: sqlite_connection, + }; + + store.insert(&[Metadata { + version: Self::VERSION, + }])?; + + Ok(store) + } + + pub fn insert(&mut self, values: &[T]) -> Result<(), BinarySQLStoreError> { + self.connection + .execute( + &format!( + "CREATE TABLE IF NOT EXISTS {} ({})", + T::table(), + T::params() + .iter() + .map(|p| format!("{} {}", p.0, p.1)) + .collect::>() + .join(", "), + ), + (), + ) + .map_err(BinarySQLStoreError::SQLiteError)?; + + let tx = self + .connection + .transaction() + .map_err(BinarySQLStoreError::SQLiteError)?; + + { + let mut query = tx + .prepare_cached(&format!( + "INSERT INTO {} ({}) VALUES ({})", + T::table(), + T::params() + .iter() + .map(|p| p.0) + .collect::>() + .join(", "), + (1..) + .take(T::params().len()) + .map(|v| format!("?{v}")) + .collect::>() + .join(", ") + )) + .map_err(BinarySQLStoreError::SQLiteError)?; + + for entry in values { + query + .execute(entry.to_params()) + .map_err(BinarySQLStoreError::SQLiteError)?; + } + } + + tx.commit().map_err(BinarySQLStoreError::SQLiteError)?; + + Ok(()) + } + + pub fn get_by_id(&self, id: usize) -> Result { + self.connection + .query_row( + &format!( + "SELECT {} FROM {} WHERE id = ?1", + T::params() + .iter() + .map(|p| p.0) + .collect::>() + .join(", "), + T::table() + ), + (id as i64,), + |r| T::from_row(r), + ) + .map_err(BinarySQLStoreError::SQLiteError) + } +} + +impl BinarySqlStore { + pub const VERSION: usize = 1; + + pub fn get_dependency_graph(&self) -> Result { + // dependency graph has id 0 as defined in its Entity impl + self.get_by_id(0) + } +} + +pub trait Entity: Sized { + /// Name of the table for this entity + fn table<'a>() -> &'a str; + + /// Parse this entity from named row + fn from_row(row: &rusqlite::Row) -> Result; + + /// List of parameters this entity has with their type and constraints + /// + /// The format is this: [("", "")]. An example would be + /// [("id", "INT PRIMARY KEY")] + fn params<'a>() -> &'a [(&'a str, &'a str)]; + + /// Convert the entity to parameters for insertion + /// + /// the parameter order must be the same as given by the `params` method + fn to_params(&self) -> impl rusqlite::Params; +} + +impl Entity for MessageLatencyExport { + fn table<'a>() -> &'a str { + "message_latencies" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(MessageLatencyExport { + id: row.get::<_, i64>("id")? as usize, + name: RosChannelCompleteName { + source_node: row.get("source_node")?, + destination_node: row.get("destination_node")?, + topic: row.get("topic")?, + }, + messages_latencies: postcard::from_bytes(&row.get::<_, Vec<_>>("latencies")?).unwrap(), + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[ + ("id", "INT PRIMARY KEY"), + ("source_node", "TEXT"), + ("destination_node", "TEXT"), + ("topic", "TEXT"), + ("latencies", "BLOB"), + ] + } + + fn to_params(&self) -> impl rusqlite::Params { + ( + self.id as i64, + &self.name.source_node, + &self.name.destination_node, + &self.name.topic, + postcard::to_allocvec(&self.messages_latencies).unwrap(), + ) + } +} + +impl Entity for MessagesDelayExport { + fn table<'a>() -> &'a str { + "message_delays" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(MessagesDelayExport { + id: row.get::<_, i64>("id")? as usize, + name: RosInterfaceCompleteName { + interface: row.get("interface")?, + node: row.get("node")?, + }, + messages_delays: postcard::from_bytes(&row.get::<_, Vec<_>>("latencies")?).unwrap(), + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[ + ("id", "INT PRIMARY KEY"), + ("node", "TEXT"), + ("interface", "TEXT"), + ("delays", "BLOB"), + ] + } + + fn to_params(&self) -> impl rusqlite::Params { + ( + self.id as i64, + &self.name.node, + &self.name.interface, + postcard::to_allocvec(&self.messages_delays).unwrap(), + ) + } +} + +impl Entity for CallbackDurationExport { + fn table<'a>() -> &'a str { + "callback_durations" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(CallbackDurationExport { + id: row.get::<_, i64>("id")? as usize, + name: RosInterfaceCompleteName { + interface: row.get("interface")?, + node: row.get("node")?, + }, + callback_durations: postcard::from_bytes(&row.get::<_, Vec<_>>("durations")?).unwrap(), + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[ + ("id", "INT PRIMARY KEY"), + ("node", "TEXT"), + ("interface", "TEXT"), + ("durations", "BLOB"), + ] + } + + fn to_params(&self) -> impl rusqlite::Params { + ( + self.id as i64, + &self.name.node, + &self.name.interface, + postcard::to_allocvec(&self.callback_durations).unwrap(), + ) + } +} + +impl Entity for PublicationDelayExport { + fn table<'a>() -> &'a str { + "publication_delays" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(PublicationDelayExport { + id: row.get::<_, i64>("id")? as usize, + name: RosInterfaceCompleteName { + interface: row.get("interface")?, + node: row.get("node")?, + }, + publication_delays: postcard::from_bytes(&row.get::<_, Vec<_>>("delays")?).unwrap(), + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[ + ("id", "INT PRIMARY KEY"), + ("node", "TEXT"), + ("interface", "TEXT"), + ("delays", "BLOB"), + ] + } + + fn to_params(&self) -> impl rusqlite::Params { + ( + self.id as i64, + &self.name.node, + &self.name.interface, + postcard::to_allocvec(&self.publication_delays).unwrap(), + ) + } +} + +impl Entity for ActivationDelayExport { + fn table<'a>() -> &'a str { + "activation_delays" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(ActivationDelayExport { + id: row.get::<_, i64>("id")? as usize, + name: RosInterfaceCompleteName { + interface: row.get("interface")?, + node: row.get("node")?, + }, + activation_delays: postcard::from_bytes(&row.get::<_, Vec<_>>("delays")?).unwrap(), + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[ + ("id", "INT PRIMARY KEY"), + ("node", "TEXT"), + ("interface", "TEXT"), + ("delays", "BLOB"), + ] + } + + fn to_params(&self) -> impl rusqlite::Params { + ( + self.id as i64, + &self.name.node, + &self.name.interface, + postcard::to_allocvec(&self.activation_delays).unwrap(), + ) + } +} + +struct Metadata { + version: usize, +} + +impl Entity for Metadata { + fn table<'a>() -> &'a str { + "metadata" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(Metadata { + version: row.get::<_, i64>("version")? as usize, + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[("id", "INT PRIMARY KEY"), ("version", "INT")] + } + + fn to_params(&self) -> impl rusqlite::Params { + (0, self.version as i64) + } +} + +pub struct DependencyGraph { + pub graph: String, +} + +impl Entity for DependencyGraph { + fn table<'a>() -> &'a str { + "graphs" + } + + fn from_row(row: &rusqlite::Row) -> Result { + Ok(Self { + graph: row.get("graph")?, + }) + } + + fn params<'a>() -> &'a [(&'a str, &'a str)] { + &[ + ("id", "INT PRIMARY KEY"), + ("name", "TEXT"), + ("graph", "TEXT"), + ] + } + + fn to_params(&self) -> impl rusqlite::Params { + (0, "dependency_graph", &self.graph) + } +} diff --git a/src/utils.rs b/src/utils/mod.rs similarity index 99% rename from src/utils.rs rename to src/utils/mod.rs index 6719df8..866ac53 100644 --- a/src/utils.rs +++ b/src/utils/mod.rs @@ -3,6 +3,8 @@ use std::sync::{Arc, Mutex, Weak}; use derive_more::derive::From; +pub mod binary_sql_store; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] pub enum Known { Known(T),