Repository: hubblo-org/scaphandre (language: rust)

Issue:
Consistently reports 0 W for DRAM consumption
## Bug description

Scaphandre reports 0 W as the DRAM consumption.

## Expected behavior

I hoped scaphandre would include DRAM measurements.

## Screenshots

![Screenshot-2021-05-07T09:09:55](https://user-images.githubusercontent.com/1533747/117412359-00f28580-af15-11eb-8ce1-3866d8fa5652.png)

![Screenshot-2021-05-07T09:27:00](https://user-images.githubusercontent.com/1533747/117413587-71e66d00-af16-11eb-9040-1c58ec43860f.png)

## Environment

- **Linux distribution version**: Ubuntu 20.04.2
- **Kernel version**: 5.4.0-67-generic
- **CPU:** Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
- **Scaphandre version:** `a7b0159ce5135e2b25fe4c97a86434d30f6e0982` (self-built). Also tried pre-built docker image.

## Additional context

I realize that this may very well be the intended behavior, and not a bug (e.g. because the Intel RAPL sensor or powercap doesn't output RAM for this CPU make). I was mainly hoping to get clarifications into this - if it's something I will be able to fix (maybe I'm just operating scaphandre wrong or am missing something silly?), or if it's just the way it is. Thank you for having written this really useful tool!

Also, could it simply be because the memory usage is low, i.e. around 1GB used with 11GB buff/cached?

```bash
root@Ubuntu-2004-focal-64-minimal ~ # free -m
              total        used        free      shared  buff/cache   available
Mem:          31900        1381       19428         140       11090       29945
Swap:         16366           0       16366
```


Discussion / hints:

Fix patch:
diff --git a/src/exporters/json.rs b/src/exporters/json.rs
index a6a3ca32..9480ab05 100644
--- a/src/exporters/json.rs
+++ b/src/exporters/json.rs
@@ -1,3 +1,4 @@
+use crate::current_system_time_since_epoch;
 use crate::exporters::*;
 use crate::sensors::{Sensor, Topology};
 use clap::Arg;
@@ -22,6 +23,7 @@ impl Exporter for JSONExporter {
     }
 
     /// Returns options needed for that exporter, as a HashMap
+
     fn get_options() -> Vec<clap::Arg<'static, 'static>> {
         let mut options = Vec::new();
         let arg = Arg::with_name("timeout")
@@ -60,6 +62,15 @@ impl Exporter for JSONExporter {
             .takes_value(true);
         options.push(arg);
 
+        let arg = Arg::with_name("max_top_consumers")
+            .default_value("10")
+            .help("Maximum number of processes to watch.")
+            .long("max-top-consumers")
+            .short("m")
+            .required(false)
+            .takes_value(true);
+        options.push(arg);
+
         options
     }
 }
@@ -83,8 +94,13 @@ struct Consumer {
     consumption: f32,
 }
 #[derive(Serialize, Deserialize)]
+struct Host {
+    consumption: f32,
+    timestamp: f64,
+}
+#[derive(Serialize, Deserialize)]
 struct Report {
-    host: f32,
+    host: Host,
     consumers: Vec<Consumer>,
     sockets: Vec<Socket>,
 }
@@ -137,18 +153,28 @@ impl JSONExporter {
     }
 
     fn retrieve_metrics(&mut self, parameters: &ArgMatches) {
-        let host_power = self
-            .topology
-            .get_records_diff_power_microwatts()
-            .map(|record| record.value.parse::<u64>().unwrap())
-            .unwrap_or(0);
+        let mut host_power = 0;
+        let host_timestamp: Duration;
+        if let Some(microwatts_record) = self.topology.get_records_diff_power_microwatts() {
+            host_timestamp = microwatts_record.timestamp;
+
+            host_power = microwatts_record.value.parse::<u64>().unwrap();
+        } else {
+            host_timestamp = current_system_time_since_epoch();
+        }
 
         let host_stat = match self.topology.get_stats_diff() {
             Some(value) => value,
             None => return,
         };
 
-        let consumers = self.topology.proc_tracker.get_top_consumers(10);
+        let consumers = self.topology.proc_tracker.get_top_consumers(
+            parameters
+                .value_of("max_top_consumers")
+                .unwrap_or("10")
+                .parse::<u16>()
+                .unwrap(),
+        );
         let top_consumers = consumers
             .iter()
             .map(|(process, value)| {
@@ -163,7 +189,7 @@ impl JSONExporter {
             })
             .collect::<Vec<_>>();
 
-        let names = ["core", "uncore", "dram"];
+        // let names = ["core", "uncore", "dram"];
         let all_sockets = self
             .topology
             .get_sockets_passive()
@@ -177,14 +203,13 @@ impl JSONExporter {
                 let domains = socket
                     .get_domains_passive()
                     .iter()
-                    .map(|d| d.get_records_diff_power_microwatts())
-                    .map(|record| record.map(|d| d.value))
-                    .enumerate()
-                    .map(|(index, d)| {
+                    .map(|d| (d.name.clone(), d.get_records_diff_power_microwatts()))
+                    .map(|(n, record)| (n, record.map(|d| d.value)))
+                    .map(|(name, d)| {
                         let domain_power =
                             d.map(|value| value.parse::<u64>().unwrap()).unwrap_or(0);
                         Domain {
-                            name: names[index].to_string(),
+                            name,
                             consumption: domain_power as f32,
                         }
                     })
@@ -198,8 +223,13 @@ impl JSONExporter {
             })
             .collect::<Vec<_>>();
 
+        let host_report = Host {
+            consumption: host_power as f32,
+            timestamp: host_timestamp.as_secs_f64(),
+        };
+
         let report = Report {
-            host: host_power as f32,
+            host: host_report,
             consumers: top_consumers,
             sockets: all_sockets,
         };
diff --git a/src/exporters/stdout.rs b/src/exporters/stdout.rs
index e22efb38..8e32fd43 100644
--- a/src/exporters/stdout.rs
+++ b/src/exporters/stdout.rs
@@ -79,7 +79,8 @@ impl StdoutExporter {
         }
     }
 
-    fn get_domains_power(&self, socket_id: u16) -> Vec<Option<Record>> {
+    // Retuns the power for each domain in a socket.
+    fn get_domains_power(&self, socket_id: u16) -> HashMap<String, Option<Record>> {
         let socket_present = self
             .topology
             .get_sockets_passive()
@@ -87,13 +88,14 @@ impl StdoutExporter {
             .find(move |x| x.id == socket_id);
 
         if let Some(socket) = socket_present {
-            let mut domains_power: Vec<Option<Record>> = vec![];
+            // let mut domains_power: Vec<Option<Record>> = vec![];
+            let mut domains_power: HashMap<String, Option<Record>> = HashMap::new();
             for d in socket.get_domains_passive() {
-                domains_power.push(d.get_records_diff_power_microwatts());
+                domains_power.insert(d.name.clone(), d.get_records_diff_power_microwatts());
             }
             domains_power
         } else {
-            vec![None, None, None]
+            HashMap::new()
         }
     }
 
@@ -107,7 +109,8 @@ impl StdoutExporter {
             Some(record) => record.value.parse::<u64>().unwrap(),
             None => 0,
         };
-        let mut sockets_power: HashMap<u16, (u64, Vec<Option<Record>>)> = HashMap::new();
+        let mut sockets_power: HashMap<u16, (u64, HashMap<String, Option<Record>>)> =
+            HashMap::new();
         let sockets = self.topology.get_sockets_passive();
         for s in sockets {
             let socket_power = match s.get_records_diff_power_microwatts() {
@@ -116,26 +119,35 @@ impl StdoutExporter {
             };
             sockets_power.insert(s.id, (socket_power, self.get_domains_power(s.id)));
         }
-        println!(
-            "Host:\t{} W\tCore\t\tUncore\t\tDRAM",
-            (host_power as f32 / 1000000.0)
-        );
+        let domain_names = self.topology.domains_names.as_ref().unwrap();
+
+        println!("Host:\t{} W", (host_power as f32 / 1000000.0));
+        println!("\tpackage \t{}", domain_names.join("\t\t"));
+
         for (s_id, v) in sockets_power.iter() {
             let power = (v.0 as f32) / 1000000.0;
-            let mut power_str = String::from('?');
+            let mut power_str = String::from("----");
             if power > 0.0 {
                 power_str = power.to_string();
             }
+
             let mut to_print = format!("Socket{}\t{} W\t", s_id, power_str);
-            for d in v.1.iter() {
-                let domain_power = match d {
-                    Some(record) => record.value.parse::<u64>().unwrap(),
-                    None => 0,
-                };
-                to_print.push_str(&format!("{} W\t", domain_power as f32 / 1000000.0));
+
+            for domain in domain_names.iter() {
+                if let Some(Some(record)) = v.1.get(domain) {
+                    to_print.push_str(&format!(
+                        "{} W\t",
+                        record.value.parse::<u64>().unwrap() as f32 / 1000000.0
+                    ));
+                } else {
+                    // This should only happen when we don't have yet enough records,
+                    // as in a multi-sockets system, all sockets have the same domains.
+                    to_print.push_str("---- \t\t");
+                }
             }
             println!("{}", to_print);
         }
+
         println!("Top 5 consumers:");
         println!("Power\tPID\tExe");
 
diff --git a/src/sensors/mod.rs b/src/sensors/mod.rs
index 32fbcdcd..7750e5e2 100644
--- a/src/sensors/mod.rs
+++ b/src/sensors/mod.rs
@@ -41,6 +41,8 @@ pub struct Topology {
     pub record_buffer: Vec<Record>,
     /// Maximum size in memory for the recor_buffer
     pub buffer_max_kbytes: u16,
+    /// Sorted list of all domains names
+    pub domains_names: Option<Vec<String>>,
 }
 
 impl RecordGenerator for Topology {
@@ -128,6 +130,7 @@ impl Topology {
             stat_buffer: vec![],
             record_buffer: vec![],
             buffer_max_kbytes: 1,
+            domains_names: None,
         }
     }
 
@@ -196,6 +199,19 @@ impl Topology {
         &self.sockets
     }
 
+    // Build a sorted list of all domains names from all sockets.
+    fn build_domains_names(&mut self) {
+        let mut names: HashMap<String, ()> = HashMap::new();
+        for s in self.sockets.iter() {
+            for d in s.get_domains_passive() {
+                names.insert(d.name.clone(), ());
+            }
+        }
+        let mut domain_names = names.keys().cloned().collect::<Vec<String>>();
+        domain_names.sort();
+        self.domains_names = Some(domain_names);
+    }
+
     /// Adds a Domain instance to a given socket, if and only if the domain
     /// id doesn't exist already for the socket.
     pub fn safe_add_domain_to_socket(
@@ -217,6 +233,7 @@ impl Topology {
                 ));
             }
         }
+        self.build_domains_names();
     }
 
     /// Generates CPUCore instances for the host and adds them
@@ -643,7 +660,7 @@ impl CPUSocket {
         &mut self.domains
     }
 
-    /// Returns a mutable reference to the domains vector.
+    /// Returns a immutable reference to the domains vector.
     pub fn get_domains_passive(&self) -> &Vec<Domain> {
         &self.domains
     }


Test patch: