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
This commit is contained in:
Dan Winship 2007-08-24 10:28:33 -04:00
parent c25861bd1d
commit ebe2b4765e
4 changed files with 278 additions and 53 deletions

View File

@ -122,7 +122,7 @@ class HomeBox(hippo.CanvasBox, hippo.CanvasItem):
self._redraw_id = None self._redraw_id = None
def _redraw_activity_ring(self): def _redraw_activity_ring(self):
self._donut.emit_request_changed() self._donut.redraw()
return True return True
def has_activities(self): def has_activities(self):

View File

@ -8,5 +8,6 @@ sugar_PYTHON = \
HomeWindow.py \ HomeWindow.py \
MeshBox.py \ MeshBox.py \
MyIcon.py \ MyIcon.py \
proc_smaps.py \
snowflakelayout.py \ snowflakelayout.py \
transitionbox.py transitionbox.py

View File

@ -18,6 +18,7 @@ import colorsys
from gettext import gettext as _ from gettext import gettext as _
import logging import logging
import math import math
import os
import hippo import hippo
import gobject import gobject
@ -29,6 +30,7 @@ from sugar.graphics.palette import Palette
from sugar.graphics import style from sugar.graphics import style
from sugar.graphics import xocolor from sugar.graphics import xocolor
from sugar import profile from sugar import profile
from proc_smaps import ProcSmaps
# TODO: rgb_to_html and html_to_rgb are useful elsewhere # TODO: rgb_to_html and html_to_rgb are useful elsewhere
# we should put this in a common module # 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) r, g, b = (r / 255.0, g / 255.0, b / 255.0)
return (r, g, b) return (r, g, b)
_MAX_ACTIVITIES = 10
_MIN_WEDGE_SIZE = 1.0 / _MAX_ACTIVITIES
class ActivityIcon(CanvasIcon): class ActivityIcon(CanvasIcon):
_INTERVAL = 250 _INTERVAL = 250
@ -74,6 +79,8 @@ class ActivityIcon(CanvasIcon):
self._activity = activity self._activity = activity
self._pulse_id = 0 self._pulse_id = 0
self.size = _MIN_WEDGE_SIZE
palette = Palette(_('Starting...')) palette = Palette(_('Starting...'))
self.set_palette(palette) self.set_palette(palette)
@ -101,9 +108,7 @@ class ActivityIcon(CanvasIcon):
stop_menu_item.show() stop_menu_item.show()
def _launching_changed_cb(self, activity, pspec): def _launching_changed_cb(self, activity, pspec):
if activity.props.launching: if not activity.props.launching:
self._start_pulsing()
else:
self._stop_pulsing() self._stop_pulsing()
self._setup_palette() self._setup_palette()
@ -166,10 +171,6 @@ class ActivityIcon(CanvasIcon):
self._level = 100.0 self._level = 100.0
self.props.xo_color = self._orig_color 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): def _resume_activate_cb(self, menuitem):
self.emit('resume') self.emit('resume')
@ -215,6 +216,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem):
self.remove(icon) self.remove(icon)
icon._cleanup() icon._cleanup()
self._activities.remove(icon) self._activities.remove(icon)
self._compute_angles()
def _add_activity(self, activity): def _add_activity(self, activity):
icon = ActivityIcon(activity) icon = ActivityIcon(activity)
@ -223,8 +225,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem):
self.append(icon, hippo.PACK_FIXED) self.append(icon, hippo.PACK_FIXED)
self._activities.append(icon) self._activities.append(icon)
self._compute_angles()
self.emit_paint_needed(0, 0, -1, -1)
def _activity_icon_resumed_cb(self, icon): def _activity_icon_resumed_cb(self, icon):
activity = icon.get_activity() activity = icon.get_activity()
@ -282,43 +283,72 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem):
activity_host.present() activity_host.present()
return True return True
MAX_ACTIVITIES = 10 def _update_activity_sizes(self):
MIN_ACTIVITY_WEDGE_SIZE = 1.0 / MAX_ACTIVITIES # 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)
def _get_activity_sizes(self): # Get the memory mappings of each process that hosts an
# First get the size of each process that hosts an activity, # activity, and count how many activity instances each
# and the number of activities it hosts. # activity process hosts, and how many processes are mapping
process_size = {} # each shared library, etc
process_smaps = {}
num_activities = {} num_activities = {}
total_activity_size = 0 num_mappings = {}
unknown_size_activities = 0
for activity in self._model: for activity in self._model:
pid = activity.get_pid() pid = activity.get_pid()
if not pid: if not pid:
# Still starting up, hasn't opened a window yet # Still starting up, hasn't opened a window yet
unknown_size_activities += 1
continue continue
if process_size.has_key(pid): if num_activities.has_key(pid):
num_activities[pid] += 1 num_activities[pid] += 1
continue continue
try: try:
statm = open('/proc/%s/statm' % pid) smaps = ProcSmaps(pid)
# We use "RSS" (the second field in /proc/PID/statm) _subtract_mappings(smaps, shell_mappings)
# for the activity size because that's what ps and top for mapping in smaps.mappings:
# use for calculating "%MEM". We multiply by 4 to if mapping.shared_clean > 0 or mapping.shared_dirty > 0:
# convert from pages to kb. if num_mappings.has_key(mapping.name):
process_size[pid] = int(statm.readline().split()[1]) * 4 num_mappings[mapping.name] += 1
total_activity_size += process_size[pid] else:
num_mappings[mapping.name] = 1
process_smaps[pid] = smaps
num_activities[pid] = 1 num_activities[pid] = 1
statm.close() except Exception, e:
except IOError: logging.warn('ActivitiesDonut: could not read /proc/%s/smaps: %r'
logging.warn('ActivitiesDonut: could not read /proc/%s/statm' % % (pid, e))
pid)
except (IndexError, ValueError):
logging.warn('ActivitiesDonut: /proc/%s/statm was not in ' +
'expected format' % pid)
# Next, see how much free memory is left. # 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 free_memory = 0
try: try:
meminfo = open('/proc/meminfo') meminfo = open('/proc/meminfo')
@ -332,39 +362,85 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem):
logging.warn('ActivitiesDonut: /proc/meminfo was not in ' + logging.warn('ActivitiesDonut: /proc/meminfo was not in ' +
'expected format') 'expected format')
# Each activity starts with MIN_ACTIVITY_WEDGE_SIZE. The total_memory = float(total_activity_size + free_memory)
# 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 = 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 = [] activity_sizes = []
for activity in self._model: overflow = 0.0
percent = ActivitiesDonut.MIN_ACTIVITY_WEDGE_SIZE reducible = free_percent
pid = activity.get_pid() for icon in self._activities:
pid = icon.get_activity().get_pid()
if process_size.has_key(pid): if process_size.has_key(pid):
size = process_size[pid] / num_activities[pid] icon.size = (process_size[pid] / num_activities[pid] /
percent += remaining_space * size / total_memory total_memory)
activity_sizes.append(percent) 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
return activity_sizes 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): def _compute_angles(self):
percentages = self._get_activity_sizes()
self._angles = [] self._angles = []
if len(percentages) == 0: if len(self._activities) == 0:
return 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 # The first wedge (Journal) should be centered at 6 o'clock
size = percentages[0] * 2 * math.pi size = self._activities[0].size or _MIN_WEDGE_SIZE
angle = (math.pi - size) / 2 angle = (math.pi - size * 2 * math.pi) / 2
self._angles.append(angle) 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) 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): def _get_angles(self, index):
return [self._angles[index], return [self._angles[index],
self._angles[(index + 1) % len(self._angles)]] 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 radius = (self._get_inner_radius() + self._get_radius()) / 2
self._compute_angles()
for i, icon in enumerate(self._activities): for i, icon in enumerate(self._activities):
[angle_start, angle_end] = self._get_angles(i) [angle_start, angle_end] = self._get_angles(i)
angle = angle_start + (angle_end - angle_start) / 2 angle = angle_start + (angle_end - angle_start) / 2

View File

@ -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