From ebe2b4765e4c17327dc00c4d38ec0c3fc5a468a5 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 24 Aug 2007 14:28:33 +0000 Subject: Fix up the activity ring drawing to be more accurate and stable. #2030 TODO: move some of this code into shell/model rather than shell/view --- (limited to 'shell') diff --git a/shell/view/home/HomeBox.py b/shell/view/home/HomeBox.py index f340c1a..869c9d0 100644 --- a/shell/view/home/HomeBox.py +++ b/shell/view/home/HomeBox.py @@ -122,7 +122,7 @@ class HomeBox(hippo.CanvasBox, hippo.CanvasItem): self._redraw_id = None def _redraw_activity_ring(self): - self._donut.emit_request_changed() + self._donut.redraw() return True def has_activities(self): diff --git a/shell/view/home/Makefile.am b/shell/view/home/Makefile.am index a052dcf..9beb651 100644 --- a/shell/view/home/Makefile.am +++ b/shell/view/home/Makefile.am @@ -8,5 +8,6 @@ sugar_PYTHON = \ HomeWindow.py \ MeshBox.py \ MyIcon.py \ + proc_smaps.py \ snowflakelayout.py \ transitionbox.py diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index cb9e4b2..8aa3933 100644 --- a/shell/view/home/activitiesdonut.py +++ b/shell/view/home/activitiesdonut.py @@ -18,6 +18,7 @@ import colorsys from gettext import gettext as _ import logging import math +import os import hippo import gobject @@ -29,6 +30,7 @@ from sugar.graphics.palette import Palette from sugar.graphics import style from sugar.graphics import xocolor from sugar import profile +from proc_smaps import ProcSmaps # TODO: rgb_to_html and html_to_rgb are useful elsewhere # we should put this in a common module @@ -48,6 +50,9 @@ def html_to_rgb(html_color): r, g, b = (r / 255.0, g / 255.0, b / 255.0) return (r, g, b) +_MAX_ACTIVITIES = 10 +_MIN_WEDGE_SIZE = 1.0 / _MAX_ACTIVITIES + class ActivityIcon(CanvasIcon): _INTERVAL = 250 @@ -74,6 +79,8 @@ class ActivityIcon(CanvasIcon): self._activity = activity self._pulse_id = 0 + self.size = _MIN_WEDGE_SIZE + palette = Palette(_('Starting...')) self.set_palette(palette) @@ -101,11 +108,9 @@ class ActivityIcon(CanvasIcon): stop_menu_item.show() def _launching_changed_cb(self, activity, pspec): - if activity.props.launching: - self._start_pulsing() - else: + if not activity.props.launching: self._stop_pulsing() - self._setup_palette() + self._setup_palette() def __del__(self): self._cleanup() @@ -166,10 +171,6 @@ class ActivityIcon(CanvasIcon): self._level = 100.0 self.props.xo_color = self._orig_color - # Force the donut to redraw now that we know how much memory - # the activity is using. - self.emit_request_changed() - def _resume_activate_cb(self, menuitem): self.emit('resume') @@ -215,6 +216,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): self.remove(icon) icon._cleanup() self._activities.remove(icon) + self._compute_angles() def _add_activity(self, activity): icon = ActivityIcon(activity) @@ -223,8 +225,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): self.append(icon, hippo.PACK_FIXED) self._activities.append(icon) - - self.emit_paint_needed(0, 0, -1, -1) + self._compute_angles() def _activity_icon_resumed_cb(self, icon): activity = icon.get_activity() @@ -282,43 +283,72 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): activity_host.present() return True - MAX_ACTIVITIES = 10 - MIN_ACTIVITY_WEDGE_SIZE = 1.0 / MAX_ACTIVITIES - - def _get_activity_sizes(self): - # First get the size of each process that hosts an activity, - # and the number of activities it hosts. - process_size = {} + def _update_activity_sizes(self): + # First, get the shell's memory mappings; this memory won't be + # counted against the memory used by activities, since it + # would still be in use even if all activities exited. + shell_mappings = {} + try: + shell_smaps = ProcSmaps(os.getpid()) + for mapping in shell_smaps.mappings: + if mapping.shared_clean > 0 or mapping.shared_dirty > 0: + shell_mappings[mapping.name] = mapping + except Exception, e: + logging.warn('ActivitiesDonut: could not read own smaps: %r' % e) + + # Get the memory mappings of each process that hosts an + # activity, and count how many activity instances each + # activity process hosts, and how many processes are mapping + # each shared library, etc + process_smaps = {} num_activities = {} - total_activity_size = 0 + num_mappings = {} + unknown_size_activities = 0 for activity in self._model: pid = activity.get_pid() if not pid: # Still starting up, hasn't opened a window yet + unknown_size_activities += 1 continue - if process_size.has_key(pid): + if num_activities.has_key(pid): num_activities[pid] += 1 continue try: - statm = open('/proc/%s/statm' % pid) - # We use "RSS" (the second field in /proc/PID/statm) - # for the activity size because that's what ps and top - # use for calculating "%MEM". We multiply by 4 to - # convert from pages to kb. - process_size[pid] = int(statm.readline().split()[1]) * 4 - total_activity_size += process_size[pid] + smaps = ProcSmaps(pid) + _subtract_mappings(smaps, shell_mappings) + for mapping in smaps.mappings: + if mapping.shared_clean > 0 or mapping.shared_dirty > 0: + if num_mappings.has_key(mapping.name): + num_mappings[mapping.name] += 1 + else: + num_mappings[mapping.name] = 1 + process_smaps[pid] = smaps num_activities[pid] = 1 - statm.close() - except IOError: - logging.warn('ActivitiesDonut: could not read /proc/%s/statm' % - pid) - except (IndexError, ValueError): - logging.warn('ActivitiesDonut: /proc/%s/statm was not in ' + - 'expected format' % pid) - - # Next, see how much free memory is left. + except Exception, e: + logging.warn('ActivitiesDonut: could not read /proc/%s/smaps: %r' + % (pid, e)) + + # Compute total memory used per process + process_size = {} + total_activity_size = 0 + for activity in self._model: + pid = activity.get_pid() + if not process_smaps.has_key(pid): + continue + + smaps = process_smaps[pid] + size = 0 + for mapping in smaps.mappings: + size += mapping.private_clean + mapping.private_dirty + if mapping.shared_clean + mapping.shared_dirty > 0: + num = num_mappings[mapping.name] + size += (mapping.shared_clean + mapping.shared_dirty) / num + process_size[pid] = size + total_activity_size += size / num_activities[pid] + + # Now, see how much free memory is left. free_memory = 0 try: meminfo = open('/proc/meminfo') @@ -332,39 +362,85 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): logging.warn('ActivitiesDonut: /proc/meminfo was not in ' + 'expected format') - # Each activity starts with MIN_ACTIVITY_WEDGE_SIZE. The - # remaining space in the donut is allocated proportionately - # among the activities-of-known-size and the free space - used_space = ActivitiesDonut.MIN_ACTIVITY_WEDGE_SIZE * len(self._model) - remaining_space = max(0.0, 1.0 - used_space) + total_memory = float(total_activity_size + free_memory) - total_memory = total_activity_size + free_memory + # Each activity has an ideal size of: + # process_size[pid] / num_activities[pid] / total_memory + # (And the free memory wedge is ideally free_memory / + # total_memory) However, no activity wedge is allowed to be + # smaller than _MIN_WEDGE_SIZE. This means the small + # activities will use up extra space, which would make the + # ring overflow. We fix that by reducing the large activities + # and the free space proportionately. If there are activities + # of unknown size, they are simply carved out of the free + # space. + free_percent = free_memory / total_memory activity_sizes = [] - for activity in self._model: - percent = ActivitiesDonut.MIN_ACTIVITY_WEDGE_SIZE - pid = activity.get_pid() + overflow = 0.0 + reducible = free_percent + for icon in self._activities: + pid = icon.get_activity().get_pid() if process_size.has_key(pid): - size = process_size[pid] / num_activities[pid] - percent += remaining_space * size / total_memory - activity_sizes.append(percent) - - return activity_sizes + icon.size = (process_size[pid] / num_activities[pid] / + total_memory) + if icon.size < _MIN_WEDGE_SIZE: + overflow += _MIN_WEDGE_SIZE - icon.size + icon.size = _MIN_WEDGE_SIZE + else: + reducible += icon.size - _MIN_WEDGE_SIZE + else: + icon.size = _MIN_WEDGE_SIZE + + if reducible > 0.0: + reduction = overflow / reducible + if unknown_size_activities > 0: + unknown_percent = _MIN_WEDGE_SIZE * unknown_size_activities + if (free_percent * (1 - reduction) < unknown_percent): + # The free wedge won't be large enough to fit the + # unknown-size activities. So adjust things + overflow += unknown_percent - free_percent + reducible -= free_percent + reduction = overflow / reducible + + if reduction > 0.0: + for icon in self._activities: + if icon.size > _MIN_WEDGE_SIZE: + icon.size -= (icon.size - _MIN_WEDGE_SIZE) * reduction + + def _subtract_mappings(smaps, mappings_to_remove): + for mapping in smaps.mappings: + if mappings_to_remove.has_key(mapping.name): + mapping.shared_clean = 0 + mapping.shared_dirty = 0 def _compute_angles(self): - percentages = self._get_activity_sizes() self._angles = [] - if len(percentages) == 0: + if len(self._activities) == 0: return + # Normally we don't _update_activity_sizes() when launching a + # new activity; but if the new wedge would overflow the ring + # then we have no choice. + total = reduce(lambda s1,s2: s1 + s2, + [icon.size for icon in self._activities]) + if total > 1.0: + self._update_activity_sizes() + # The first wedge (Journal) should be centered at 6 o'clock - size = percentages[0] * 2 * math.pi - angle = (math.pi - size) / 2 + size = self._activities[0].size or _MIN_WEDGE_SIZE + angle = (math.pi - size * 2 * math.pi) / 2 self._angles.append(angle) - for size in percentages: + for icon in self._activities: + size = icon.size or _MIN_WEDGE_SIZE self._angles.append(self._angles[-1] + size * 2 * math.pi) + def redraw(self): + self._update_activity_sizes() + self._compute_angles() + self.emit_request_changed() + def _get_angles(self, index): return [self._angles[index], self._angles[(index + 1) % len(self._angles)]] @@ -431,7 +507,6 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): radius = (self._get_inner_radius() + self._get_radius()) / 2 - self._compute_angles() for i, icon in enumerate(self._activities): [angle_start, angle_end] = self._get_angles(i) angle = angle_start + (angle_end - angle_start) / 2 diff --git a/shell/view/home/proc_smaps.py b/shell/view/home/proc_smaps.py new file mode 100644 index 0000000..c7a81ec --- /dev/null +++ b/shell/view/home/proc_smaps.py @@ -0,0 +1,149 @@ +#################################################################### +# This class open the /proc/PID/maps and /proc/PID/smaps files +# to get useful information about the real memory usage +#################################################################### + +import os +import logging + +_smaps_has_references = None + +# Parse the /proc/PID/smaps file +class ProcSmaps: + + mappings = [] # Devices information + + def __init__(self, pid): + global _smaps_has_references + if _smaps_has_references is None: + _smaps_has_references = os.path.isfile('/proc/%s/clear_refs' % + os.getpid()) + + smapfile = "/proc/%s/smaps" % pid + self.mappings = [] + + # Coded by Federico Mena (script) + infile = open(smapfile, "r") + input = infile.read() + infile.close() + + lines = input.splitlines() + + num_lines = len (lines) + line_idx = 0 + + # 08065000-08067000 rw-p 0001c000 03:01 147613 /opt/gnome/bin/evolution-2.6 + # Size: 8 kB + # Rss: 8 kB + # Shared_Clean: 0 kB + # Shared_Dirty: 0 kB + # Private_Clean: 8 kB + # Private_Dirty: 0 kB + # Referenced: 4 kb -> Introduced in kernel 2.6.22 + + while num_lines > 0: + fields = lines[line_idx].split (" ", 5) + if len (fields) == 6: + (offsets, permissions, bin_permissions, device, inode, name) = fields + else: + (offsets, permissions, bin_permissions, device, inode) = fields + name = "" + + size = self.parse_smaps_size_line (lines[line_idx + 1]) + rss = self.parse_smaps_size_line (lines[line_idx + 2]) + shared_clean = self.parse_smaps_size_line (lines[line_idx + 3]) + shared_dirty = self.parse_smaps_size_line (lines[line_idx + 4]) + private_clean = self.parse_smaps_size_line (lines[line_idx + 5]) + private_dirty = self.parse_smaps_size_line (lines[line_idx + 6]) + if _smaps_has_references: + referenced = self.parse_smaps_size_line (lines[line_idx + 7]) + else: + referenced = None + name = name.strip () + + mapping = Mapping (size, rss, shared_clean, shared_dirty, \ + private_clean, private_dirty, referenced, permissions, name) + self.mappings.append (mapping) + + if _smaps_has_references: + num_lines -= 8 + line_idx += 8 + else: + num_lines -= 7 + line_idx += 7 + + if _smaps_has_references: + self._clear_reference(pid) + + def _clear_reference(self, pid): + os.system("echo 1 > /proc/%s/clear_refs" % pid) + + # Parses a line of the form "foo: 42 kB" and returns an integer for the "42" field + def parse_smaps_size_line (self, line): + # Rss: 8 kB + fields = line.split () + return int(fields[1]) + +class Mapping: + def __init__ (self, size, rss, shared_clean, shared_dirty, \ + private_clean, private_dirty, referenced, permissions, name): + self.size = size + self.rss = rss + self.shared_clean = shared_clean + self.shared_dirty = shared_dirty + self.private_clean = private_clean + self.private_dirty = private_dirty + self.referenced = referenced + self.permissions = permissions + self.name = name + +# Parse /proc/PID/maps file to get the clean memory usage by process, +# we avoid lines with backed-files +class ProcMaps: + + clean_size = 0 + + def __init__(self, pid): + mapfile = "/proc/%s/maps" % pid + + try: + infile = open(mapfile, "r") + except: + print "Error trying " + mapfile + return None + + sum = 0 + to_data_do = { + "[anon]": self.parse_size_line, + "[heap]": self.parse_size_line + } + + for line in infile: + arr = line.split() + + # Just parse writable mapped areas + if arr[1][1] != "w": + continue + + if len(arr) == 6: + # if we got a backed-file we skip this info + if os.path.isfile(arr[5]): + continue + else: + line_size = to_data_do.get(arr[5], self.skip)(line) + sum += line_size + else: + line_size = self.parse_size_line(line) + sum += line_size + + infile.close() + self.clean_size = sum + + def skip(self, line): + return 0 + + # Parse a maps line and return the mapped size + def parse_size_line(self, line): + start, end = line.split()[0].split('-') + size = int(end, 16) - int(start, 16) + return size -- cgit v0.9.1