Add 'Actual' bandwidth stats to summary header
authorIgor Bazhitov <ibazhitov@parallels.com>
Wed, 10 Oct 2012 10:14:49 +0000 (10 14:14 +0400)
committerGuillaume Chazarain <guichaz@gmail.com>
Sun, 3 Feb 2013 17:02:44 +0000 (3 18:02 +0100)
'Total' values in the summary header may look confusing to users.
They represent actual kernel <-> disk I/O bandwidth, while individual
values for processes/threads show process <-> kernel I/O bandwidth.

Rename 'Total' to 'Actual' and add old 'Total' status line that sums
up all individual process/thread bandwidths. Explain the difference
between 'Total' and 'Actual' in the manpage.

iotop.8
iotop/data.py
iotop/ui.py

diff --git a/iotop.8 b/iotop.8
index f059189..68d9edd 100644 (file)
--- a/iotop.8
+++ b/iotop.8
@@ -15,8 +15,16 @@ options need to be enabled in your Linux kernel build configuration.
 iotop displays columns for the I/O bandwidth read and written by each
 process/thread during the sampling period. It also displays the percentage
 of time the thread/process spent while swapping in and while waiting on I/O. For each process, its I/O priority (class/level) is shown.
+.PP
 In addition, the total I/O bandwidth read and written during the sampling
 period is displayed at the top of the interface.
+\fBTotal DISK READ\fR and \fBTotal DISK WRITE\fR values represent total read
+and write bandwidth between processes and kernel threads on the one side and
+kernel block device subsystem on the other. While \fBActual DISK READ\fR and
+\fBActual DISK WRITE\fR values represent corresponding bandwidths for actual
+disk I/O between kernel block device subsystem and underlying hardware (HDD, SSD, etc.).
+Thus \fBTotal\fR and \fBActual\fR values may not be equal at any given moment of time
+due to data caching and I/O operations reordering that take place inside Linux kernel.
 .PP
 Use the left and right arrows to change the sorting, r to reverse the
 sorting order, o to toggle the \-\-only option, p to toggle the \-\-processes option, a to toggle the \-\-accumulated option, q to quit or i to change the priority of a thread or a process' thread(s). Any other key will force a refresh.
index 0e43a2c..6dae4b8 100644 (file)
@@ -433,6 +433,8 @@ class ProcessList(DumpableObject):
         self.duration = new_timestamp - self.timestamp
         self.timestamp = new_timestamp
 
+        total_read = total_write = 0
+
         for tgid in self.list_tgids():
             process = self.get_process(tgid)
             if not process:
@@ -442,9 +444,11 @@ class ProcessList(DumpableObject):
                 stats = self.taskstats_connection.get_single_task_stats(thread)
                 if stats:
                     thread.update_stats(stats)
+                    delta = thread.stats_delta
+                    total_read += delta.read_bytes
+                    total_write += delta.write_bytes
                     thread.mark = False
-
-        return self.vmstat.delta()
+        return (total_read, total_write), self.vmstat.delta()
 
     def refresh_processes(self):
         for process in self.processes.values():
index 938d70e..1184ba2 100644 (file)
@@ -150,9 +150,8 @@ class IOTopUI(object):
             poll.register(sys.stdin.fileno(), select.POLLIN|select.POLLPRI)
         while self.options.iterations is None or \
               iterations < self.options.iterations:
-            total = self.process_list.refresh_processes()
-            total_read, total_write = total
-            self.refresh_display(iterations == 0, total_read, total_write,
+            total, actual = self.process_list.refresh_processes()
+            self.refresh_display(iterations == 0, total, actual,
                                  self.process_list.duration)
             if self.options.iterations is not None:
                 iterations += 1
@@ -412,10 +411,15 @@ class IOTopUI(object):
             del processes[self.height - 2:]
         return list(map(format, processes))
 
-    def refresh_display(self, first_time, total_read, total_write, duration):
-        summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
-                format_bandwidth(self.options, total_read, duration).rjust(14),
-                format_bandwidth(self.options, total_write, duration).rjust(14))
+    def refresh_display(self, first_time, total, actual, duration):
+        summary = [
+                'Total DISK READ : %s | Total DISK WRITE : %s' % (
+                format_bandwidth(self.options, total[0], duration).rjust(14),
+                format_bandwidth(self.options, total[1], duration).rjust(14)),
+                'Actual DISK READ: %s | Actual DISK WRITE: %s' % (
+                format_bandwidth(self.options, actual[0], duration).rjust(14),
+                format_bandwidth(self.options, actual[1], duration).rjust(14))
+        ]
 
         pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
         if self.options.processes:
@@ -429,10 +433,11 @@ class IOTopUI(object):
             titles = ['    TIME'] + titles
             current_time = time.strftime('%H:%M:%S ')
             lines = [current_time + l for l in lines]
-            summary = current_time + summary
+            summary = [current_time + s for s in summary]
         if self.options.batch:
             if self.options.quiet <= 2:
-                print(summary)
+                for s in summary:
+                    print(s)
                 if self.options.quiet <= int(first_time):
                     print(''.join(titles))
             for l in lines:
@@ -440,8 +445,9 @@ class IOTopUI(object):
             sys.stdout.flush()
         else:
             self.win.erase()
-            self.win.addstr(summary[:self.width])
-            self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
+            for i, s in enumerate(summary):
+                self.win.addstr(i, 0, s[:self.width])
+            self.win.hline(len(summary), 0, ord(' ') | curses.A_REVERSE, self.width)
             remaining_cols = self.width
             for i in range(len(titles)):
                 attr = curses.A_REVERSE
@@ -462,11 +468,11 @@ class IOTopUI(object):
             num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
             for i in range(num_lines):
                 try:
-                    self.win.addstr(i + 2, 0, lines[i])
+                    self.win.addstr(i + len(summary) + 1, 0, lines[i])
                 except curses.error:
                     pass
             if status_msg:
-                self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
+                self.win.insstr(self.height - len(summary), 0, status_msg, curses.A_BOLD)
             self.win.refresh()
 
 def run_iotop_window(win, options):