Combine historical data, provide accessor functions.
[virt-top.git] / virt-ctrl / vc_connections.ml
index 5525f5e..c99b2c4 100644 (file)
@@ -53,15 +53,52 @@ and connection = int (* connection ID *) * (active list * inactive list)
 and active = int (* domain's ID *)
 and inactive = string (* domain's name *)
 
-(* The last "CPU time" seen for a domain, so we can calculate CPU % usage.
- * Hash of (connid, domid) -> cpu_time [int64].
+(* Store the node_info and hostname for each connection, fetched
+ * once just after we connect since these don't normally change.
+ * Hash of connid -> (C.node_info, hostname option, uri)
  *)
-let last_cpu_time = Hashtbl.create 13
-let last_time = ref (Unix.gettimeofday ())
+let static_conn_info = Hashtbl.create 13
 
+(* Stores the state and history for each domain.
+ * Hash of (connid, domid) -> mutable domhistory structure.
+ * We never delete entries in this hash table, which may be a problem
+ * for very very long-lived instances of virt-ctrl.
+ *)
+type domhistory = {
+  (* for %CPU calculation: *)
+  mutable last_cpu_time : int64;       (* last virDomainInfo->cpuTime *)
+  mutable last_time : float;           (* exact time we measured the above *)
+
+  (* historical data for graphs etc: *)
+  mutable hist : dhentry array;                (* historical data *)
+  mutable hist_posn : int;             (* position within array *)
+}
+and dhentry = {
+  hist_cpu : int;                      (* historical %CPU entry *)
+  hist_mem : int64;                    (* historical memory entry (KB) *)
+}
+
+let domhistory = Hashtbl.create 13
+
+let empty_dhentry = {
+  hist_cpu = 0; hist_mem = 0L;
+}
+let new_domhistory () = {
+  last_cpu_time = 0L; last_time = 0.;
+  hist = Array.make 0 empty_dhentry; hist_posn = 0;
+}
+
+(* These set limits on the amount of history we collect. *)
+let hist_max = 86400                   (* max history stored, seconds *)
+let hist_rot = 3600                    (* rotation of array when we hit max *)
+
+(* The types of the display columns in the main window.  The interesting
+ * one of the final (int) field which stores the ID of the row, either
+ * connid or domid.
+ *)
 type columns = string GTree.column * string GTree.column * string GTree.column * string GTree.column * string GTree.column * int GTree.column
 
-let debug_repopulate = true
+let debug_repopulate = false
 
 (* Populate the tree with the current list of connections, domains.
  * This function is called once per second.
@@ -69,12 +106,6 @@ let debug_repopulate = true
 let repopulate (tree : GTree.view) (model : GTree.tree_store)
     (col_name_id, col_domname, col_status, col_cpu, col_mem, col_id)
     state =
-  let time_passed =
-    let time_now = Unix.gettimeofday () in
-    let time_passed = time_now -. !last_time in
-    last_time := time_now;
-    time_passed in
-
   (* Which connections have been added or removed? *)
   let conns = get_conns () in
   let added, _, removed =
@@ -97,14 +128,15 @@ let repopulate (tree : GTree.view) (model : GTree.tree_store)
   List.iter (
     fun conn_id ->
       let row = model#append () in
-      (* Get the connection name. *)
+      (* Get the connection name, usually the hostname. *)
       let name =
-       try C.get_hostname (List.assoc conn_id conns)
-       with Not_found | Libvirt.Virterror _ ->
-         "Conn #" ^ string_of_int conn_id in
+       match Hashtbl.find static_conn_info conn_id with
+       | (_, Some hostname, _) -> hostname
+       | (_, None, _) -> sprintf "Conn #%d" conn_id in
       model#set ~row ~column:col_name_id name;
       model#set ~row ~column:col_id conn_id;
-      (* XXX This doesn't work, why? *)
+      (* Expand the new row. *)
+      (* XXX This doesn't work, why? - Because we haven't create subrows yet.*)
       tree#expand_row (model#get_path row)
   ) added;
 
@@ -127,8 +159,8 @@ let repopulate (tree : GTree.view) (model : GTree.tree_store)
          with Not_found -> assert false (* Should never happen. *) in
 
        try
-         (* Node info & number of CPUs available. *)
-         let node_info = C.get_node_info conn in
+         (* Number of CPUs available. *)
+         let node_info, _, _ = Hashtbl.find static_conn_info conn_id in
          let nr_cpus = C.maxcpus_of_node_info node_info in
 
          (* For this connection, get a current list of active domains (IDs) *)
@@ -219,23 +251,78 @@ let repopulate (tree : GTree.view) (model : GTree.tree_store)
                  let memory = sprintf "%Ld K" info.D.memory in
                  model#set ~row ~column:col_mem memory;
 
-                 let ns_now = info.D.cpu_time in (* ns = nanoseconds *)
-                 let ns_prev =
-                   try
-                     let ns = Hashtbl.find last_cpu_time (conn_id, domid) in
-                     if ns > ns_now then 0L else ns (* Rebooted? *)
-                   with Not_found -> 0L in
-                 Hashtbl.replace last_cpu_time (conn_id, domid) ns_now;
-                 let ns_now = Int64.to_float ns_now in
-                 let ns_prev = Int64.to_float ns_prev in
-                 let ns_used = ns_now -. ns_prev in
-                 let ns_available = 1_000_000_000. *. float nr_cpus in
-                 let cpu_percent =
-                   100. *. (ns_used /. ns_available) /. time_passed in
-                 let cpu_percent = sprintf "%.1f %%" cpu_percent in
-                 model#set ~row ~column:col_cpu cpu_percent;
+                 (* Get domhistory.  For a new domain it won't exist, so
+                  * create an empty one.
+                  *)
+                 let dh =
+                   let key = conn_id, domid in
+                   try Hashtbl.find domhistory key
+                   with Not_found ->
+                     let dh = new_domhistory () in
+                     Hashtbl.add domhistory key dh;
+                     dh in
+
+                 (* Measure current time and domain cpuTime as close
+                  * together as possible.
+                  *)
+                 let time_now = Unix.gettimeofday () in
+                 let cpu_now = info.D.cpu_time in
+
+                 let time_prev = dh.last_time in
+                 let cpu_prev =
+                   if dh.last_cpu_time > cpu_now then 0L (* Rebooted? *)
+                   else dh.last_cpu_time in
+
+                 dh.last_time <- time_now;
+                 dh.last_cpu_time <- cpu_now;
 
-               with Libvirt.Virterror _ -> () (* Ignore any transient error *)
+                 let cpu_percent =
+                   if time_prev > 0. then (
+                     let cpu_now = Int64.to_float cpu_now in
+                     let cpu_prev = Int64.to_float cpu_prev in
+                     let cpu_used = cpu_now -. cpu_prev in
+                     let cpu_available = 1_000_000_000. *. float nr_cpus in
+                     let time_passed = time_now -. time_prev in
+
+                     let cpu_percent =
+                       100. *. (cpu_used /. cpu_available) /. time_passed in
+
+                     let cpu_percent =
+                       if cpu_percent < 0. then 0.
+                       else if cpu_percent > 100. then 100.
+                       else cpu_percent in
+
+                     let cpu_percent_str = sprintf "%.1f %%" cpu_percent in
+                     model#set ~row ~column:col_cpu cpu_percent_str;
+                     int_of_float cpu_percent
+                   ) else -1 in
+
+                 (* Store history. *)
+                 let datum = { hist_cpu = cpu_percent;
+                               hist_mem = info.D.memory } in
+
+                 if dh.hist_posn >= hist_max then (
+                   (* rotate the array *)
+                   Array.blit dh.hist hist_rot dh.hist 0 (hist_max-hist_rot);
+                   dh.hist_posn <- dh.hist_posn - hist_rot;
+                   dh.hist.(dh.hist_posn) <- datum;
+                 ) else (
+                   let len = Array.length dh.hist in
+                   if dh.hist_posn < len then
+                     (* normal update *)
+                     dh.hist.(dh.hist_posn) <- datum
+                   else (
+                     (* extend the array *)
+                     let len' = min (max (2*len) 1) hist_max in
+                     let arr' = Array.make len' datum in
+                     Array.blit dh.hist 0 arr' 0 len;
+                     dh.hist <- arr';
+                   )
+                 );
+                 dh.hist_posn <- dh.hist_posn+1
+
+               with
+                 Libvirt.Virterror _ -> () (* Ignore any transient error *)
              )
          ) (model#iter_children (Some parent));
 
@@ -314,13 +401,85 @@ let make_treeview ?packing () =
  *)
 let open_connection () =
   let title = "Open connection to hypervisor" in
-  let name =
+  let uri =
     GToolbox.input_string ~title ~text:"xen:///" ~ok:"Open" "Connection:" in
-  match name with
+  match uri with
   | None -> ()
-  | Some name ->
+  | Some uri ->
       (* If this fails, let the exception escape and be printed
        * in the global exception handler.
        *)
-      let conn = C.connect ~name () in
-      ignore (add_conn conn)
+      let conn = C.connect ~name:uri () in
+
+      let node_info = C.get_node_info conn in
+      let hostname =
+       try Some (C.get_hostname conn)
+       with
+       | Libvirt.Not_supported "virConnectGetHostname"
+       | Libvirt.Virterror _ -> None in
+
+      (* Add it to our list of connections. *)
+      let conn_id = add_conn conn in
+      Hashtbl.add static_conn_info conn_id (node_info, hostname, uri)
+
+(* Get historical data size. *)
+let get_hist_size connid domid =
+  try
+    let dh = Hashtbl.find domhistory (connid, domid) in
+    dh.hist_posn
+  with
+    Not_found -> 0
+
+(* Get historical data entries. *)
+let _get_hist ?(latest=0) ?earliest ?(granularity=1)
+    extract fold zero connid domid =
+  try
+    let dh = Hashtbl.find domhistory (connid, domid) in
+    let earliest =
+      match earliest with
+      | None -> dh.hist_posn
+      | Some e -> min e dh.hist_posn in
+
+    let src = dh.hist in
+    let src_start = dh.hist_posn - earliest in assert (src_start >= 0);
+    let src_end = dh.hist_posn - latest in     assert (src_end <= dh.hist_posn);
+
+    (* Create a sufficiently large array to store the result. *)
+    let len = (earliest-latest) / granularity in
+    let r = Array.make len zero in
+
+    if granularity = 1 then (
+      for j = 0 to len-1 do
+       r.(j) <- extract src.(src_start+j)
+      done
+    ) else (
+      let i = ref src_start in
+      for j = 0 to len-1 do
+       let sub = Array.sub src !i (min (!i+granularity) src_end - !i) in
+       let sub = Array.map extract sub in
+       r.(j) <- fold sub;
+       i := !i + granularity
+      done
+    );
+    r
+  with
+    Not_found -> [| |]
+
+let get_hist_cpu ?latest ?earliest ?granularity connid domid =
+  let zero = 0 in
+  let extract { hist_cpu = c } = c in
+  let fold a =
+    let len = Array.length a in
+    if len > 0 then Array.fold_left (+) zero a / len else -1 in
+  _get_hist ?latest ?earliest ?granularity extract fold zero connid domid
+
+let get_hist_mem ?latest ?earliest ?granularity connid domid =
+  let zero = 0L in
+  let extract { hist_mem = m } = m in
+  let fold a =
+    let len = Array.length a in
+    if len > 0 then
+      Int64.div (Array.fold_left (Int64.add) zero a) (Int64.of_int len)
+    else
+      -1L in
+  _get_hist ?latest ?earliest ?granularity extract fold zero connid domid