Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/shell
diff options
context:
space:
mode:
authorDan Winship <dwinship@redhat.com>2007-08-24 14:28:33 (GMT)
committer Dan Winship <dwinship@redhat.com>2007-08-24 14:28:33 (GMT)
commitebe2b4765e4c17327dc00c4d38ec0c3fc5a468a5 (patch)
tree007e24f8d09b64928b7a7aa2700d8c0f501bf30e /shell
parentc25861bd1dc041c63d9bb9268e5394e28c1d7217 (diff)
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
Diffstat (limited to 'shell')
-rw-r--r--shell/view/home/HomeBox.py2
-rw-r--r--shell/view/home/Makefile.am1
-rw-r--r--shell/view/home/activitiesdonut.py185
-rw-r--r--shell/view/home/proc_smaps.py149
4 files changed, 281 insertions, 56 deletions
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