Add and use wibox.hierarchy:update()

This function updates a hierarchy if the layout of some widgets changed. It does
nothing on the parts that did not change. This should be more efficient than
recomputing the whole hierarchy whenever something changes.

Once again, this has some positive results on the "benchmark test":

Before:
        create wibox: 0.083016   sec/iter ( 13 iters, 1.161 sec for benchmark)
    update textclock: 0.00391091 sec/iter (271 iters, 3.219 sec for benchmark)
  relayout textclock: 0.00273234 sec/iter (397 iters, 1.087 sec for benchmark)
    redraw textclock: 0.0010191  sec/iter (989 iters, 1.745 sec for benchmark)

After:
        create wibox: 0.083146   sec/iter ( 13 iters, 1.163 sec for benchmark)
    update textclock: 0.00170519 sec/iter (647 iters, 2.201 sec for benchmark)
  relayout textclock: 0.000581637 sec/iter (1880 iters, 1.094 sec for benchmark)
    redraw textclock: 0.0010167  sec/iter (997 iters, 1.773 sec for benchmark)

So again no difference for creating wiboxes (100.16% compared to before). This
time we also have no real difference for creating wiboxes (99.76%). Update (44%)
and relayout (21%) are improved a lot.

Closes https://github.com/awesomeWM/awesome/pull/463.

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2015-09-19 13:57:18 +02:00 committed by Daniel Hahler
parent f7f8420d24
commit dfcbf20d81
3 changed files with 197 additions and 87 deletions

View File

@ -74,19 +74,25 @@ local function do_redraw(self)
-- Relayout
if self._need_relayout or self._need_complete_repaint then
self._need_relayout = false
local old_hierarchy = self._widget_hierarchy
if self._widget_hierarchy and self.widget then
self._widget_hierarchy:update(get_widget_context(self),
self.widget, width, height, self._dirty_area)
else
self._need_complete_repaint = true
if self.widget then
self._widget_hierarchy_callback_arg = {}
self._widget_hierarchy = self.widget and
hierarchy.new(get_widget_context(self), self.widget, width, height,
self._widget_hierarchy = hierarchy.new(get_widget_context(self), self.widget, width, height,
self._redraw_callback, self._layout_callback, self._widget_hierarchy_callback_arg)
else
self._widget_hierarchy = nil
end
end
if old_hierarchy == nil or self._widget_hierarchy == nil or self._need_complete_repaint then
if self._need_complete_repaint then
self._need_complete_repaint = false
self._dirty_area:union_rectangle(cairo.RectangleInt{
x = 0, y = 0, width = width, height = height
})
else
self._dirty_area:union(self._widget_hierarchy:find_differences(old_hierarchy))
end
end

View File

@ -16,48 +16,40 @@ local no_parent = base.no_parent_I_know_what_I_am_doing
local hierarchy = {}
local function hierarchy_new(context, widget, width, height, redraw_callback, layout_callback, callback_arg,
matrix_to_parent, matrix_to_device)
local children = base.layout_widget(no_parent, context, widget, width, height)
local draws_x1, draws_y1, draws_x2, draws_y2 = 0, 0, width, height
local function hierarchy_new(widget, redraw_callback, layout_callback, callback_arg)
local result = {
_matrix = matrix_to_parent,
_matrix_to_device = matrix_to_device,
_widget = widget,
_matrix = matrix.identity,
_matrix_to_device = matrix.identity,
_need_update = true,
_widget = nil,
_redraw_callback = redraw_callback,
_layout_callback = layout_callback,
_callback_arg = callback_arg,
_size = {
width = width,
height = height
width = nil,
height = nil
},
_draw_extents = nil,
_draw_extents = {
x = 0,
y = 0,
width = 0,
height = 0
},
_parent = nil,
_children = {}
}
result._redraw = function() redraw_callback(result, callback_arg) end
result._layout = function() layout_callback(result, callback_arg) end
widget:weak_connect_signal("widget::redraw_needed", result._redraw)
widget:weak_connect_signal("widget::layout_changed", result._layout)
for _, w in ipairs(children or {}) do
local r = hierarchy_new(context, w._widget, w._width, w._height, redraw_callback, layout_callback,
callback_arg, w._matrix, matrix_to_device * w._matrix)
table.insert(result._children, r)
-- Update our drawing extents
local s = r._draw_extents
local px, py, pwidth, pheight = matrix.transform_rectangle(r._matrix,
s.x, s.y, s.width, s.height)
local px2, py2 = px + pwidth, py + pheight
draws_x1 = math.min(draws_x1, px)
draws_y1 = math.min(draws_y1, py)
draws_x2 = math.max(draws_x2, px2)
draws_y2 = math.max(draws_y2, py2)
function result._redraw()
redraw_callback(result, callback_arg)
end
function result._layout()
local h = result
while h do
h._need_update = true
h = h._parent
end
layout_callback(result, callback_arg)
end
result._draw_extents = {
x = draws_x1,
y = draws_y1,
width = draws_x2 - draws_x1,
height = draws_y2 - draws_y1
}
for k, f in pairs(hierarchy) do
if type(f) == "function" then
@ -67,6 +59,102 @@ local function hierarchy_new(context, widget, width, height, redraw_callback, la
return result
end
local hierarchy_update
function hierarchy_update(self, context, widget, width, height, region, matrix_to_parent, matrix_to_device)
if (not self._need_update) and self._widget == widget and
self._size.width == width and self._size.height == height and
matrix.equals(self._matrix, matrix_to_parent) and
matrix.equals(self._matrix_to_device, matrix_to_device) then
-- Nothing changed
return
end
self._need_update = false
local old_x, old_y, old_width, old_height
local old_widget = self._widget
if self._size.width and self._size.height then
local x, y, w, h = matrix.transform_rectangle(self._matrix_to_device, 0, 0, self._size.width, self._size.height)
old_x, old_y = math.floor(x), math.floor(y)
old_width, old_height = math.ceil(x + w) - old_x, math.ceil(y + h) - old_y
else
old_x, old_y, old_width, old_height = 0, 0, 0, 0
end
-- Disconnect old signals
if old_widget and old_widget ~= widget then
self._widget:disconnect_signal("widget::redraw_needed", self._redraw)
self._widget:disconnect_signal("widget::layout_changed", self._layout)
end
-- Save the arguments we need to save
self._widget = widget
self._size.width = width
self._size.height = height
self._matrix = matrix_to_parent
self._matrix_to_device = matrix_to_device
-- Connect signals
if old_widget ~= widget then
widget:weak_connect_signal("widget::redraw_needed", self._redraw)
widget:weak_connect_signal("widget::layout_changed", self._layout)
end
-- Update children
local old_children = self._children
local layout_result = base.layout_widget(no_parent, context, widget, width, height)
self._children = {}
for _, w in ipairs(layout_result or {}) do
local r = table.remove(old_children, 1)
if not r then
r = hierarchy_new(w._widget, self._redraw_callback, self._layout_callback, self._callback_arg)
r._parent = self
end
hierarchy_update(r, context, w._widget, w._width, w._height, region, w._matrix, matrix_to_device * w._matrix)
table.insert(self._children, r)
end
-- Calculate the draw extents
local x1, y1, x2, y2 = 0, 0, width, height
for _, h in ipairs(self._children) do
local px, py, pwidth, pheight = matrix.transform_rectangle(h._matrix, h:get_draw_extents())
x1 = math.min(x1, px)
y1 = math.min(y1, py)
x2 = math.max(x2, px + pwidth)
y2 = math.max(y2, py + pheight)
end
self._draw_extents = {
x = x1, y = y1,
width = x2 - x1,
height = y2 - y1
}
-- Check which part needs to be redrawn
-- Are there any children which were removed? Their area needs a redraw.
for _, h in ipairs(old_children) do
local x, y, width, height = matrix.transform_rectangle(h._matrix_to_device, h:get_draw_extents())
region:union_rectangle(cairo.RectangleInt{
x = x, y = y, width = width, height = height
})
h._parent = nil
end
-- Did we change and need to be redrawn?
local x, y, w, h = matrix.transform_rectangle(self._matrix_to_device, 0, 0, self._size.width, self._size.height)
local new_x, new_y = math.floor(x), math.floor(y)
local new_width, new_height = math.ceil(x + w) - new_x, math.ceil(y + h) - new_y
if new_x ~= old_x or new_y ~= old_y or new_width ~= old_width or new_height ~= old_height or
widget ~= old_widget then
region:union_rectangle(cairo.RectangleInt{
x = old_x, y = old_y, width = old_width, height = old_height
})
region:union_rectangle(cairo.RectangleInt{
x = new_x, y = new_y, width = new_width, height = new_height
})
end
end
--- Create a new widget hierarchy that has no parent.
-- @param context The context in which we are laid out.
-- @param widget The widget that is at the base of the hierarchy.
@ -79,8 +167,23 @@ end
-- @param callback_arg A second argument that is given to the above callbacks.
-- @return A new widget hierarchy
function hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, callback_arg)
return hierarchy_new(context, widget, width, height, redraw_callback, layout_callback, callback_arg,
matrix.identity, matrix.identity)
local result = hierarchy_new(widget, redraw_callback, layout_callback, callback_arg)
result:update(context, widget, width, height)
return result
end
--- Update a widget hierarchy with some new state.
-- @param context The context in which we are laid out.
-- @param widget The widget that is at the base of the hierarchy.
-- @param width The available width for this hierarchy.
-- @param height The available height for this hierarchy.
-- @param[opt] region A region to use for accumulating changed parts
-- @return A cairo region describing the changed parts (either the `region`
-- argument or a new, internally created region).
function hierarchy:update(context, widget, width, height, region)
local region = region or cairo.Region.create()
hierarchy_update(self, context, widget, width, height, region, self._matrix, self._matrix_to_device)
return region
end
--- Get the widget that this hierarchy manages.
@ -142,40 +245,6 @@ function hierarchy:get_children()
return self._children
end
--- Compare two widget hierarchies and compute a cairo Region that contains all
-- rectangles that aren't the same between both hierarchies.
-- @param other The hierarchy to compare with
-- @return A cairo Region containing the differences.
function hierarchy:find_differences(other)
local region = cairo.Region.create()
local function needs_redraw(h)
local m = h:get_matrix_to_device()
local p = h._draw_extents
local x, y, width, height = matrix.transform_rectangle(m, p.x, p.y, p.width, p.height)
local x1, y1 = math.floor(x), math.floor(y)
local x2, y2 = math.ceil(x + width), math.ceil(y + height)
region:union_rectangle(cairo.RectangleInt({
x = x1, y = y1, width = x2 - x1, height = y2 - y1
}))
end
local compare
compare = function(self, other)
local s_size, o_size = self._size, other._size
if s_size.width ~= o_size.width or s_size.height ~= o_size.height or
#self._children ~= #other._children or self._widget ~= other._widget or
not matrix.equals(self._matrix, other._matrix) then
needs_redraw(self)
needs_redraw(other)
else
for i = 1, #self._children do
compare(self._children[i], other._children[i])
end
end
end
compare(self, other)
return region
end
--- Does the given cairo context have an empty clip (aka "no drawing possible")?
local function empty_clip(cr)
local x, y, width, height = cr:clip_extents()

View File

@ -188,7 +188,7 @@ describe("wibox.hierarchy", function()
end)
end)
describe("find_differences", function()
describe("update", function()
local child, intermediate, parent
local instance
local function nop() end
@ -205,25 +205,60 @@ describe("wibox.hierarchy", function()
instance = hierarchy.new(context, parent, 15, 16, nop, nop)
end)
it("No difference", function()
local context = {}
local instance2 = hierarchy.new(context, parent, 15, 16, nop, nop)
local region = instance:find_differences(instance2)
it("No difference 1", function()
local region = instance:update(context, parent, 15, 16)
assert.is.equal(region:num_rectangles(), 0)
end)
it("No difference 2", function()
local region1 = Region.create()
local region2 = instance:update(context, parent, 15, 16, region1)
assert.is.equal(region1, region2)
assert.is.equal(region2:num_rectangles(), 0)
end)
it("child moved", function()
-- Clear caches and change result of intermediate
intermediate.layout = function()
return { make_child(child, 10, 20, matrix.create_translate(0, 4)) }
end
local context = {}
local instance2 = hierarchy.new(context, parent, 15, 16, nop, nop)
local region = instance:find_differences(instance2)
intermediate:emit_signal("widget::layout_changed")
local region = instance:update(context, parent, 15, 16)
assert.is.equal(region:num_rectangles(), 1)
local rect = region:get_rectangle(0)
-- The widget drew to 4, 5, 10, 20 before and 4, 4, 10, 20 after
assert.is.same({ rect.x, rect.y, rect.width, rect.height }, { 4, 4, 10, 21 })
end)
it("child disappears", function()
-- Clear caches and change result of intermediate
intermediate.layout = function() end
intermediate:emit_signal("widget::layout_changed")
local region = instance:update(context, parent, 15, 16)
assert.is.equal(region:num_rectangles(), 1)
local rect = region:get_rectangle(0)
-- The child was drawn to 4, 5, 10, 20
assert.is.same({ rect.x, rect.y, rect.width, rect.height }, { 4, 5, 10, 20 })
end)
it("widget changed", function()
-- Clear caches and change result of parent
local new_intermediate = make_widget({
make_child(child, 10, 20, matrix.create_translate(0, 5))
})
parent.layout = function()
return { make_child(new_intermediate, 5, 2, matrix.create_translate(4, 0)) }
end
parent:emit_signal("widget::layout_changed")
local region = instance:update(context, parent, 15, 16)
assert.is.equal(region:num_rectangles(), 1)
local rect = region:get_rectangle(0)
-- Intermediate drew to 4, 0, 5, 2 (and so does new_intermediate)
assert.is.same({ rect.x, rect.y, rect.width, rect.height }, { 4, 0, 5, 2 })
end)
end)
end)