mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-22 16:26:39 +00:00
1099 lines
33 KiB
Python
1099 lines
33 KiB
Python
# -*- coding: utf-8; mode: python; -*-
|
|
#
|
|
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
|
|
#
|
|
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the Free
|
|
# Software Foundation; either version 3 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
# more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""GStreamer Debug Viewer timeline widget plugin."""
|
|
|
|
import logging
|
|
|
|
from GstDebugViewer import Common, Data
|
|
from GstDebugViewer.GUI.colors import LevelColorThemeTango, ThreadColorThemeTango
|
|
from GstDebugViewer.Plugins import FeatureBase, PluginBase
|
|
|
|
from gettext import gettext as _
|
|
from gi.repository import GObject
|
|
from gi.repository import Gtk
|
|
from gi.repository import Gdk
|
|
import cairo
|
|
|
|
|
|
def iter_model_reversed(model):
|
|
|
|
count = model.iter_n_children(None)
|
|
for i in range(count - 1, 0, -1):
|
|
yield model[i]
|
|
|
|
|
|
class LineFrequencySentinel (object):
|
|
|
|
def __init__(self, model):
|
|
|
|
self.model = model
|
|
self.clear()
|
|
|
|
def clear(self):
|
|
|
|
self.data = None
|
|
self.n_partitions = None
|
|
self.partitions = None
|
|
self.step = None
|
|
self.ts_range = None
|
|
|
|
def _search_ts(self, target_ts, first_index, last_index):
|
|
|
|
model_get = self.model.get_value
|
|
model_iter_nth_child = self.model.iter_nth_child
|
|
col_id = self.model.COL_TIME
|
|
|
|
# TODO: Rewrite using a lightweight view object + bisect.
|
|
|
|
while True:
|
|
middle = (last_index - first_index) // 2 + first_index
|
|
if middle == first_index:
|
|
return first_index
|
|
ts = model_get(model_iter_nth_child(None, middle), col_id)
|
|
if ts < target_ts:
|
|
first_index = middle
|
|
elif ts > target_ts:
|
|
last_index = middle
|
|
else:
|
|
return middle
|
|
|
|
def run_for(self, n):
|
|
|
|
if n == 0:
|
|
raise ValueError("illegal value for n")
|
|
|
|
self.n_partitions = n
|
|
|
|
def process(self):
|
|
|
|
model = self.model
|
|
result = []
|
|
partitions = []
|
|
|
|
first_ts = None
|
|
for row in self.model:
|
|
first_ts = row[model.COL_TIME]
|
|
if first_ts is not None:
|
|
break
|
|
|
|
if first_ts is None:
|
|
return
|
|
|
|
last_ts = None
|
|
i = 0
|
|
UNPARSABLE_LIMIT = 500
|
|
for row in iter_model_reversed(self.model):
|
|
last_ts = row[model.COL_TIME]
|
|
# FIXME: We ignore 0 here (unparsable lines!), this should be
|
|
# handled differently!
|
|
i += 1
|
|
if i == UNPARSABLE_LIMIT:
|
|
break
|
|
if last_ts:
|
|
last_index = row.path[0]
|
|
break
|
|
|
|
if last_ts is None or last_ts < first_ts:
|
|
return
|
|
|
|
step = int(float(last_ts - first_ts) / float(self.n_partitions))
|
|
|
|
YIELD_LIMIT = 100
|
|
limit = YIELD_LIMIT
|
|
|
|
first_index = 0
|
|
target_ts = first_ts + step
|
|
old_found = 0
|
|
while target_ts < last_ts:
|
|
limit -= 1
|
|
if limit == 0:
|
|
limit = YIELD_LIMIT
|
|
yield True
|
|
found = self._search_ts(target_ts, first_index, last_index)
|
|
result.append(found - old_found)
|
|
partitions.append(found)
|
|
old_found = found
|
|
first_index = found
|
|
target_ts += step
|
|
|
|
if step == 0:
|
|
result = []
|
|
partitions = []
|
|
|
|
self.step = step
|
|
self.data = result
|
|
self.partitions = partitions
|
|
self.ts_range = (first_ts, last_ts,)
|
|
|
|
|
|
class LevelDistributionSentinel (object):
|
|
|
|
def __init__(self, freq_sentinel, model):
|
|
|
|
self.freq_sentinel = freq_sentinel
|
|
self.model = model
|
|
self.data = []
|
|
|
|
def clear(self):
|
|
|
|
del self.data[:]
|
|
|
|
def process(self):
|
|
|
|
MAX_LEVELS = 9
|
|
YIELD_LIMIT = 10000
|
|
y = YIELD_LIMIT
|
|
|
|
model_get = self.model.get_value
|
|
model_next = self.model.iter_next
|
|
id_time = self.model.COL_TIME
|
|
id_level = self.model.COL_LEVEL
|
|
del self.data[:]
|
|
data = self.data
|
|
i = 0
|
|
partitions_i = 0
|
|
partitions = self.freq_sentinel.partitions
|
|
counts = [0] * MAX_LEVELS
|
|
tree_iter = self.model.get_iter_first()
|
|
|
|
if not partitions:
|
|
return
|
|
|
|
level_index = 0
|
|
level_iter = None
|
|
|
|
finished = False
|
|
while tree_iter:
|
|
y -= 1
|
|
if y == 0:
|
|
y = YIELD_LIMIT
|
|
yield True
|
|
if level_iter is None:
|
|
stop_index = level_index + 512
|
|
levels = self.model.get_value_range(id_level,
|
|
level_index, stop_index)
|
|
level_index = stop_index
|
|
level_iter = iter(levels)
|
|
try:
|
|
level = level_iter.__next__()
|
|
except StopIteration:
|
|
level_iter = None
|
|
continue
|
|
while i > partitions[partitions_i]:
|
|
data.append(tuple(counts))
|
|
counts = [0] * MAX_LEVELS
|
|
partitions_i += 1
|
|
if partitions_i == len(partitions):
|
|
finished = True
|
|
break
|
|
if finished:
|
|
break
|
|
counts[level] += 1
|
|
i += 1
|
|
|
|
# Now handle the last one:
|
|
data.append(tuple(counts))
|
|
|
|
yield False
|
|
|
|
|
|
class UpdateProcess (object):
|
|
|
|
def __init__(self, freq_sentinel, dist_sentinel):
|
|
|
|
self.freq_sentinel = freq_sentinel
|
|
self.dist_sentinel = dist_sentinel
|
|
self.is_running = False
|
|
self.dispatcher = Common.Data.GSourceDispatcher()
|
|
|
|
def __process(self):
|
|
|
|
if self.freq_sentinel is None or self.dist_sentinel is None:
|
|
return
|
|
|
|
self.is_running = True
|
|
|
|
for x in self.freq_sentinel.process():
|
|
yield True
|
|
|
|
self.handle_sentinel_finished(self.freq_sentinel)
|
|
|
|
for x in self.dist_sentinel.process():
|
|
yield True
|
|
self.handle_sentinel_progress(self.dist_sentinel)
|
|
|
|
self.is_running = False
|
|
|
|
self.handle_sentinel_finished(self.dist_sentinel)
|
|
self.handle_process_finished()
|
|
|
|
yield False
|
|
|
|
def run(self):
|
|
|
|
if self.is_running:
|
|
return
|
|
|
|
self.dispatcher(self.__process())
|
|
|
|
def abort(self):
|
|
|
|
if not self.is_running:
|
|
return
|
|
|
|
self.dispatcher.cancel()
|
|
self.is_running = False
|
|
|
|
def handle_sentinel_progress(self, sentinel):
|
|
|
|
pass
|
|
|
|
def handle_sentinel_finished(self, sentinel):
|
|
|
|
pass
|
|
|
|
def handle_process_finished(self):
|
|
|
|
pass
|
|
|
|
|
|
class VerticalTimelineWidget (Gtk.DrawingArea):
|
|
|
|
__gtype_name__ = "GstDebugViewerVerticalTimelineWidget"
|
|
|
|
def __init__(self, log_view):
|
|
|
|
GObject.GObject.__init__(self)
|
|
|
|
self.logger = logging.getLogger("ui.vtimeline")
|
|
|
|
self.log_view = log_view
|
|
self.theme = ThreadColorThemeTango()
|
|
self.params = None
|
|
self.thread_colors = {}
|
|
self.next_thread_color = 0
|
|
|
|
try:
|
|
self.set_tooltip_text(_("Vertical timeline\n"
|
|
"Different colors represent different threads"))
|
|
except AttributeError:
|
|
# Compatibility.
|
|
pass
|
|
|
|
def do_draw(self, ctx):
|
|
|
|
alloc = self.get_allocation()
|
|
x = alloc.x
|
|
y = alloc.y
|
|
w = alloc.width
|
|
h = alloc.height
|
|
|
|
# White background rectangle.
|
|
ctx.set_line_width(0.)
|
|
ctx.rectangle(0, 0, w, h)
|
|
ctx.set_source_rgb(1., 1., 1.)
|
|
ctx.fill()
|
|
ctx.new_path()
|
|
|
|
if self.params is None:
|
|
self.__update_params()
|
|
|
|
if self.params is None:
|
|
return
|
|
|
|
first_y, cell_height, data = self.params
|
|
if len(data) < 2:
|
|
return
|
|
first_ts, last_ts = data[0][0], data[-1][0]
|
|
ts_range = last_ts - first_ts
|
|
if ts_range == 0:
|
|
return
|
|
|
|
ctx.set_line_width(1.)
|
|
ctx.set_source_rgb(0., 0., 0.)
|
|
|
|
half_height = cell_height // 2 - .5
|
|
quarter_height = cell_height // 4 - .5
|
|
first_y += half_height
|
|
for i, i_data in enumerate(data):
|
|
ts, thread = i_data
|
|
if thread in self.thread_colors:
|
|
ctx.set_source_rgb(*self.thread_colors[thread])
|
|
else:
|
|
self.next_thread_color += 1
|
|
if self.next_thread_color == len(self.theme.colors):
|
|
self.next_thread_color = 0
|
|
color = self.theme.colors[
|
|
self.next_thread_color][0].float_tuple()
|
|
self.thread_colors[thread] = color
|
|
ctx.set_source_rgb(*color)
|
|
ts_fraction = float(ts - first_ts) / ts_range
|
|
ts_offset = ts_fraction * h
|
|
row_offset = first_y + i * cell_height
|
|
ctx.move_to(-.5, ts_offset)
|
|
ctx.line_to(half_height, ts_offset)
|
|
ctx.line_to(w - quarter_height, row_offset)
|
|
ctx.stroke()
|
|
ctx.line_to(w - quarter_height, row_offset)
|
|
ctx.line_to(w + .5, row_offset - half_height)
|
|
ctx.line_to(w + .5, row_offset + half_height)
|
|
ctx.fill()
|
|
return True
|
|
|
|
def do_configure_event(self, event):
|
|
|
|
self.params = None
|
|
self.queue_draw()
|
|
|
|
return False
|
|
|
|
def do_get_preferred_width(self):
|
|
|
|
return 64, 64 # FIXME
|
|
|
|
def clear(self):
|
|
|
|
self.params = None
|
|
self.thread_colors.clear()
|
|
self.next_thread_color = 0
|
|
self.queue_draw()
|
|
|
|
def __update_params(self):
|
|
|
|
# FIXME: Ideally we should take the vertical position difference of the
|
|
# view into account (which is 0 with the current UI layout).
|
|
|
|
view = self.log_view
|
|
model = view.get_model()
|
|
visible_range = view.get_visible_range()
|
|
if visible_range is None:
|
|
return
|
|
start_path, end_path = visible_range
|
|
|
|
if not start_path or not end_path:
|
|
return
|
|
|
|
column = view.get_column(0)
|
|
bg_rect = view.get_background_area(start_path, column)
|
|
cell_height = bg_rect.height
|
|
cell_rect = view.get_cell_area(start_path, column)
|
|
try:
|
|
first_y = view.convert_bin_window_to_widget_coords(
|
|
cell_rect.x, cell_rect.y)[1]
|
|
except (AttributeError, SystemError,):
|
|
# AttributeError is with PyGTK before 2.12. SystemError is raised
|
|
# with PyGTK 2.12.0, pygtk bug #479012.
|
|
first_y = cell_rect.y % cell_height
|
|
|
|
global _warn_tree_view_coords
|
|
try:
|
|
_warn_tree_view_coords
|
|
except NameError:
|
|
self.logger.warning("tree view coordinate conversion method "
|
|
"not available, using aproximate offset")
|
|
# Only warn once:
|
|
_warn_tree_view_coords = True
|
|
|
|
data = []
|
|
tree_iter = model.get_iter(start_path)
|
|
if tree_iter is None:
|
|
return
|
|
while model.get_path(tree_iter) != end_path:
|
|
data.append(
|
|
model.get(tree_iter, model.COL_TIME, model.COL_THREAD))
|
|
tree_iter = model.iter_next(tree_iter)
|
|
|
|
self.params = (first_y, cell_height, data,)
|
|
|
|
def update(self):
|
|
|
|
self.params = None
|
|
self.queue_draw()
|
|
|
|
|
|
class TimelineWidget (Gtk.DrawingArea):
|
|
|
|
__gtype_name__ = "GstDebugViewerTimelineWidget"
|
|
|
|
__gsignals__ = {"change-position": (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_INT,),)}
|
|
|
|
def __init__(self):
|
|
|
|
GObject.GObject.__init__(self)
|
|
|
|
self.logger = logging.getLogger("ui.timeline")
|
|
|
|
self.add_events(Gdk.EventMask.BUTTON1_MOTION_MASK |
|
|
Gdk.EventMask.BUTTON_PRESS_MASK |
|
|
Gdk.EventMask.BUTTON_RELEASE_MASK)
|
|
|
|
self.process = UpdateProcess(None, None)
|
|
self.process.handle_sentinel_progress = self.__handle_sentinel_progress
|
|
self.process.handle_sentinel_finished = self.__handle_sentinel_finished
|
|
|
|
self.model = None
|
|
self.__offscreen = None
|
|
self.__offscreen_size = (0, 0)
|
|
self.__offscreen_dirty = (0, 0)
|
|
|
|
self.__position_ts_range = None
|
|
|
|
try:
|
|
self.set_tooltip_text(_("Log event histogram\n"
|
|
"Different colors represent different log-levels"))
|
|
except AttributeError:
|
|
# Compatibility.
|
|
pass
|
|
|
|
def __handle_sentinel_progress(self, sentinel):
|
|
|
|
if sentinel == self.process.dist_sentinel:
|
|
old_progress = self.__dist_sentinel_progress
|
|
new_progress = len(sentinel.data)
|
|
if new_progress - old_progress >= 32:
|
|
self.__invalidate_offscreen(old_progress, new_progress)
|
|
self.__dist_sentinel_progress = new_progress
|
|
|
|
def __handle_sentinel_finished(self, sentinel):
|
|
|
|
if sentinel == self.process.freq_sentinel:
|
|
self.__invalidate_offscreen(0, -1)
|
|
else:
|
|
self.__invalidate_offscreen(self.__dist_sentinel_progress, -1)
|
|
|
|
def __ensure_offscreen(self):
|
|
|
|
alloc = self.get_allocation()
|
|
if self.__offscreen_size == (alloc.width, alloc.height):
|
|
return
|
|
|
|
self.__offscreen = cairo.ImageSurface(
|
|
cairo.FORMAT_ARGB32, alloc.width, alloc.height)
|
|
self.__offscreen_size = (alloc.width, alloc.height)
|
|
self.__offscreen_dirty = (0, alloc.width)
|
|
if not self.__offscreen:
|
|
self.__offscreen_size = (0, 0)
|
|
raise ValueError("could not obtain offscreen image surface")
|
|
|
|
def __invalidate_offscreen(self, start, stop):
|
|
|
|
alloc = self.get_allocation()
|
|
if stop < 0:
|
|
stop += alloc.width
|
|
|
|
dirty_start, dirty_stop = self.__offscreen_dirty
|
|
if dirty_start != dirty_stop:
|
|
dirty_start = min(dirty_start, start)
|
|
dirty_stop = max(dirty_stop, stop)
|
|
else:
|
|
dirty_start = start
|
|
dirty_stop = stop
|
|
self.__offscreen_dirty = (dirty_start, dirty_stop)
|
|
|
|
# Just like in __draw_offscreen. FIXME: Need this in one place!
|
|
start -= 8
|
|
stop += 8
|
|
self.queue_draw_area(start, 0, stop - start, alloc.height)
|
|
|
|
def __draw_from_offscreen(self, ctx):
|
|
|
|
if not self.props.visible:
|
|
return
|
|
|
|
alloc = self.get_allocation()
|
|
offscreen_width, offscreen_height = self.__offscreen_size
|
|
rect = Gdk.Rectangle() # TODO: damage region
|
|
rect.x, rect.y, rect.width, rect.height = 0, 0, alloc.width, alloc.height
|
|
|
|
# Fill the background (where the offscreen pixmap doesn't fit) with
|
|
# white. This happens after enlarging the window, until all sentinels
|
|
# have finished running.
|
|
if offscreen_width < alloc.width or offscreen_height < alloc.height:
|
|
ctx.rectangle(rect.x, rect.y, rect.width, rect.height)
|
|
ctx.clip()
|
|
|
|
if offscreen_width < alloc.width:
|
|
ctx.rectangle(
|
|
offscreen_width, 0, alloc.width, offscreen_height)
|
|
if offscreen_height < alloc.height:
|
|
ctx.new_path()
|
|
ctx.rectangle(0, offscreen_height, alloc.width, alloc.height)
|
|
|
|
ctx.set_line_width(0.)
|
|
ctx.set_source_rgb(1., 1., 1.)
|
|
ctx.fill()
|
|
|
|
ctx.set_source_surface(self.__offscreen)
|
|
ctx.rectangle(rect.x, rect.y, rect.width, rect.height)
|
|
ctx.paint()
|
|
|
|
self.__draw_position(ctx, clip=rect)
|
|
|
|
def update(self, model):
|
|
|
|
self.clear()
|
|
self.model = model
|
|
|
|
if model is not None:
|
|
self.__dist_sentinel_progress = 0
|
|
self.process.freq_sentinel = LineFrequencySentinel(model)
|
|
self.process.dist_sentinel = LevelDistributionSentinel(
|
|
self.process.freq_sentinel, model)
|
|
width = self.get_allocation().width
|
|
self.process.freq_sentinel.run_for(width)
|
|
self.process.run()
|
|
|
|
def clear(self):
|
|
|
|
self.model = None
|
|
self.process.abort()
|
|
self.process.freq_sentinel = None
|
|
self.process.dist_sentinel = None
|
|
self.__invalidate_offscreen(0, -1)
|
|
|
|
def update_position(self, start_ts, end_ts):
|
|
|
|
if not self.process.freq_sentinel:
|
|
return
|
|
|
|
if not self.process.freq_sentinel.data:
|
|
return
|
|
|
|
alloc = self.get_allocation()
|
|
|
|
# Queue old position rectangle for redraw:
|
|
if self.__position_ts_range is not None:
|
|
start, stop = self.ts_range_to_position(*self.__position_ts_range)
|
|
self.queue_draw_area(start - 1, 0, stop - start + 2, alloc.height)
|
|
# And the new one:
|
|
start, stop = self.ts_range_to_position(start_ts, end_ts)
|
|
self.queue_draw_area(start - 1, 0, stop - start + 2, alloc.height)
|
|
|
|
self.__position_ts_range = (start_ts, end_ts,)
|
|
|
|
def find_indicative_time_step(self):
|
|
|
|
MINIMUM_PIXEL_STEP = 32
|
|
time_per_pixel = self.process.freq_sentinel.step
|
|
return 32 # FIXME use self.freq_sentinel.step and len (self.process.freq_sentinel.data)
|
|
|
|
def __draw_offscreen(self):
|
|
|
|
dirty_start, dirty_stop = self.__offscreen_dirty
|
|
if dirty_start == dirty_stop:
|
|
return
|
|
|
|
self.__offscreen_dirty = (0, 0)
|
|
width, height = self.__offscreen_size
|
|
|
|
ctx = cairo.Context(self.__offscreen)
|
|
|
|
# Indicator (triangle) size is 8, so we need to draw surrounding areas
|
|
# a bit:
|
|
dirty_start -= 8
|
|
dirty_stop += 8
|
|
dirty_start = max(dirty_start, 0)
|
|
dirty_stop = min(dirty_stop, width)
|
|
|
|
ctx.rectangle(dirty_start, 0., dirty_stop, height)
|
|
ctx.clip()
|
|
|
|
# White background rectangle.
|
|
ctx.set_line_width(0.)
|
|
ctx.rectangle(0, 0, width, height)
|
|
ctx.set_source_rgb(1., 1., 1.)
|
|
ctx.fill()
|
|
ctx.new_path()
|
|
|
|
# Horizontal reference lines.
|
|
ctx.set_line_width(1.)
|
|
ctx.set_source_rgb(.95, .95, .95)
|
|
for i in range(height // 16):
|
|
y = i * 16 - .5
|
|
ctx.move_to(0, y)
|
|
ctx.line_to(width, y)
|
|
ctx.stroke()
|
|
|
|
if self.process.freq_sentinel is None:
|
|
return
|
|
|
|
# Vertical reference lines.
|
|
pixel_step = self.find_indicative_time_step()
|
|
ctx.set_source_rgb(.9, .9, .9)
|
|
start = dirty_start - dirty_start % pixel_step
|
|
for x in range(start + pixel_step, dirty_stop, pixel_step):
|
|
ctx.move_to(x - .5, 0)
|
|
ctx.line_to(x - .5, height)
|
|
ctx.stroke()
|
|
|
|
if not self.process.freq_sentinel.data:
|
|
self.logger.debug("frequency sentinel has no data yet")
|
|
return
|
|
|
|
ctx.translate(dirty_start, 0.)
|
|
|
|
maximum = max(self.process.freq_sentinel.data)
|
|
|
|
ctx.set_source_rgb(0., 0., 0.)
|
|
data = self.process.freq_sentinel.data[dirty_start:dirty_stop]
|
|
self.__draw_graph(ctx, height, maximum, data)
|
|
|
|
if not self.process.dist_sentinel.data:
|
|
self.logger.debug("level distribution sentinel has no data yet")
|
|
return
|
|
|
|
colors = LevelColorThemeTango().colors
|
|
dist_data = self.process.dist_sentinel.data[dirty_start:dirty_stop]
|
|
|
|
def cumulative_level_counts(*levels):
|
|
for level_counts in dist_data:
|
|
yield sum((level_counts[level] for level in levels))
|
|
|
|
level = Data.debug_level_info
|
|
levels_prev = (Data.debug_level_trace,
|
|
Data.debug_level_fixme,
|
|
Data.debug_level_log,
|
|
Data.debug_level_debug,)
|
|
ctx.set_source_rgb(*(colors[level][1].float_tuple()))
|
|
self.__draw_graph(ctx, height, maximum,
|
|
list(cumulative_level_counts(level, *levels_prev)))
|
|
|
|
level = Data.debug_level_debug
|
|
levels_prev = (Data.debug_level_trace,
|
|
Data.debug_level_fixme,
|
|
Data.debug_level_log,)
|
|
ctx.set_source_rgb(*(colors[level][1].float_tuple()))
|
|
self.__draw_graph(ctx, height, maximum,
|
|
list(cumulative_level_counts(level, *levels_prev)))
|
|
|
|
level = Data.debug_level_log
|
|
levels_prev = (Data.debug_level_trace, Data.debug_level_fixme,)
|
|
ctx.set_source_rgb(*(colors[level][1].float_tuple()))
|
|
self.__draw_graph(ctx, height, maximum,
|
|
list(cumulative_level_counts(level, *levels_prev)))
|
|
|
|
level = Data.debug_level_fixme
|
|
levels_prev = (Data.debug_level_trace,)
|
|
ctx.set_source_rgb(*(colors[level][1].float_tuple()))
|
|
self.__draw_graph(ctx, height, maximum,
|
|
list(cumulative_level_counts(level, *levels_prev)))
|
|
|
|
level = Data.debug_level_trace
|
|
ctx.set_source_rgb(*(colors[level][1].float_tuple()))
|
|
self.__draw_graph(ctx, height, maximum, [
|
|
counts[level] for counts in dist_data])
|
|
|
|
# Draw error and warning triangle indicators:
|
|
|
|
def triangle(ctx, size=8):
|
|
ctx.move_to(-size // 2, 0)
|
|
ctx.line_to((size + 1) // 2, 0)
|
|
ctx.line_to(0, size / 1.41)
|
|
ctx.close_path()
|
|
|
|
for level in (Data.debug_level_warning, Data.debug_level_error,):
|
|
ctx.set_source_rgb(*(colors[level][1].float_tuple()))
|
|
for i, counts in enumerate(dist_data):
|
|
if counts[level] == 0:
|
|
continue
|
|
ctx.translate(i, 0.)
|
|
triangle(ctx)
|
|
ctx.fill()
|
|
ctx.translate(-i, 0.)
|
|
|
|
def __draw_graph(self, ctx, height, maximum, data):
|
|
|
|
if not data:
|
|
return
|
|
|
|
if maximum:
|
|
heights = [height * float(d) / maximum for d in data]
|
|
else:
|
|
heights = [0. for d in data]
|
|
|
|
ctx.move_to(0, height)
|
|
for i in range(len(heights)):
|
|
ctx.line_to(i - .5, height - heights[i] + .5)
|
|
|
|
ctx.line_to(i, height)
|
|
ctx.close_path()
|
|
|
|
ctx.fill()
|
|
|
|
def __have_position(self):
|
|
|
|
if ((self.process is not None) and
|
|
(self.process.freq_sentinel is not None) and
|
|
(self.process.freq_sentinel.ts_range is not None)):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def ts_range_to_position(self, start_ts, end_ts):
|
|
|
|
if not self.__have_position():
|
|
return (0, 0)
|
|
|
|
first_ts, last_ts = self.process.freq_sentinel.ts_range
|
|
step = self.process.freq_sentinel.step
|
|
if step == 0:
|
|
return (0, 0)
|
|
|
|
position1 = int(float(start_ts - first_ts) / step)
|
|
position2 = int(float(end_ts - first_ts) / step)
|
|
|
|
return (position1, position2)
|
|
|
|
def __draw_position(self, ctx, clip=None):
|
|
|
|
if not self.__have_position() or self.__position_ts_range is None:
|
|
if not self.__have_position():
|
|
self.logger.debug("have no positions")
|
|
else:
|
|
self.logger.debug("have no positions_ts_range")
|
|
return
|
|
|
|
start_ts, end_ts = self.__position_ts_range
|
|
position1, position2 = self.ts_range_to_position(start_ts, end_ts)
|
|
|
|
if clip:
|
|
if clip.x + clip.width < position1 - 1 or clip.x > position2 + 1:
|
|
self.logger.debug(
|
|
"outside of clip range: %d + %d, pos: %d, %d", clip.x, clip.width, position1, position2)
|
|
return
|
|
ctx.rectangle(clip.x, clip.y, clip.width, clip.height)
|
|
ctx.clip()
|
|
|
|
height = self.get_allocation().height
|
|
|
|
line_width = position2 - position1
|
|
if line_width <= 1:
|
|
ctx.set_source_rgb(1., 0., 0.)
|
|
ctx.set_line_width(1.)
|
|
ctx.move_to(position1 + .5, 0)
|
|
ctx.line_to(position1 + .5, height)
|
|
ctx.stroke()
|
|
else:
|
|
ctx.set_source_rgba(1., 0., 0., .5)
|
|
ctx.rectangle(position1, 0, line_width, height)
|
|
ctx.fill()
|
|
|
|
def do_draw(self, cr):
|
|
|
|
self.__ensure_offscreen()
|
|
self.__draw_offscreen()
|
|
self.__draw_from_offscreen(cr)
|
|
|
|
return True
|
|
|
|
def do_configure_event(self, event):
|
|
|
|
self.logger.debug("widget size configured to %ix%i",
|
|
event.width, event.height)
|
|
|
|
if event.width < 16:
|
|
return False
|
|
|
|
self.update(self.model)
|
|
|
|
return False
|
|
|
|
def do_get_preferred_height(self):
|
|
|
|
return 64, 64 # FIXME:
|
|
|
|
def do_button_press_event(self, event):
|
|
|
|
if event.button != 1:
|
|
return False
|
|
|
|
# TODO: Check if clicked inside a warning/error indicator triangle and
|
|
# navigate there.
|
|
|
|
if not self.has_grab():
|
|
self.grab_add()
|
|
self.props.has_tooltip = False
|
|
|
|
pos = int(event.x)
|
|
self.emit("change-position", pos)
|
|
return True
|
|
|
|
def do_button_release_event(self, event):
|
|
|
|
if event.button != 1:
|
|
return False
|
|
|
|
if self.has_grab():
|
|
self.grab_remove()
|
|
self.props.has_tooltip = True
|
|
|
|
return True
|
|
|
|
def do_motion_notify_event(self, event):
|
|
|
|
if event.get_state() & Gdk.ModifierType.BUTTON1_MASK:
|
|
self.emit("change-position", int(event.x))
|
|
Gdk.event_request_motions(event)
|
|
return True
|
|
else:
|
|
self._handle_motion(event.x, event.y)
|
|
Gdk.event_request_motions(event)
|
|
return False
|
|
|
|
def _handle_motion(self, x, y):
|
|
|
|
# TODO: Prelight warning and error indicator triangles.
|
|
|
|
pass
|
|
|
|
|
|
class AttachedWindow (object):
|
|
|
|
def __init__(self, feature, window):
|
|
|
|
self.window = window
|
|
|
|
ui = window.ui_manager
|
|
|
|
ui.insert_action_group(feature.action_group, 0)
|
|
|
|
self.merge_id = ui.new_merge_id()
|
|
ui.add_ui(self.merge_id, "/menubar/ViewMenu/ViewMenuAdditions",
|
|
"ViewTimeline", "show-timeline",
|
|
Gtk.UIManagerItemType.MENUITEM, False)
|
|
|
|
ui.add_ui(self.merge_id, "/", "TimelineContextMenu", None,
|
|
Gtk.UIManagerItemType.POPUP, False)
|
|
# TODO: Make hide before/after operate on the partition that the mouse
|
|
# is pointed at instead of the currently selected line.
|
|
# ui.add_ui (self.merge_id, "/TimelineContextMenu", "TimelineHideLinesBefore",
|
|
# "hide-before-line", Gtk.UIManagerItemType.MENUITEM, False)
|
|
# ui.add_ui (self.merge_id, "/TimelineContextMenu", "TimelineHideLinesAfter",
|
|
# "hide-after-line", Gtk.UIManagerItemType.MENUITEM, False)
|
|
ui.add_ui(
|
|
self.merge_id, "/TimelineContextMenu", "TimelineShowHiddenLines",
|
|
"show-hidden-lines", Gtk.UIManagerItemType.MENUITEM, False)
|
|
|
|
box = window.get_top_attach_point()
|
|
|
|
self.timeline = TimelineWidget()
|
|
self.timeline.connect("change-position",
|
|
self.handle_timeline_change_position)
|
|
box.pack_start(self.timeline, False, False, 0)
|
|
self.timeline.hide()
|
|
|
|
self.popup = ui.get_widget("/TimelineContextMenu")
|
|
Common.GUI.widget_add_popup_menu(self.timeline, self.popup)
|
|
|
|
box = window.get_side_attach_point()
|
|
|
|
self.vtimeline = VerticalTimelineWidget(self.window.log_view)
|
|
box.pack_start(self.vtimeline, False, False, 0)
|
|
self.vtimeline.hide()
|
|
|
|
handler = self.handle_log_view_adjustment_value_changed
|
|
adjustment = window.widgets.log_view_scrolled_window.props.vadjustment
|
|
adjustment.connect("value-changed", handler)
|
|
|
|
handler = self.handle_show_action_toggled
|
|
action = feature.action_group.get_action("show-timeline")
|
|
action.connect("toggled", handler)
|
|
handler(action)
|
|
|
|
handler = self.handle_log_view_notify_model
|
|
self.notify_model_id = window.log_view.connect(
|
|
"notify::model", handler)
|
|
|
|
self.idle_scroll_path = None
|
|
self.idle_scroll_id = None
|
|
|
|
def detach(self, feature):
|
|
|
|
self.window.log_view.disconnect(self.notify_model_id)
|
|
self.notify_model_id = None
|
|
|
|
self.window.ui_manager.remove_ui(self.merge_id)
|
|
self.merge_id = None
|
|
|
|
self.window.ui_manager.remove_action_group(feature.action_group)
|
|
|
|
self.timeline.destroy()
|
|
self.timeline = None
|
|
|
|
self.idle_scroll_path = None
|
|
if self.idle_scroll_id is not None:
|
|
GObject.source_remove(self.idle_scroll_id)
|
|
self.idle_scroll_id = None
|
|
|
|
def handle_detach_log_file(self, log_file):
|
|
|
|
self.timeline.clear()
|
|
self.vtimeline.clear()
|
|
|
|
def handle_log_view_notify_model(self, view, gparam):
|
|
|
|
model = view.get_model()
|
|
|
|
if model is None:
|
|
self.timeline.clear()
|
|
self.vtimeline.clear()
|
|
return
|
|
|
|
self.timeline.update(model)
|
|
|
|
# Need to dispatch these idly with a low priority to avoid triggering a
|
|
# warning in treeview.get_visible_range:
|
|
def idle_update():
|
|
self.update_timeline_position()
|
|
self.vtimeline.update()
|
|
return False
|
|
GObject.idle_add(idle_update, priority=GObject.PRIORITY_LOW)
|
|
|
|
def handle_log_view_adjustment_value_changed(self, adj):
|
|
|
|
# FIXME: If not visible, disconnect this handler!
|
|
if not self.timeline.props.visible:
|
|
return
|
|
|
|
self.update_timeline_position()
|
|
self.vtimeline.update()
|
|
|
|
def update_timeline_position(self):
|
|
|
|
visible_range = self.window.get_range()
|
|
if visible_range is None:
|
|
return
|
|
ts1, ts2 = visible_range
|
|
self.timeline.update_position(ts1, ts2)
|
|
|
|
def handle_show_action_toggled(self, action):
|
|
|
|
show = action.props.active
|
|
|
|
if show:
|
|
self.timeline.show()
|
|
self.vtimeline.show()
|
|
else:
|
|
self.timeline.hide()
|
|
self.vtimeline.hide()
|
|
|
|
def handle_timeline_change_position(self, widget, pos):
|
|
|
|
self.goto_time_position(pos)
|
|
|
|
def goto_time_position(self, pos):
|
|
|
|
if not self.timeline.process.freq_sentinel:
|
|
return True
|
|
|
|
data = self.timeline.process.freq_sentinel.data
|
|
if not data:
|
|
return True
|
|
|
|
if pos < 0:
|
|
pos = 0
|
|
elif pos >= len(data):
|
|
pos = len(data) - 1
|
|
|
|
count = sum(data[:pos + 1])
|
|
|
|
path = (count,)
|
|
self.idle_scroll_path = path
|
|
|
|
if self.idle_scroll_id is None:
|
|
self.idle_scroll_id = GObject.idle_add(self.idle_scroll)
|
|
|
|
return False
|
|
|
|
def idle_scroll(self):
|
|
|
|
self.idle_scroll_id = None
|
|
|
|
if self.idle_scroll_path is None:
|
|
return False
|
|
|
|
path = self.idle_scroll_path
|
|
self.idle_scroll_path = None
|
|
|
|
view = self.window.log_view
|
|
view.scroll_to_cell(path, use_align=True, row_align=.5)
|
|
|
|
return False
|
|
|
|
|
|
class TimelineFeature (FeatureBase):
|
|
|
|
def __init__(self, app):
|
|
|
|
self.logger = logging.getLogger("ui.timeline")
|
|
|
|
self.action_group = Gtk.ActionGroup("TimelineActions")
|
|
self.action_group.add_toggle_actions([("show-timeline",
|
|
None, _("_Timeline"),
|
|
"<Ctrl>t")])
|
|
|
|
self.state = app.state.sections[TimelineState._name]
|
|
|
|
self.attached_windows = {}
|
|
|
|
action = self.action_group.get_action("show-timeline")
|
|
action.props.active = self.state.shown
|
|
action.connect("toggled", self.handle_show_action_toggled)
|
|
|
|
def handle_show_action_toggled(self, action):
|
|
|
|
self.state.shown = action.props.active
|
|
|
|
def handle_attach_window(self, window):
|
|
|
|
self.attached_windows[window] = AttachedWindow(self, window)
|
|
|
|
def handle_detach_window(self, window):
|
|
|
|
attached_window = self.attached_windows.pop(window)
|
|
attached_window.detach(self)
|
|
|
|
def handle_attach_log_file(self, window, log_file):
|
|
|
|
pass
|
|
|
|
def handle_detach_log_file(self, window, log_file):
|
|
|
|
attached_window = self.attached_windows[window]
|
|
attached_window.handle_detach_log_file(log_file)
|
|
|
|
|
|
class TimelineState (Common.GUI.StateSection):
|
|
|
|
_name = "timeline"
|
|
|
|
shown = Common.GUI.StateBool("shown", default=True)
|
|
|
|
|
|
class Plugin (PluginBase):
|
|
|
|
features = (TimelineFeature,)
|
|
|
|
def __init__(self, app):
|
|
|
|
app.state.add_section_class(TimelineState)
|
|
self.state = app.state.sections[TimelineState._name]
|